feat(sessions): filter study history

This commit is contained in:
Luiz Gustavo 2026-07-15 19:37:01 -03:00
parent 535b1adc74
commit 774dc48419
5 changed files with 263 additions and 8 deletions

View file

@ -3969,6 +3969,10 @@ var SessionsTab = class {
this.editingSessionId = null;
this.isCreatingSession = false;
this.pendingDeleteSessionId = null;
this.historySubjectFilter = "";
this.historyTypeFilter = "";
this.historyFromFilter = "";
this.historyToFilter = "";
const repositoryFactory = new EntityRepositoryFactory(dataStore);
this.registerStudySessionUseCase = new RegisterStudySessionUseCase(dataStore, repositoryFactory);
this.deleteStudySessionUseCase = new DeleteStudySessionUseCase(dataStore, repositoryFactory);
@ -4047,10 +4051,18 @@ var SessionsTab = class {
container.appendChild(this.renderCreateSessionForm(data));
}
const subjects = data.subjects.filter((subject) => subject.contestId === activeContest.id);
const recentSessions = DomHelpers.createCard("Hist\xF3rico recente");
const sessions = data.studySessions.filter((session) => session.contestId === activeContest.id).slice().reverse().slice(0, 10);
const recentSessions = DomHelpers.createCard("Hist\xF3rico de sess\xF5es");
const allSessions = data.studySessions.filter((session) => session.contestId === activeContest.id).slice().reverse();
const sessions = this.filterSessions(allSessions);
if (allSessions.length > 0) {
recentSessions.appendChild(this.renderHistoryFilters(subjects));
}
if (sessions.length === 0) {
recentSessions.appendChild(DomHelpers.createParagraph("Nenhuma sess\xE3o registrada."));
recentSessions.appendChild(
DomHelpers.createParagraph(
allSessions.length === 0 ? "Nenhuma sess\xE3o registrada." : "Nenhuma sess\xE3o encontrada com esses filtros."
)
);
} else {
const { container: tableContainer, tbody } = DomHelpers.createCrudTable([
"Data",
@ -4070,6 +4082,72 @@ var SessionsTab = class {
}
container.appendChild(recentSessions);
}
renderHistoryFilters(subjects) {
const subjectSelect = DomHelpers.createSelect(
[
["", "Todas"],
...subjects.map((subject) => [subject.id, subject.name])
],
this.historySubjectFilter
);
const typeSelect = DomHelpers.createSelect(
[
["", "Todos"],
[StudySessionType.PDF, "PDF"],
[StudySessionType.VIDEO, "V\xEDdeo"],
[StudySessionType.QUESTIONS, "Quest\xF5es"]
],
this.historyTypeFilter
);
const fromInput = DomHelpers.createInput("date", "De", this.historyFromFilter);
const toInput = DomHelpers.createInput("date", "At\xE9", this.historyToFilter);
subjectSelect.dataset.sessionFilter = "subject";
typeSelect.dataset.sessionFilter = "type";
fromInput.dataset.sessionFilter = "from";
toInput.dataset.sessionFilter = "to";
subjectSelect.addEventListener("change", async () => {
this.historySubjectFilter = subjectSelect.value;
await this.onUpdate();
});
typeSelect.addEventListener("change", async () => {
this.historyTypeFilter = typeSelect.value;
await this.onUpdate();
});
fromInput.addEventListener("change", async () => {
this.historyFromFilter = fromInput.value;
await this.onUpdate();
});
toInput.addEventListener("change", async () => {
this.historyToFilter = toInput.value;
await this.onUpdate();
});
const filters = DomHelpers.createElement("div", "leif-session-filters");
filters.append(
DomHelpers.createStackedLabel("Mat\xE9ria", subjectSelect),
DomHelpers.createStackedLabel("Tipo", typeSelect),
DomHelpers.createStackedLabel("De", fromInput),
DomHelpers.createStackedLabel("At\xE9", toInput)
);
return filters;
}
filterSessions(sessions) {
return sessions.filter((session) => {
if (this.historySubjectFilter && session.subjectId !== this.historySubjectFilter) {
return false;
}
if (this.historyTypeFilter && session.type !== this.historyTypeFilter) {
return false;
}
const sessionDate = session.studiedAt.slice(0, 10);
if (this.historyFromFilter && sessionDate < this.historyFromFilter) {
return false;
}
if (this.historyToFilter && sessionDate > this.historyToFilter) {
return false;
}
return true;
});
}
renderDisplayRow(session, data) {
const tr = DomHelpers.createElement("tr");
tr.dataset.sessionId = session.id;

View file

@ -880,6 +880,13 @@
min-width: 0;
}
.leif-session-filters {
display: grid;
grid-template-columns: minmax(180px, 1fr) minmax(120px, 0.6fr) minmax(140px, 0.5fr) minmax(140px, 0.5fr);
gap: var(--size-4-3);
align-items: end;
}
@media (max-width: 720px) {
.leif-view {
padding: 14px;
@ -898,6 +905,10 @@
grid-template-columns: 1fr;
}
.leif-session-filters {
grid-template-columns: 1fr;
}
.leif-list-row,
.leif-key-value {
flex-direction: column;

View file

@ -29,6 +29,10 @@ export class SessionsTab {
private editingSessionId: string | null = null;
private isCreatingSession = false;
private pendingDeleteSessionId: string | null = null;
private historySubjectFilter = "";
private historyTypeFilter = "";
private historyFromFilter = "";
private historyToFilter = "";
constructor(
private readonly dataStore: PluginDataStore,
@ -126,15 +130,25 @@ export class SessionsTab {
const subjects = data.subjects.filter((subject) => subject.contestId === activeContest.id);
const recentSessions = DomHelpers.createCard("Histórico recente");
const sessions = data.studySessions
const recentSessions = DomHelpers.createCard("Histórico de sessões");
const allSessions = data.studySessions
.filter((session) => session.contestId === activeContest.id)
.slice()
.reverse()
.slice(0, 10);
.reverse();
const sessions = this.filterSessions(allSessions);
if (allSessions.length > 0) {
recentSessions.appendChild(this.renderHistoryFilters(subjects));
}
if (sessions.length === 0) {
recentSessions.appendChild(DomHelpers.createParagraph("Nenhuma sessão registrada."));
recentSessions.appendChild(
DomHelpers.createParagraph(
allSessions.length === 0
? "Nenhuma sessão registrada."
: "Nenhuma sessão encontrada com esses filtros."
)
);
} else {
const { container: tableContainer, tbody } = DomHelpers.createCrudTable([
"Data",
@ -158,6 +172,80 @@ export class SessionsTab {
container.appendChild(recentSessions);
}
private renderHistoryFilters(subjects: LeifPluginData["subjects"]): HTMLElement {
const subjectSelect = DomHelpers.createSelect(
[
["", "Todas"],
...subjects.map((subject): [string, string] => [subject.id, subject.name])
],
this.historySubjectFilter
);
const typeSelect = DomHelpers.createSelect(
[
["", "Todos"],
[StudySessionType.PDF, "PDF"],
[StudySessionType.VIDEO, "Vídeo"],
[StudySessionType.QUESTIONS, "Questões"]
],
this.historyTypeFilter
);
const fromInput = DomHelpers.createInput("date", "De", this.historyFromFilter);
const toInput = DomHelpers.createInput("date", "Até", this.historyToFilter);
subjectSelect.dataset.sessionFilter = "subject";
typeSelect.dataset.sessionFilter = "type";
fromInput.dataset.sessionFilter = "from";
toInput.dataset.sessionFilter = "to";
subjectSelect.addEventListener("change", async () => {
this.historySubjectFilter = subjectSelect.value;
await this.onUpdate();
});
typeSelect.addEventListener("change", async () => {
this.historyTypeFilter = typeSelect.value;
await this.onUpdate();
});
fromInput.addEventListener("change", async () => {
this.historyFromFilter = fromInput.value;
await this.onUpdate();
});
toInput.addEventListener("change", async () => {
this.historyToFilter = toInput.value;
await this.onUpdate();
});
const filters = DomHelpers.createElement("div", "leif-session-filters");
filters.append(
DomHelpers.createStackedLabel("Matéria", subjectSelect),
DomHelpers.createStackedLabel("Tipo", typeSelect),
DomHelpers.createStackedLabel("De", fromInput),
DomHelpers.createStackedLabel("Até", toInput)
);
return filters;
}
private filterSessions(sessions: StudySession[]): StudySession[] {
return sessions.filter((session) => {
if (this.historySubjectFilter && session.subjectId !== this.historySubjectFilter) {
return false;
}
if (this.historyTypeFilter && session.type !== this.historyTypeFilter) {
return false;
}
const sessionDate = session.studiedAt.slice(0, 10);
if (this.historyFromFilter && sessionDate < this.historyFromFilter) {
return false;
}
if (this.historyToFilter && sessionDate > this.historyToFilter) {
return false;
}
return true;
});
}
private renderDisplayRow(session: StudySession, data: LeifPluginData): HTMLElement {
const tr = DomHelpers.createElement("tr");
tr.dataset.sessionId = session.id;

View file

@ -880,6 +880,13 @@
min-width: 0;
}
.leif-session-filters {
display: grid;
grid-template-columns: minmax(180px, 1fr) minmax(120px, 0.6fr) minmax(140px, 0.5fr) minmax(140px, 0.5fr);
gap: var(--size-4-3);
align-items: end;
}
@media (max-width: 720px) {
.leif-view {
padding: 14px;
@ -898,6 +905,10 @@
grid-template-columns: 1fr;
}
.leif-session-filters {
grid-template-columns: 1fr;
}
.leif-list-row,
.leif-key-value {
flex-direction: column;

View file

@ -386,6 +386,73 @@ describe("LeifView", () => {
expect(questionsRow?.querySelector("td.leif-session-date-cell button[title='Excluir']")).not.toBeNull();
});
it("filters session history by subject, type and period", async () => {
const dataStore = new InMemoryPluginDataStore();
const factory = await seedUiData(dataStore);
const registerStudySession = new RegisterStudySessionUseCase(dataStore, factory);
await registerStudySession.execute({
id: "session-video-constitutional",
contestId: "contest-1",
subjectId: "subject-2",
type: "video",
studiedAt: "2026-06-13T20:00:00.000Z",
pagesOrCount: 1,
completed: true
});
await registerStudySession.execute({
id: "session-questions-portuguese",
contestId: "contest-1",
subjectId: "subject-1",
type: "questions",
studiedAt: "2026-06-20T20:00:00.000Z",
pagesOrCount: 20,
correctAnswers: 15,
completed: true
});
const { leaf } = await openLeifView(dataStore);
const sessionsTabButton = leaf.containerEl.querySelector<HTMLButtonElement>("[data-tab='sessions']");
if (!sessionsTabButton) {
throw new Error("Sessions tab button was not rendered.");
}
sessionsTabButton.click();
await new Promise((resolve) => setTimeout(resolve, 0));
const setFilter = async (name: string, value: string): Promise<void> => {
const control = leaf.containerEl.querySelector<HTMLInputElement | HTMLSelectElement>(
`[data-session-filter='${name}']`
);
if (!control) {
throw new Error(`${name} filter was not rendered.`);
}
control.value = value;
control.dispatchEvent(new Event("change"));
await new Promise((resolve) => setTimeout(resolve, 0));
};
expect(leaf.containerEl.querySelector(".leif-session-filters")).not.toBeNull();
await setFilter("subject", "subject-2");
expect(leaf.containerEl.querySelector("tr[data-session-id='session-video-constitutional']")).not.toBeNull();
expect(leaf.containerEl.querySelector("tr[data-session-id='session-1']")).toBeNull();
await setFilter("subject", "");
await setFilter("type", "questions");
expect(leaf.containerEl.querySelector("tr[data-session-id='session-questions-portuguese']")).not.toBeNull();
expect(leaf.containerEl.querySelector("tr[data-session-id='session-video-constitutional']")).toBeNull();
await setFilter("type", "");
await setFilter("from", "2026-06-14");
await setFilter("to", "2026-06-30");
expect(leaf.containerEl.querySelector("tr[data-session-id='session-questions-portuguese']")).not.toBeNull();
expect(leaf.containerEl.querySelector("tr[data-session-id='session-video-constitutional']")).toBeNull();
});
it("keeps contest actions next to the contest name", async () => {
const dataStore = new InMemoryPluginDataStore();
await seedUiData(dataStore);