diff --git a/app/formatters/SummaryHtml/Formatter.test.ts b/app/formatters/SummaryHtml/Formatter.test.ts index 471b2e0d..4b3f03dc 100644 --- a/app/formatters/SummaryHtml/Formatter.test.ts +++ b/app/formatters/SummaryHtml/Formatter.test.ts @@ -1,4 +1,100 @@ import { beforeEach, describe, test, expect } from "@jest/globals"; +import * as moment from "moment-timezone"; +import { AppConfigurations, Dates, Tags, WorklogSets } from "../../../tests/entities"; +import { Tag } from "../../models"; +import { IAppConfiguration } from "../../models/AppConfiguration"; +import { SummaryHtmlFormatter, SummaryHtmlFormatterConfiguration } from "."; + +describe('format with filters', () => { + let formatter: SummaryHtmlFormatter; + let appConfiguration: IAppConfiguration; + + beforeEach(() => { + appConfiguration = AppConfigurations.normal(); + }); + + test('filters worklog entries by tag value', async () => { + const configuration = new SummaryHtmlFormatterConfiguration( + [['client', 'project']], + [{ + tagNames: ['client', 'project'], + filter: (worklog) => worklog.getTagValue('project') === 'Project1' + }] + ); + formatter = new SummaryHtmlFormatter(configuration, appConfiguration); + + const worklogSet = WorklogSets.double(); + worklogSet.worklogs[0].addTag(Tags.client.ProCorp()); + worklogSet.worklogs[0].addTag(new Tag('project', 'Project1')); + worklogSet.worklogs[1].addTag(Tags.client.ProCorp()); + worklogSet.worklogs[1].addTag(new Tag('project', 'Project2')); + + const formatted = await formatter.format(worklogSet); + + expect(formatted).toMatch('Project1'); + expect(formatted).not.toMatch('Project2'); + }); + + test('applies multiple filters to grouped entries', async () => { + const configuration = new SummaryHtmlFormatterConfiguration( + [['client', 'project']], + [{ + tagNames: ['client', 'project'], + filter: (worklog) => worklog.getTagValue('project') === 'Project1' + }, { + tagNames: ['client', 'project'], + filter: (worklog) => worklog.getDurationInMinutes() > 60 + }] + ); + formatter = new SummaryHtmlFormatter(configuration, appConfiguration); + + const worklogSet = WorklogSets.double(); + worklogSet.worklogs[0].startDateTime = Dates.pastTwoHours(); + worklogSet.worklogs[0].endDateTime = Dates.now(); + worklogSet.worklogs[0].addTag(Tags.client.ProCorp()); + worklogSet.worklogs[0].addTag(new Tag('project', 'Project1')); + + worklogSet.worklogs[1].startDateTime = Dates.pastHalfHour(); + worklogSet.worklogs[1].endDateTime = Dates.now(); + worklogSet.worklogs[1].addTag(Tags.client.ProCorp()); + worklogSet.worklogs[1].addTag(new Tag('project', 'Project1')); + + const formatted = await formatter.format(worklogSet); + + expect(formatted).toMatch('2hs 0m'); + expect(formatted).not.toMatch('0hs 30m'); + }); + + test('handles empty filters gracefully', async () => { + const configuration = new SummaryHtmlFormatterConfiguration( + [['client', 'project']], + [] + ); + formatter = new SummaryHtmlFormatter(configuration, appConfiguration); + + const worklogSet = WorklogSets.double(); + const formatted = await formatter.format(worklogSet); + + expect(formatted).toMatch('Total time:'); + }); + + test('maintains HTML structure with filtered groups', async () => { + const configuration = new SummaryHtmlFormatterConfiguration( + [['client', 'project']], + [{ + tagNames: ['client', 'project'], + filter: () => false // Filter out everything + }] + ); + formatter = new SummaryHtmlFormatter(configuration, appConfiguration); + + const worklogSet = WorklogSets.double(); + const formatted = await formatter.format(worklogSet); + + expect(formatted).toMatch(/<[^>]+>/); // Contains HTML tags + expect(formatted).toMatch(/
.*<\/p>/); // Has proper HTML structure
+ });
+});foreEach, describe, test, expect } from "@jest/globals";
import * as moment from "moment-timezone";
import { AppConfigurations, Dates, Tags, WorklogSets } from "../../../tests/entities";
diff --git a/app/formatters/SummaryHtml/Formatter.ts b/app/formatters/SummaryHtml/Formatter.ts
index d675adc4..015dc214 100644
--- a/app/formatters/SummaryHtml/Formatter.ts
+++ b/app/formatters/SummaryHtml/Formatter.ts
@@ -3,6 +3,57 @@ import { WorklogSet } from '../../models/WorklogSet';
import { getLogger, LoggerCategory } from '../../services/Logger';
import { SummaryTextFormatter } from '../SummaryText';
import { SummaryHtmlFormatterConfiguration } from '.';
+import { Worklog } from '../../models/Worklog';
+
+const logger = getLogger(LoggerCategory.Formatters);
+
+export class SummaryHtmlFormatter extends SummaryTextFormatter {
+ protected _configuration: SummaryHtmlFormatterConfiguration;
+
+ _generateAggregationLines(worklogs: Worklog[], tags: string[], indentationLevel = 1): string[] {
+ if (!worklogs || !worklogs.length || !tags || !tags.length) return [];
+
+ // Apply filters for this tag group
+ const filteredWorklogs = this._applyFilters(worklogs, tags);
+
+ return super._generateAggregationLines(filteredWorklogs, tags, indentationLevel);
+ }
+
+ private _applyFilters(worklogs: Worklog[], tags: string[]): Worklog[] {
+ const matchingFilters = this._configuration.filters.filter(f =>
+ this._arraysHaveSameElements(f.tagNames, tags));
+
+ if (!matchingFilters.length) {
+ return worklogs;
+ }
+
+ return worklogs.filter(worklog =>
+ matchingFilters.every(filter => filter.filter(worklog))
+ );
+ }
+
+ private _arraysHaveSameElements(arr1: string[], arr2: string[]): boolean {
+ if (arr1.length !== arr2.length) return false;
+ const sorted1 = [...arr1].sort();
+ const sorted2 = [...arr2].sort();
+ return sorted1.every((element, index) => element === sorted2[index]);
+ }
+
+ async format(worklogSet: WorklogSet): Promise