mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Fix time-entry timezone consistency and remove persisted durations
This commit is contained in:
parent
206b240ed1
commit
f6450c8c3e
12 changed files with 138 additions and 84 deletions
|
|
@ -284,7 +284,6 @@ Stop the currently active time tracking session for a task.
|
|||
{
|
||||
"startTime": "2025-08-14T10:00:00.000Z",
|
||||
"endTime": "2025-08-14T11:30:00.000Z",
|
||||
"duration": 90,
|
||||
"description": "Working on API endpoint implementation"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -51,7 +51,12 @@ function transform(payload) {
|
|||
}
|
||||
|
||||
const totalMinutes = task.timeEntries.reduce((sum, entry) => {
|
||||
return sum + (entry.duration || 0);
|
||||
if (!entry.startTime) return sum;
|
||||
const start = new Date(entry.startTime);
|
||||
const end = entry.endTime ? new Date(entry.endTime) : new Date();
|
||||
const diffMs = end.getTime() - start.getTime();
|
||||
const minutes = Math.max(0, Math.floor(diffMs / (1000 * 60)));
|
||||
return sum + minutes;
|
||||
}, 0);
|
||||
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
|
|
@ -320,4 +325,4 @@ function transform(payload) {
|
|||
}]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,7 +49,12 @@ function transform(payload) {
|
|||
}
|
||||
|
||||
const totalMinutes = task.timeEntries.reduce((sum, entry) => {
|
||||
return sum + (entry.duration || 0);
|
||||
if (!entry.startTime) return sum;
|
||||
const start = new Date(entry.startTime);
|
||||
const end = entry.endTime ? new Date(entry.endTime) : new Date();
|
||||
const diffMs = end.getTime() - start.getTime();
|
||||
const minutes = Math.max(0, Math.floor(diffMs / (1000 * 60)));
|
||||
return sum + minutes;
|
||||
}, 0);
|
||||
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
|
|
@ -283,4 +288,4 @@ function transform(payload) {
|
|||
]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,14 +26,24 @@ Example:
|
|||
|
||||
## Added
|
||||
|
||||
- [Mdbase spec](https://mdbase.dev) now emits `tn_role` annotations on schema fields, allowing external tools (e.g. [mtn CLI](https://github.com/callumalpass/mdbase-tasknotes)) to discover each field's semantic role regardless of custom frontmatter names
|
||||
- [Mdbase spec](https://mdbase.dev) match rules now use tag or frontmatter property matching (based on task identification settings) instead of path glob, with automatic fallback to tag matching
|
||||
- [Mdbase spec](https://mdbase.dev) status field now includes `tn_completed_values` annotation, listing which status values count as completed
|
||||
- [Mdbase](https://mdbase.dev) type generation now emits `tn_role` annotations on schema fields, allowing external tools (e.g. [mtn CLI](https://github.com/callumalpass/mdbase-tasknotes)) to discover each field's semantic role regardless of custom frontmatter names
|
||||
- Mdbase type match rules now use tag or frontmatter property matching (based on task identification settings) instead of path glob, with automatic fallback to tag matching
|
||||
- Mdbase type status field now includes `tn_completed_values` annotation, listing which status values count as completed
|
||||
|
||||
## Changed
|
||||
|
||||
- [Mdbase spec](https://mdbase.dev) no longer overwrites `mdbase.yaml` if the file already exists, preserving user customisations
|
||||
|
||||
- Mdbase type generation no longer overwrites `mdbase.yaml` if the file already exists, preserving user customisations
|
||||
- Webhook emissions moved from API controllers and MCP layer into the service/domain layer, ensuring webhooks fire consistently regardless of entry point
|
||||
- Webhook runtime state now syncs automatically when plugin settings change
|
||||
- Extracted shared HTTP response/body-parsing utilities into a dedicated `httpUtils` module
|
||||
|
||||
## Fixed
|
||||
|
||||
- (#1602) Fixed time tracking statistics showing incorrect or zero values for Today/Week/Month due to UTC-anchored date range boundaries in Stats View
|
||||
- Updated range calculations to use local calendar-day boundaries consistently
|
||||
- (#1602) Fixed inconsistent `timeEntries` timestamp formats across create/edit/drag/resize flows
|
||||
- Time entry timestamps are now written in canonical UTC ISO format (`toISOString()` with `Z`) across all write paths
|
||||
- (#1602) Fixed denormalized `timeEntries.duration` drift after edits
|
||||
- Time tracking calculations now derive duration from `startTime`/`endTime`
|
||||
- Time entry save paths now strip legacy `duration` values instead of persisting them
|
||||
- Thanks to @dy66 for reporting issue #1602
|
||||
|
|
|
|||
|
|
@ -1364,14 +1364,16 @@ export class CalendarView extends BasesViewBase {
|
|||
|
||||
entry.startTime = new Date(oldStartDate.getTime() + timeDiffMs).toISOString();
|
||||
entry.endTime = new Date(oldEndDate.getTime() + timeDiffMs).toISOString();
|
||||
delete entry.duration;
|
||||
|
||||
// Recalculate duration
|
||||
entry.duration = Math.round(
|
||||
(new Date(entry.endTime).getTime() - new Date(entry.startTime).getTime()) / 60000
|
||||
);
|
||||
const sanitizedEntries = updatedEntries.map((timeEntry) => {
|
||||
const sanitizedEntry = { ...timeEntry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
|
||||
await this.plugin.taskService.updateTask(taskInfo, {
|
||||
timeEntries: updatedEntries,
|
||||
timeEntries: sanitizedEntries,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -1482,14 +1484,16 @@ export class CalendarView extends BasesViewBase {
|
|||
// Update start and end times
|
||||
entry.startTime = newStart.toISOString();
|
||||
entry.endTime = newEnd.toISOString();
|
||||
delete entry.duration;
|
||||
|
||||
// Recalculate duration
|
||||
entry.duration = Math.round(
|
||||
(newEnd.getTime() - newStart.getTime()) / 60000
|
||||
);
|
||||
const sanitizedEntries = updatedEntries.map((timeEntry) => {
|
||||
const sanitizedEntry = { ...timeEntry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
|
||||
await this.plugin.taskService.updateTask(taskInfo, {
|
||||
timeEntries: updatedEntries,
|
||||
timeEntries: sanitizedEntries,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -1134,11 +1134,16 @@ export async function handleTimeEntryCreation(
|
|||
startTime: start.toISOString(),
|
||||
endTime: end.toISOString(),
|
||||
description: "",
|
||||
duration: durationMinutes,
|
||||
};
|
||||
|
||||
// Add to task's time entries
|
||||
const updatedTimeEntries = [...(selectedTask.timeEntries || []), newEntry];
|
||||
const updatedTimeEntries = [...(selectedTask.timeEntries || []), newEntry].map(
|
||||
(entry) => {
|
||||
const sanitizedEntry = { ...entry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
}
|
||||
);
|
||||
|
||||
// Save to file
|
||||
await plugin.taskService.updateTask(selectedTask, {
|
||||
|
|
|
|||
|
|
@ -2805,9 +2805,15 @@ export default class TaskNotesPlugin extends Plugin {
|
|||
openTimeEntryEditor(task: TaskInfo, onSave?: () => void): void {
|
||||
const modal = new TimeEntryEditorModal(this.app, this, task, async (updatedEntries) => {
|
||||
try {
|
||||
const sanitizedEntries = updatedEntries.map((entry) => {
|
||||
const sanitizedEntry = { ...entry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
|
||||
// Save to file
|
||||
await this.taskService.updateTask(task, {
|
||||
timeEntries: updatedEntries,
|
||||
timeEntries: sanitizedEntries,
|
||||
});
|
||||
|
||||
// Signal immediate update before triggering data change
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ export class TimeEntryEditorModal extends Modal {
|
|||
|
||||
private calculateTotalMinutes(): number {
|
||||
return this.timeEntries.reduce((total, entry) => {
|
||||
const duration = entry.duration || this.calculateDuration(entry);
|
||||
const duration = this.calculateDuration(entry);
|
||||
return total + duration;
|
||||
}, 0);
|
||||
}
|
||||
|
|
@ -259,14 +259,12 @@ export class TimeEntryEditorModal extends Modal {
|
|||
}
|
||||
}
|
||||
|
||||
// Calculate durations for all entries with end times
|
||||
this.timeEntries.forEach(entry => {
|
||||
if (entry.endTime) {
|
||||
entry.duration = this.calculateDuration(entry);
|
||||
}
|
||||
const sanitizedEntries = this.timeEntries.map((entry) => {
|
||||
const sanitizedEntry = { ...entry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
|
||||
this.onSave(this.timeEntries);
|
||||
this.onSave(sanitizedEntries);
|
||||
this.close();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1127,9 +1127,14 @@ export class TaskService {
|
|||
if (!updatedTask.timeEntries) {
|
||||
updatedTask.timeEntries = [];
|
||||
}
|
||||
updatedTask.timeEntries = updatedTask.timeEntries.map((entry) => {
|
||||
const sanitizedEntry = { ...entry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
|
||||
const newEntry: TimeEntry = {
|
||||
startTime: getCurrentTimestamp(),
|
||||
startTime: new Date().toISOString(),
|
||||
description: "Work session",
|
||||
};
|
||||
updatedTask.timeEntries = [...updatedTask.timeEntries, newEntry];
|
||||
|
|
@ -1142,6 +1147,13 @@ export class TaskService {
|
|||
if (!frontmatter[timeEntriesField]) {
|
||||
frontmatter[timeEntriesField] = [];
|
||||
}
|
||||
if (Array.isArray(frontmatter[timeEntriesField])) {
|
||||
frontmatter[timeEntriesField] = frontmatter[timeEntriesField].map((entry: TimeEntry) => {
|
||||
const sanitizedEntry = { ...entry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
}
|
||||
|
||||
// Add new time entry with start time
|
||||
frontmatter[timeEntriesField].push(newEntry);
|
||||
|
|
@ -1195,12 +1207,18 @@ export class TaskService {
|
|||
if (!activeSession) {
|
||||
throw new Error("No active time tracking session for this task");
|
||||
}
|
||||
const stopTimestamp = new Date().toISOString();
|
||||
|
||||
// Step 1: Construct new state in memory
|
||||
const updatedTask = { ...task };
|
||||
updatedTask.dateModified = getCurrentTimestamp();
|
||||
|
||||
if (updatedTask.timeEntries && Array.isArray(updatedTask.timeEntries)) {
|
||||
updatedTask.timeEntries = updatedTask.timeEntries.map((entry) => {
|
||||
const sanitizedEntry = { ...entry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
const entryIndex = updatedTask.timeEntries.findIndex(
|
||||
(entry: TimeEntry) => entry.startTime === activeSession.startTime && !entry.endTime
|
||||
);
|
||||
|
|
@ -1208,7 +1226,7 @@ export class TaskService {
|
|||
updatedTask.timeEntries = [...updatedTask.timeEntries];
|
||||
updatedTask.timeEntries[entryIndex] = {
|
||||
...updatedTask.timeEntries[entryIndex],
|
||||
endTime: getCurrentTimestamp(),
|
||||
endTime: stopTimestamp,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -1219,6 +1237,11 @@ export class TaskService {
|
|||
const dateModifiedField = this.plugin.fieldMapper.toUserField("dateModified");
|
||||
|
||||
if (frontmatter[timeEntriesField] && Array.isArray(frontmatter[timeEntriesField])) {
|
||||
frontmatter[timeEntriesField] = frontmatter[timeEntriesField].map((entry: TimeEntry) => {
|
||||
const sanitizedEntry = { ...entry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
// Find and update the active session
|
||||
const entryIndex = frontmatter[timeEntriesField].findIndex(
|
||||
(entry: TimeEntry) =>
|
||||
|
|
@ -1226,7 +1249,7 @@ export class TaskService {
|
|||
);
|
||||
|
||||
if (entryIndex !== -1) {
|
||||
frontmatter[timeEntriesField][entryIndex].endTime = getCurrentTimestamp();
|
||||
frontmatter[timeEntriesField][entryIndex].endTime = stopTimestamp;
|
||||
}
|
||||
}
|
||||
frontmatter[dateModifiedField] = updatedTask.dateModified;
|
||||
|
|
@ -1280,6 +1303,14 @@ export class TaskService {
|
|||
throw new Error(`Cannot find task file: ${originalTask.path}`);
|
||||
}
|
||||
|
||||
if (Array.isArray(updates.timeEntries)) {
|
||||
updates.timeEntries = updates.timeEntries.map((entry) => {
|
||||
const sanitizedEntry = { ...entry };
|
||||
delete sanitizedEntry.duration;
|
||||
return sanitizedEntry;
|
||||
});
|
||||
}
|
||||
|
||||
const isRenameNeeded =
|
||||
this.plugin.settings.storeTitleInFilename &&
|
||||
updates.title &&
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@ export interface TimeEntry {
|
|||
startTime: string; // ISO timestamp
|
||||
endTime?: string; // ISO timestamp, undefined if currently running
|
||||
description?: string; // Optional description of what was worked on
|
||||
duration?: number; // Duration in minutes (calculated or manually set)
|
||||
duration?: number; // Legacy field; duration should be derived from start/end timestamps
|
||||
}
|
||||
|
||||
// Reminder types
|
||||
|
|
|
|||
|
|
@ -78,9 +78,7 @@ export function calculateTotalTimeSpent(timeEntries: TimeEntry[]): number {
|
|||
if (!timeEntries || timeEntries.length === 0) return 0;
|
||||
|
||||
return timeEntries.reduce((total, entry) => {
|
||||
if (entry.duration) {
|
||||
return total + entry.duration;
|
||||
} else if (entry.endTime) {
|
||||
if (entry.endTime) {
|
||||
const durationMs =
|
||||
new Date(entry.endTime).getTime() - new Date(entry.startTime).getTime();
|
||||
return total + Math.floor(durationMs / (1000 * 60));
|
||||
|
|
@ -194,9 +192,7 @@ export function computeTimeSummary(
|
|||
const entryStart = new Date(entry.startTime);
|
||||
|
||||
if (entryStart >= startDate && entryStart <= endDate) {
|
||||
if (entry.duration) {
|
||||
taskMinutes += entry.duration;
|
||||
} else if (!entry.endTime) {
|
||||
if (!entry.endTime) {
|
||||
taskMinutes += Math.floor(
|
||||
(Date.now() - entryStart.getTime()) / (1000 * 60)
|
||||
);
|
||||
|
|
@ -281,12 +277,18 @@ export function computeTaskTimeData(
|
|||
const totalMinutes = calculateTotalTimeSpent(timeEntries);
|
||||
|
||||
const completedSessions = timeEntries.filter((entry) => entry.endTime).length;
|
||||
const completedEntries = timeEntries.filter((entry) => entry.endTime && entry.duration);
|
||||
const completedEntries = timeEntries.filter((entry) => entry.endTime);
|
||||
const averageSessionMinutes =
|
||||
completedEntries.length > 0
|
||||
? Math.round(
|
||||
(completedEntries.reduce(
|
||||
(sum, entry) => sum + (entry.duration || 0),
|
||||
(sum, entry) =>
|
||||
sum +
|
||||
Math.floor(
|
||||
(new Date(entry.endTime as string).getTime() -
|
||||
new Date(entry.startTime).getTime()) /
|
||||
(1000 * 60)
|
||||
),
|
||||
0
|
||||
) /
|
||||
completedEntries.length) *
|
||||
|
|
@ -323,18 +325,16 @@ export function computeTaskTimeData(
|
|||
startTime: entry.startTime,
|
||||
endTime: entry.endTime || null,
|
||||
description: entry.description || null,
|
||||
duration:
|
||||
entry.duration ||
|
||||
(entry.endTime
|
||||
? Math.floor(
|
||||
(new Date(entry.endTime).getTime() -
|
||||
new Date(entry.startTime).getTime()) /
|
||||
(1000 * 60)
|
||||
)
|
||||
: Math.floor(
|
||||
(Date.now() - new Date(entry.startTime).getTime()) /
|
||||
(1000 * 60)
|
||||
)),
|
||||
duration: entry.endTime
|
||||
? Math.floor(
|
||||
(new Date(entry.endTime).getTime() -
|
||||
new Date(entry.startTime).getTime()) /
|
||||
(1000 * 60)
|
||||
)
|
||||
: Math.floor(
|
||||
(Date.now() - new Date(entry.startTime).getTime()) /
|
||||
(1000 * 60)
|
||||
),
|
||||
isActive: !entry.endTime,
|
||||
})),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,12 +6,13 @@ import {
|
|||
startOfMonth,
|
||||
endOfMonth,
|
||||
startOfDay,
|
||||
endOfDay,
|
||||
subDays,
|
||||
} from "date-fns";
|
||||
import TaskNotesPlugin from "../main";
|
||||
import { STATS_VIEW_TYPE, TaskInfo, EVENT_TASK_UPDATED } from "../types";
|
||||
import { calculateTotalTimeSpent, filterEmptyProjects } from "../utils/helpers";
|
||||
import { getTodayLocal, createUTCDateFromLocalCalendarDate } from "../utils/dateUtils";
|
||||
import { getTodayLocal } from "../utils/dateUtils";
|
||||
import { createTaskCard } from "../ui/TaskCard";
|
||||
import { convertInternalToUserProperties } from "../utils/propertyMapping";
|
||||
import { getProjectDisplayName } from "../utils/linkUtils";
|
||||
|
|
@ -355,11 +356,11 @@ export class StatsView extends ItemView {
|
|||
private async updateTodayStats() {
|
||||
if (!this.todayStatsEl) return;
|
||||
|
||||
// Use UTC-anchored today for consistent timezone handling
|
||||
const todayLocal = getTodayLocal();
|
||||
const todayUTCAnchor = createUTCDateFromLocalCalendarDate(todayLocal);
|
||||
const startOfToday = startOfDay(todayUTCAnchor);
|
||||
const stats = await this.calculateStatsForRange(startOfToday, todayUTCAnchor);
|
||||
const stats = await this.calculateStatsForRange(
|
||||
startOfDay(todayLocal),
|
||||
endOfDay(todayLocal)
|
||||
);
|
||||
|
||||
this.renderTimeRangeStats(this.todayStatsEl, stats);
|
||||
}
|
||||
|
|
@ -367,13 +368,11 @@ export class StatsView extends ItemView {
|
|||
private async updateWeekStats() {
|
||||
if (!this.weekStatsEl) return;
|
||||
|
||||
// Use UTC-anchored today for consistent timezone handling
|
||||
const todayLocal = getTodayLocal();
|
||||
const todayUTCAnchor = createUTCDateFromLocalCalendarDate(todayLocal);
|
||||
const firstDaySetting = this.plugin.settings.calendarViewSettings.firstDay || 0;
|
||||
const weekStartOptions = { weekStartsOn: firstDaySetting as 0 | 1 | 2 | 3 | 4 | 5 | 6 };
|
||||
const weekStart = startOfWeek(todayUTCAnchor, weekStartOptions);
|
||||
const weekEnd = endOfWeek(todayUTCAnchor, weekStartOptions);
|
||||
const weekStart = startOfWeek(todayLocal, weekStartOptions);
|
||||
const weekEnd = endOfWeek(todayLocal, weekStartOptions);
|
||||
|
||||
const stats = await this.calculateStatsForRange(weekStart, weekEnd);
|
||||
this.renderTimeRangeStats(this.weekStatsEl, stats);
|
||||
|
|
@ -382,11 +381,9 @@ export class StatsView extends ItemView {
|
|||
private async updateMonthStats() {
|
||||
if (!this.monthStatsEl) return;
|
||||
|
||||
// Use UTC-anchored today for consistent timezone handling
|
||||
const todayLocal = getTodayLocal();
|
||||
const todayUTCAnchor = createUTCDateFromLocalCalendarDate(todayLocal);
|
||||
const monthStart = startOfMonth(todayUTCAnchor);
|
||||
const monthEnd = endOfMonth(todayUTCAnchor);
|
||||
const monthStart = startOfMonth(todayLocal);
|
||||
const monthEnd = endOfMonth(todayLocal);
|
||||
|
||||
const stats = await this.calculateStatsForRange(monthStart, monthEnd);
|
||||
this.renderTimeRangeStats(this.monthStatsEl, stats);
|
||||
|
|
@ -817,33 +814,31 @@ export class StatsView extends ItemView {
|
|||
* Get date range based on current filters
|
||||
*/
|
||||
private getFilterDateRange(): { start?: Date; end?: Date } {
|
||||
// Use UTC-anchored today for consistent timezone handling
|
||||
const todayLocal = getTodayLocal();
|
||||
const todayUTCAnchor = createUTCDateFromLocalCalendarDate(todayLocal);
|
||||
|
||||
switch (this.currentFilters.dateRange) {
|
||||
case "7days":
|
||||
return {
|
||||
start: subDays(todayUTCAnchor, 7),
|
||||
end: todayUTCAnchor,
|
||||
start: startOfDay(subDays(todayLocal, 7)),
|
||||
end: endOfDay(todayLocal),
|
||||
};
|
||||
case "30days":
|
||||
return {
|
||||
start: subDays(todayUTCAnchor, 30),
|
||||
end: todayUTCAnchor,
|
||||
start: startOfDay(subDays(todayLocal, 30)),
|
||||
end: endOfDay(todayLocal),
|
||||
};
|
||||
case "90days":
|
||||
return {
|
||||
start: subDays(todayUTCAnchor, 90),
|
||||
end: todayUTCAnchor,
|
||||
start: startOfDay(subDays(todayLocal, 90)),
|
||||
end: endOfDay(todayLocal),
|
||||
};
|
||||
case "custom":
|
||||
return {
|
||||
start: this.currentFilters.customStartDate
|
||||
? new Date(this.currentFilters.customStartDate)
|
||||
? new Date(`${this.currentFilters.customStartDate}T00:00:00`)
|
||||
: undefined,
|
||||
end: this.currentFilters.customEndDate
|
||||
? new Date(this.currentFilters.customEndDate)
|
||||
? new Date(`${this.currentFilters.customEndDate}T23:59:59.999`)
|
||||
: undefined,
|
||||
};
|
||||
case "all":
|
||||
|
|
@ -1190,12 +1185,10 @@ export class StatsView extends ItemView {
|
|||
|
||||
// Calculate daily time spent over last 30 days
|
||||
const trendData: TrendDataPoint[] = [];
|
||||
// Use UTC-anchored today for consistent timezone handling
|
||||
const todayLocal = getTodayLocal();
|
||||
const todayUTCAnchor = createUTCDateFromLocalCalendarDate(todayLocal);
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = subDays(todayUTCAnchor, i);
|
||||
const date = subDays(todayLocal, i);
|
||||
const dateStr = format(date, "yyyy-MM-dd");
|
||||
let dailyTime = 0;
|
||||
|
||||
|
|
@ -1424,12 +1417,10 @@ export class StatsView extends ItemView {
|
|||
|
||||
// Calculate time by day for the last 30 days
|
||||
const timeByDay: TimeByDay[] = [];
|
||||
// Use UTC-anchored today for consistent timezone handling
|
||||
const todayLocal = getTodayLocal();
|
||||
const todayUTCAnchor = createUTCDateFromLocalCalendarDate(todayLocal);
|
||||
|
||||
for (let i = 29; i >= 0; i--) {
|
||||
const date = subDays(todayUTCAnchor, i);
|
||||
const date = subDays(todayLocal, i);
|
||||
const dateStr = format(date, "yyyy-MM-dd");
|
||||
let dayTime = 0;
|
||||
let dayTasks = 0;
|
||||
|
|
|
|||
Loading…
Reference in a new issue