diff --git a/src/app/features/metric/activity-heatmap/activity-heatmap.component.html b/src/app/features/metric/activity-heatmap/activity-heatmap.component.html index 909f10be41..b50bbcb97c 100644 --- a/src/app/features/metric/activity-heatmap/activity-heatmap.component.html +++ b/src/app/features/metric/activity-heatmap/activity-heatmap.component.html @@ -24,13 +24,9 @@
-
Sun
-
Mon
-
Tue
-
Wed
-
Thu
-
Fri
-
Sat
+ @for (label of dayLabels(); track $index) { +
{{ label }}
+ }
diff --git a/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts b/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts index 0578c2d19c..18f2a8a223 100644 --- a/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts +++ b/src/app/features/metric/activity-heatmap/activity-heatmap.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + computed, + inject, + signal, +} from '@angular/core'; import { toSignal } from '@angular/core/rxjs-interop'; import { WorklogService } from '../../worklog/worklog.service'; import { WorkContextService } from '../../work-context/work-context.service'; @@ -14,6 +20,7 @@ import { MatIconButton } from '@angular/material/button'; import { MatTooltip } from '@angular/material/tooltip'; import { MatIcon } from '@angular/material/icon'; import { SnackService } from '../../../core/snack/snack.service'; +import { GlobalConfigService } from '../../config/global-config.service'; interface DayData { date: Date; @@ -40,6 +47,7 @@ export class ActivityHeatmapComponent { private readonly _taskService = inject(TaskService); private readonly _taskArchiveService = inject(TaskArchiveService); private readonly _snackService = inject(SnackService); + private readonly _globalConfigService = inject(GlobalConfigService); T: typeof T = T; weeks: WeekData[] = []; @@ -49,11 +57,20 @@ export class ActivityHeatmapComponent { { initialValue: '' }, ); - // Compute heatmap data - // NOTE: Reacts to work context changes - // - For TODAY tag: shows ALL tasks from all projects/tags (current + archived) - // - For other tags/projects: shows only tasks from that context - heatmapData = toSignal( + // Get first day of week setting (0 = Sunday, 1 = Monday, etc.) + private readonly _firstDayOfWeek = computed(() => { + return this._globalConfigService.misc()?.firstDayOfWeek ?? 1; + }); + + // Day labels adjusted for first day of week + readonly dayLabels = computed(() => { + const allDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const firstDay = this._firstDayOfWeek(); + return [...allDays.slice(firstDay), ...allDays.slice(0, firstDay)]; + }); + + // Raw data signals + private readonly _rawHeatmapData = toSignal( this._workContextService.activeWorkContext$.pipe( switchMap((context) => { // Special case: TODAY tag shows ALL data from all tasks @@ -73,6 +90,24 @@ export class ActivityHeatmapComponent { { initialValue: null }, ); + // Compute heatmap data - reacts to both data changes AND firstDayOfWeek setting changes + heatmapData = computed(() => { + const rawData = this._rawHeatmapData(); + const firstDay = this._firstDayOfWeek(); + + if (!rawData || !rawData.dayMap) { + return null; + } + + // Rebuild the weeks grid with the current firstDayOfWeek setting + return this._buildWeeksGrid( + rawData.dayMap, + rawData.startDate, + rawData.endDate, + firstDay, + ); + }); + private async _loadAllTasks(): Promise { // Load both current tasks and archived tasks const [archive, currentTasks] = await Promise.all([ @@ -96,8 +131,9 @@ export class ActivityHeatmapComponent { } private _buildHeatmapDataFromTasks(tasks: Task[]): { - weeks: WeekData[]; - monthLabels: string[]; + dayMap: Map; + startDate: Date; + endDate: Date; } | null { const dayMap = new Map(); const now = new Date(); @@ -175,12 +211,17 @@ export class ActivityHeatmapComponent { } }); - return this._buildWeeksGrid(dayMap, oneYearAgo, now); + return { + dayMap, + startDate: oneYearAgo, + endDate: now, + }; } private _buildHeatmapData(worklog: any): { - weeks: WeekData[]; - monthLabels: string[]; + dayMap: Map; + startDate: Date; + endDate: Date; } | null { if (!worklog) { return null; @@ -267,7 +308,11 @@ export class ActivityHeatmapComponent { } }); - return this._buildWeeksGrid(dayMap, oneYearAgo, now); + return { + dayMap, + startDate: oneYearAgo, + endDate: now, + }; } private _getDateStr(date: Date): string { @@ -281,15 +326,18 @@ export class ActivityHeatmapComponent { dayMap: Map, startDate: Date, endDate: Date, + firstDayOfWeek: number = 0, ): { weeks: WeekData[]; monthLabels: string[] } { const weeks: WeekData[] = []; const monthLabels: string[] = []; let currentMonth = -1; - // Find the first Sunday before or on the start date + // Find the first day (based on firstDayOfWeek setting) before or on the start date const firstDay = new Date(startDate); const dayOfWeek = firstDay.getDay(); - firstDay.setDate(firstDay.getDate() - dayOfWeek); + // Calculate days to go back to reach the first day of the week + const daysToGoBack = (dayOfWeek - firstDayOfWeek + 7) % 7; + firstDay.setDate(firstDay.getDate() - daysToGoBack); // Build weeks const currentDate = new Date(firstDay); @@ -482,7 +530,7 @@ export class ActivityHeatmapComponent { ctx.textAlign = 'right'; ctx.textBaseline = 'middle'; - const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + const dayNames = this.dayLabels(); dayNames.forEach((day, i) => { // eslint-disable-next-line no-mixed-operators const y = padding + monthLabelHeight + i * (cellSize + gap) + cellSize / 2;