refactor: unify date handling with utility functions

- Replaced direct date manipulations with utility functions like `getToday`, `parseDate`, and `createDateFromParts` across various components and services for consistency.
- Updated date initialization in `JiraAnalyticsService`, `BackupService`, and `DailyClient` to improve clarity and maintainability.
- Enhanced date parsing in forms and API routes to ensure proper handling of date strings.
This commit is contained in:
Julien Froidefond
2025-09-21 13:04:34 +02:00
parent c3c1d24fa2
commit 4ba6ba2c0b
23 changed files with 117 additions and 68 deletions

View File

@@ -104,7 +104,7 @@ export async function exportJiraAnalytics(format: ExportFormat = 'csv'): Promise
} }
const analytics = analyticsResult.data; const analytics = analyticsResult.data;
const timestamp = new Date().toISOString().slice(0, 16).replace(/:/g, '-'); const timestamp = getToday().toISOString().slice(0, 16).replace(/:/g, '-');
const projectKey = analytics.project.key; const projectKey = analytics.project.key;
if (format === 'json') { if (format === 'json') {

View File

@@ -4,6 +4,7 @@ import { JiraAnalyticsService, JiraAnalyticsConfig } from '@/services/jira-analy
import { userPreferencesService } from '@/services/user-preferences'; import { userPreferencesService } from '@/services/user-preferences';
import { SprintDetails } from '@/components/jira/SprintDetailModal'; import { SprintDetails } from '@/components/jira/SprintDetailModal';
import { JiraTask, AssigneeDistribution, StatusDistribution, SprintVelocity } from '@/lib/types'; import { JiraTask, AssigneeDistribution, StatusDistribution, SprintVelocity } from '@/lib/types';
import { parseDate } from '@/lib/date-utils';
export interface SprintDetailsResult { export interface SprintDetailsResult {
success: boolean; success: boolean;
@@ -48,11 +49,11 @@ export async function getSprintDetails(sprintName: string): Promise<SprintDetail
// Filtrer les issues pour ce sprint spécifique // Filtrer les issues pour ce sprint spécifique
// Note: En réalité, il faudrait une requête JQL plus précise pour récupérer les issues d'un sprint // Note: En réalité, il faudrait une requête JQL plus précise pour récupérer les issues d'un sprint
// Pour simplifier, on prend les issues dans la période du sprint // Pour simplifier, on prend les issues dans la période du sprint
const sprintStart = new Date(sprint.startDate); const sprintStart = parseDate(sprint.startDate);
const sprintEnd = new Date(sprint.endDate); const sprintEnd = parseDate(sprint.endDate);
const sprintIssues = allIssues.filter(issue => { const sprintIssues = allIssues.filter(issue => {
const issueDate = new Date(issue.created); const issueDate = parseDate(issue.created);
return issueDate >= sprintStart && issueDate <= sprintEnd; return issueDate >= sprintStart && issueDate <= sprintEnd;
}); });
@@ -116,8 +117,8 @@ function calculateSprintMetrics(issues: JiraTask[], sprint: SprintVelocity) {
let averageCycleTime = 0; let averageCycleTime = 0;
if (completedIssuesWithDates.length > 0) { if (completedIssuesWithDates.length > 0) {
const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => { const totalCycleTime = completedIssuesWithDates.reduce((total, issue) => {
const created = new Date(issue.created); const created = parseDate(issue.created);
const updated = new Date(issue.updated); const updated = parseDate(issue.updated);
const cycleTime = (updated.getTime() - created.getTime()) / (1000 * 60 * 60 * 24); // en jours const cycleTime = (updated.getTime() - created.getTime()) / (1000 * 60 * 60 * 24); // en jours
return total + cycleTime; return total + cycleTime;
}, 0); }, 0);

View File

@@ -1,6 +1,6 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/daily';
import { getToday, parseDate, isValidAPIDate } from '@/lib/date-utils'; import { getToday, parseDate, isValidAPIDate, createDateFromParts } from '@/lib/date-utils';
/** /**
* API route pour récupérer la vue daily (hier + aujourd'hui) * API route pour récupérer la vue daily (hier + aujourd'hui)
@@ -79,9 +79,9 @@ export async function POST(request: Request) {
if (typeof body.date === 'string') { if (typeof body.date === 'string') {
// Si c'est une string YYYY-MM-DD, créer une date locale // Si c'est une string YYYY-MM-DD, créer une date locale
const [year, month, day] = body.date.split('-').map(Number); const [year, month, day] = body.date.split('-').map(Number);
date = new Date(year, month - 1, day); // month est 0-indexé date = createDateFromParts(year, month, day);
} else { } else {
date = new Date(body.date); date = parseDate(body.date);
} }
if (isNaN(date.getTime())) { if (isNaN(date.getTime())) {

View File

@@ -1,6 +1,7 @@
import { Metadata } from 'next'; import { Metadata } from 'next';
import { DailyPageClient } from './DailyPageClient'; import { DailyPageClient } from './DailyPageClient';
import { dailyService } from '@/services/daily'; import { dailyService } from '@/services/daily';
import { getToday } from '@/lib/date-utils';
// Force dynamic rendering (no static generation) // Force dynamic rendering (no static generation)
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -12,7 +13,7 @@ export const metadata: Metadata = {
export default async function DailyPage() { export default async function DailyPage() {
// Récupérer les données côté serveur // Récupérer les données côté serveur
const today = new Date(); const today = getToday();
try { try {
const [dailyView, dailyDates] = await Promise.all([ const [dailyView, dailyDates] = await Promise.all([

View File

@@ -1,6 +1,6 @@
import { httpClient } from './base/http-client'; import { httpClient } from './base/http-client';
import { DailyCheckbox, DailyView, Task } from '@/lib/types'; import { DailyCheckbox, DailyView, Task } from '@/lib/types';
import { formatDateForAPI, parseDate } from '@/lib/date-utils'; import { formatDateForAPI, parseDate, getToday, addDays, subtractDays } from '@/lib/date-utils';
// Types pour les réponses API (avec dates en string) // Types pour les réponses API (avec dates en string)
interface ApiCheckbox { interface ApiCheckbox {
@@ -74,7 +74,7 @@ export class DailyClient {
const result = await httpClient.get<ApiHistoryItem[]>(`/daily?${params}`); const result = await httpClient.get<ApiHistoryItem[]>(`/daily?${params}`);
return result.map(item => ({ return result.map(item => ({
date: new Date(item.date), date: parseDate(item.date),
checkboxes: item.checkboxes.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)) checkboxes: item.checkboxes.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb))
})); }));
} }
@@ -128,16 +128,19 @@ export class DailyClient {
* Récupère la vue daily d'une date relative (hier, aujourd'hui, demain) * Récupère la vue daily d'une date relative (hier, aujourd'hui, demain)
*/ */
async getDailyViewByRelativeDate(relative: 'yesterday' | 'today' | 'tomorrow'): Promise<DailyView> { async getDailyViewByRelativeDate(relative: 'yesterday' | 'today' | 'tomorrow'): Promise<DailyView> {
const date = new Date(); let date: Date;
switch (relative) { switch (relative) {
case 'yesterday': case 'yesterday':
date.setDate(date.getDate() - 1); date = subtractDays(getToday(), 1);
break; break;
case 'tomorrow': case 'tomorrow':
date.setDate(date.getDate() + 1); date = addDays(getToday(), 1);
break;
case 'today':
default:
date = getToday();
break; break;
// 'today' ne change rien
} }
return this.getDailyView(date); return this.getDailyView(date);

View File

@@ -8,6 +8,7 @@ import { TagInput } from '@/components/ui/TagInput';
import { TaskPriority, TaskStatus } from '@/lib/types'; import { TaskPriority, TaskStatus } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { getAllStatuses, getAllPriorities } from '@/lib/status-config'; import { getAllStatuses, getAllPriorities } from '@/lib/status-config';
import { formatDateForDateTimeInput, parseDateTimeInput } from '@/lib/date-utils';
interface CreateTaskFormProps { interface CreateTaskFormProps {
isOpen: boolean; isOpen: boolean;
@@ -151,10 +152,10 @@ export function CreateTaskForm({ isOpen, onClose, onSubmit, loading = false }: C
<Input <Input
label="Date d'échéance" label="Date d'échéance"
type="datetime-local" type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''} value={formData.dueDate ? formatDateForDateTimeInput(formData.dueDate) : ''}
onChange={(e) => setFormData((prev: CreateTaskData) => ({ onChange={(e) => setFormData((prev: CreateTaskData) => ({
...prev, ...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined dueDate: e.target.value ? parseDateTimeInput(e.target.value) : undefined
}))} }))}
disabled={loading} disabled={loading}
/> />

View File

@@ -11,6 +11,7 @@ import { Task, TaskPriority, TaskStatus } from '@/lib/types';
import { useUserPreferences } from '@/contexts/UserPreferencesContext'; import { useUserPreferences } from '@/contexts/UserPreferencesContext';
// UpdateTaskData removed - using Server Actions directly // UpdateTaskData removed - using Server Actions directly
import { getAllStatuses, getAllPriorities } from '@/lib/status-config'; import { getAllStatuses, getAllPriorities } from '@/lib/status-config';
import { formatDateForDateTimeInput, parseDateTimeInput } from '@/lib/date-utils';
interface EditTaskFormProps { interface EditTaskFormProps {
isOpen: boolean; isOpen: boolean;
@@ -56,7 +57,7 @@ export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false
status: task.status, status: task.status,
priority: task.priority, priority: task.priority,
tags: task.tags || [], tags: task.tags || [],
dueDate: task.dueDate ? new Date(task.dueDate) : undefined dueDate: task.dueDate
}); });
} }
}, [task]); }, [task]);
@@ -181,10 +182,10 @@ export function EditTaskForm({ isOpen, onClose, onSubmit, task, loading = false
<Input <Input
label="Date d'échéance" label="Date d'échéance"
type="datetime-local" type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''} value={formData.dueDate ? formatDateForDateTimeInput(formData.dueDate) : ''}
onChange={(e) => setFormData(prev => ({ onChange={(e) => setFormData(prev => ({
...prev, ...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined dueDate: e.target.value ? parseDateTimeInput(e.target.value) : undefined
}))} }))}
disabled={loading} disabled={loading}
/> />

View File

@@ -6,6 +6,7 @@ import { TagInput } from '@/components/ui/TagInput';
import { TaskStatus, TaskPriority } from '@/lib/types'; import { TaskStatus, TaskPriority } from '@/lib/types';
import { CreateTaskData } from '@/clients/tasks-client'; import { CreateTaskData } from '@/clients/tasks-client';
import { getAllPriorities } from '@/lib/status-config'; import { getAllPriorities } from '@/lib/status-config';
import { formatDateForDateTimeInput, parseDateTimeInput } from '@/lib/date-utils';
interface QuickAddTaskProps { interface QuickAddTaskProps {
status: TaskStatus; status: TaskStatus;
@@ -189,10 +190,10 @@ export function QuickAddTask({ status, onSubmit, onCancel, swimlaneContext }: Qu
<div className="flex items-center justify-between text-xs min-w-0"> <div className="flex items-center justify-between text-xs min-w-0">
<input <input
type="datetime-local" type="datetime-local"
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''} value={formData.dueDate ? formatDateForDateTimeInput(formData.dueDate) : ''}
onChange={(e) => setFormData(prev => ({ onChange={(e) => setFormData(prev => ({
...prev, ...prev,
dueDate: e.target.value ? new Date(e.target.value) : undefined dueDate: e.target.value ? parseDateTimeInput(e.target.value) : undefined
}))} }))}
onFocus={() => setActiveField('date')} onFocus={() => setActiveField('date')}
disabled={isSubmitting} disabled={isSubmitting}

View File

@@ -194,7 +194,7 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
const formatDate = (date: string | Date): string => { const formatDate = (date: string | Date): string => {
// Format cohérent serveur/client pour éviter les erreurs d'hydratation // Format cohérent serveur/client pour éviter les erreurs d'hydratation
const d = typeof date === 'string' ? new Date(date) : date; const d = typeof date === 'string' ? parseDate(date) : date;
return formatDateForDisplay(d, 'DISPLAY_MEDIUM'); return formatDateForDisplay(d, 'DISPLAY_MEDIUM');
}; };

View File

@@ -3,7 +3,7 @@ import { exec } from 'child_process';
import { promisify } from 'util'; import { promisify } from 'util';
import path from 'path'; import path from 'path';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { formatDateForDisplay, getToday } from './date-utils'; import { formatDateForDisplay, getToday, parseDate } from './date-utils';
const execAsync = promisify(exec); const execAsync = promisify(exec);
@@ -160,7 +160,7 @@ export class BackupUtils {
// Format: 2025-09-18T14-12-05-737Z -> 2025-09-18T14:12:05.737Z // Format: 2025-09-18T14-12-05-737Z -> 2025-09-18T14:12:05.737Z
const isoString = dateMatch[2] const isoString = dateMatch[2]
.replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/, 'T$1:$2:$3.$4Z'); .replace(/T(\d{2})-(\d{2})-(\d{2})-(\d{3})Z/, 'T$1:$2:$3.$4Z');
date = new Date(isoString); date = parseDate(isoString);
} }
return { type, date }; return { type, date };
@@ -170,7 +170,7 @@ export class BackupUtils {
* Génère un nom de fichier de backup * Génère un nom de fichier de backup
*/ */
static generateBackupFilename(type: 'manual' | 'automatic'): string { static generateBackupFilename(type: 'manual' | 'automatic'): string {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const timestamp = getToday().toISOString().replace(/[:.]/g, '-');
return `towercontrol_${type}_${timestamp}.db`; return `towercontrol_${type}_${timestamp}.db`;
} }

View File

@@ -117,6 +117,28 @@ export function createDate(date: Date): Date {
return new Date(date); return new Date(date);
} }
/**
* Crée une date à partir de composants année/mois/jour
*/
export function createDateFromParts(year: number, month: number, day: number): Date {
return new Date(year, month - 1, day); // month est 0-indexé en JavaScript
}
/**
* Convertit une date pour un input datetime-local (gestion timezone)
*/
export function formatDateForDateTimeInput(date: Date): string {
const adjustedDate = new Date(date.getTime() - date.getTimezoneOffset() * 60000);
return adjustedDate.toISOString().slice(0, 16);
}
/**
* Parse une valeur d'input datetime-local vers une Date
*/
export function parseDateTimeInput(value: string): Date {
return new Date(value);
}
/** /**
* Ajoute des jours à une date * Ajoute des jours à une date
*/ */
@@ -126,6 +148,15 @@ export function addDays(date: Date, days: number): Date {
return result; return result;
} }
/**
* Ajoute des minutes à une date
*/
export function addMinutes(date: Date, minutes: number): Date {
const result = createDate(date);
result.setMinutes(result.getMinutes() + minutes);
return result;
}
/** /**
* Soustrait des jours à une date * Soustrait des jours à une date
*/ */

View File

@@ -1,4 +1,5 @@
import { JiraAnalytics } from './types'; import { JiraAnalytics } from './types';
import { getToday, subtractDays, parseDate } from './date-utils';
export type PeriodFilter = '7d' | '30d' | '3m' | 'current'; export type PeriodFilter = '7d' | '30d' | '3m' | 'current';
@@ -9,18 +10,18 @@ export function filterAnalyticsByPeriod(
analytics: JiraAnalytics, analytics: JiraAnalytics,
period: PeriodFilter period: PeriodFilter
): JiraAnalytics { ): JiraAnalytics {
const now = new Date(); const now = getToday();
let cutoffDate: Date; let cutoffDate: Date;
switch (period) { switch (period) {
case '7d': case '7d':
cutoffDate = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000)); cutoffDate = subtractDays(now, 7);
break; break;
case '30d': case '30d':
cutoffDate = new Date(now.getTime() - (30 * 24 * 60 * 60 * 1000)); cutoffDate = subtractDays(now, 30);
break; break;
case '3m': case '3m':
cutoffDate = new Date(now.getTime() - (90 * 24 * 60 * 60 * 1000)); cutoffDate = subtractDays(now, 90);
break; break;
case 'current': case 'current':
default: default:
@@ -56,7 +57,7 @@ function filterCurrentSprintAnalytics(analytics: JiraAnalytics): JiraAnalytics {
function filterAnalyticsByDate(analytics: JiraAnalytics, cutoffDate: Date): JiraAnalytics { function filterAnalyticsByDate(analytics: JiraAnalytics, cutoffDate: Date): JiraAnalytics {
// Filtrer l'historique des sprints // Filtrer l'historique des sprints
const filteredSprintHistory = analytics.velocityMetrics.sprintHistory.filter(sprint => { const filteredSprintHistory = analytics.velocityMetrics.sprintHistory.filter(sprint => {
const sprintEndDate = new Date(sprint.endDate); const sprintEndDate = parseDate(sprint.endDate);
return sprintEndDate >= cutoffDate; return sprintEndDate >= cutoffDate;
}); });

View File

@@ -151,22 +151,22 @@ function compareTasksByField(a: Task, b: Task, sortConfig: SortConfig): number {
case 'createdAt': case 'createdAt':
return compareValues( return compareValues(
new Date(a.createdAt).getTime(), a.createdAt.getTime(),
new Date(b.createdAt).getTime(), b.createdAt.getTime(),
direction direction
); );
case 'updatedAt': case 'updatedAt':
return compareValues( return compareValues(
new Date(a.updatedAt).getTime(), a.updatedAt.getTime(),
new Date(b.updatedAt).getTime(), b.updatedAt.getTime(),
direction direction
); );
case 'dueDate': case 'dueDate':
return compareValues( return compareValues(
a.dueDate ? new Date(a.dueDate).getTime() : null, a.dueDate ? a.dueDate.getTime() : null,
b.dueDate ? new Date(b.dueDate).getTime() : null, b.dueDate ? b.dueDate.getTime() : null,
direction direction
); );

View File

@@ -1,5 +1,6 @@
import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; import { Task, TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
import { prisma } from './database'; import { prisma } from './database';
import { getToday, parseDate, subtractDays, addDays } from '@/lib/date-utils';
export interface ProductivityMetrics { export interface ProductivityMetrics {
completionTrend: Array<{ completionTrend: Array<{
@@ -42,8 +43,8 @@ export class AnalyticsService {
*/ */
static async getProductivityMetrics(timeRange?: TimeRange): Promise<ProductivityMetrics> { static async getProductivityMetrics(timeRange?: TimeRange): Promise<ProductivityMetrics> {
try { try {
const now = new Date(); const now = getToday();
const defaultStart = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); // 30 jours const defaultStart = subtractDays(now, 30); // 30 jours
const start = timeRange?.start || defaultStart; const start = timeRange?.start || defaultStart;
const end = timeRange?.end || now; const end = timeRange?.end || now;
@@ -99,7 +100,7 @@ export class AnalyticsService {
const trend: Array<{ date: string; completed: number; created: number; total: number }> = []; const trend: Array<{ date: string; completed: number; created: number; total: number }> = [];
// Générer les dates pour la période // Générer les dates pour la période
const currentDate = new Date(start); const currentDate = new Date(start.getTime());
while (currentDate <= end) { while (currentDate <= end) {
const dateStr = currentDate.toISOString().split('T')[0]; const dateStr = currentDate.toISOString().split('T')[0];
@@ -116,7 +117,7 @@ export class AnalyticsService {
// Total cumulé jusqu'à ce jour // Total cumulé jusqu'à ce jour
const totalUntilThisDay = tasks.filter(task => const totalUntilThisDay = tasks.filter(task =>
new Date(task.createdAt) <= currentDate task.createdAt <= currentDate
).length; ).length;
trend.push({ trend.push({
@@ -156,7 +157,7 @@ export class AnalyticsService {
// Convertir en format pour le graphique // Convertir en format pour le graphique
weekGroups.forEach((count, weekKey) => { weekGroups.forEach((count, weekKey) => {
const weekDate = new Date(weekKey); const weekDate = parseDate(weekKey);
weeklyData.push({ weeklyData.push({
week: `Sem. ${this.getWeekNumber(weekDate)}`, week: `Sem. ${this.getWeekNumber(weekDate)}`,
completed: count, completed: count,
@@ -209,10 +210,10 @@ export class AnalyticsService {
* Calcule les statistiques hebdomadaires * Calcule les statistiques hebdomadaires
*/ */
private static calculateWeeklyStats(tasks: Task[]) { private static calculateWeeklyStats(tasks: Task[]) {
const now = new Date(); const now = getToday();
const thisWeekStart = this.getWeekStart(now); const thisWeekStart = this.getWeekStart(now);
const lastWeekStart = new Date(thisWeekStart.getTime() - 7 * 24 * 60 * 60 * 1000); const lastWeekStart = subtractDays(thisWeekStart, 7);
const lastWeekEnd = new Date(thisWeekStart.getTime() - 1); const lastWeekEnd = subtractDays(thisWeekStart, 1);
const thisWeekCompleted = tasks.filter(task => const thisWeekCompleted = tasks.filter(task =>
task.completedAt && task.completedAt &&
@@ -243,7 +244,7 @@ export class AnalyticsService {
* Obtient le début de la semaine pour une date * Obtient le début de la semaine pour une date
*/ */
private static getWeekStart(date: Date): Date { private static getWeekStart(date: Date): Date {
const d = new Date(date); const d = new Date(date.getTime());
const day = d.getDay(); const day = d.getDay();
const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Lundi = début de semaine const diff = d.getDate() - day + (day === 0 ? -6 : 1); // Lundi = début de semaine
return new Date(d.setDate(diff)); return new Date(d.setDate(diff));

View File

@@ -1,4 +1,5 @@
import { backupService, BackupConfig } from './backup'; import { backupService, BackupConfig } from './backup';
import { addMinutes, getToday } from '@/lib/date-utils';
export class BackupScheduler { export class BackupScheduler {
private timer: NodeJS.Timeout | null = null; private timer: NodeJS.Timeout | null = null;
@@ -106,7 +107,7 @@ export class BackupScheduler {
const config = backupService.getConfig(); const config = backupService.getConfig();
const intervalMs = this.getIntervalMs(config.interval); const intervalMs = this.getIntervalMs(config.interval);
return new Date(Date.now() + intervalMs); return addMinutes(getToday(), Math.floor(intervalMs / (1000 * 60)));
} }
/** /**

View File

@@ -3,6 +3,7 @@ import path from 'path';
import { prisma } from './database'; import { prisma } from './database';
import { userPreferencesService } from './user-preferences'; import { userPreferencesService } from './user-preferences';
import { BackupUtils } from '../lib/backup-utils'; import { BackupUtils } from '../lib/backup-utils';
import { getToday } from '@/lib/date-utils';
export interface BackupConfig { export interface BackupConfig {
enabled: boolean; enabled: boolean;
@@ -280,7 +281,7 @@ export class BackupService {
id: backupId, id: backupId,
filename: path.basename(finalPath), filename: path.basename(finalPath),
size: stats.size, size: stats.size,
createdAt: new Date(), createdAt: getToday(),
type, type,
status: 'success', status: 'success',
databaseHash, databaseHash,
@@ -289,7 +290,7 @@ export class BackupService {
// Sauvegarder les métadonnées du backup // Sauvegarder les métadonnées du backup
await this.saveBackupMetadata(path.basename(finalPath), { await this.saveBackupMetadata(path.basename(finalPath), {
databaseHash, databaseHash,
createdAt: new Date(), createdAt: getToday(),
type, type,
}); });
@@ -313,7 +314,7 @@ export class BackupService {
id: backupId, id: backupId,
filename, filename,
size: 0, size: 0,
createdAt: new Date(), createdAt: getToday(),
type, type,
status: 'failed', status: 'failed',
error: errorMessage, error: errorMessage,

View File

@@ -4,6 +4,7 @@
*/ */
import { JiraTask, JiraAnalytics, JiraAnalyticsFilters, AvailableFilters, FilterOption } from '@/lib/types'; import { JiraTask, JiraAnalytics, JiraAnalyticsFilters, AvailableFilters, FilterOption } from '@/lib/types';
import { parseDate } from '@/lib/date-utils';
export class JiraAdvancedFiltersService { export class JiraAdvancedFiltersService {
@@ -137,7 +138,7 @@ export class JiraAdvancedFiltersService {
// Filtrage par date // Filtrage par date
if (filters.dateRange) { if (filters.dateRange) {
const issueDate = new Date(issue.created); const issueDate = parseDate(issue.created);
if (issueDate < filters.dateRange.from || issueDate > filters.dateRange.to) { if (issueDate < filters.dateRange.from || issueDate > filters.dateRange.to) {
return false; return false;
} }

View File

@@ -5,6 +5,7 @@
import { JiraService } from './jira'; import { JiraService } from './jira';
import { jiraAnalyticsCache } from './jira-analytics-cache'; import { jiraAnalyticsCache } from './jira-analytics-cache';
import { getToday, parseDate, addDays, subtractDays } from '@/lib/date-utils';
import { import {
JiraAnalytics, JiraAnalytics,
JiraTask, JiraTask,
@@ -248,23 +249,23 @@ export class JiraAnalyticsService {
* Génère un historique de sprints basé sur les dates de création/résolution des tickets * Génère un historique de sprints basé sur les dates de création/résolution des tickets
*/ */
private generateSprintHistoryFromIssues(allIssues: JiraTask[], completedIssues: JiraTask[]): SprintVelocity[] { private generateSprintHistoryFromIssues(allIssues: JiraTask[], completedIssues: JiraTask[]): SprintVelocity[] {
const now = new Date(); const now = getToday();
const sprintHistory: SprintVelocity[] = []; const sprintHistory: SprintVelocity[] = [];
// Créer 4 périodes de 2 semaines (8 semaines au total) // Créer 4 périodes de 2 semaines (8 semaines au total)
for (let i = 3; i >= 0; i--) { for (let i = 3; i >= 0; i--) {
const endDate = new Date(now.getTime() - (i * 14 * 24 * 60 * 60 * 1000)); const endDate = subtractDays(now, i * 14);
const startDate = new Date(endDate.getTime() - (14 * 24 * 60 * 60 * 1000)); const startDate = subtractDays(endDate, 14);
// Compter les tickets complétés dans cette période // Compter les tickets complétés dans cette période
const completedInPeriod = completedIssues.filter(issue => { const completedInPeriod = completedIssues.filter(issue => {
const updatedDate = new Date(issue.updated); const updatedDate = parseDate(issue.updated);
return updatedDate >= startDate && updatedDate <= endDate; return updatedDate >= startDate && updatedDate <= endDate;
}); });
// Compter les tickets créés dans cette période (approximation du planifié) // Compter les tickets créés dans cette période (approximation du planifié)
const createdInPeriod = allIssues.filter(issue => { const createdInPeriod = allIssues.filter(issue => {
const createdDate = new Date(issue.created); const createdDate = parseDate(issue.created);
return createdDate >= startDate && createdDate <= endDate; return createdDate >= startDate && createdDate <= endDate;
}); });
@@ -315,8 +316,8 @@ export class JiraAnalyticsService {
const cycleTimes = completedIssues const cycleTimes = completedIssues
.filter(issue => issue.created && issue.updated) // S'assurer qu'on a les dates .filter(issue => issue.created && issue.updated) // S'assurer qu'on a les dates
.map(issue => { .map(issue => {
const created = new Date(issue.created); const created = parseDate(issue.created);
const resolved = new Date(issue.updated); const resolved = parseDate(issue.updated);
const days = Math.max(0.1, (resolved.getTime() - created.getTime()) / (1000 * 60 * 60 * 24)); // Minimum 0.1 jour const days = Math.max(0.1, (resolved.getTime() - created.getTime()) / (1000 * 60 * 60 * 24)); // Minimum 0.1 jour
return Math.round(days * 10) / 10; // Arrondir à 1 décimale return Math.round(days * 10) / 10; // Arrondir à 1 décimale
}) })

View File

@@ -4,6 +4,7 @@
*/ */
import { JiraAnalytics, SprintVelocity, CycleTimeByType, AssigneeWorkload } from '@/lib/types'; import { JiraAnalytics, SprintVelocity, CycleTimeByType, AssigneeWorkload } from '@/lib/types';
import { getToday } from '@/lib/date-utils';
export interface JiraAnomaly { export interface JiraAnomaly {
id: string; id: string;
@@ -44,7 +45,7 @@ export class JiraAnomalyDetectionService {
*/ */
async detectAnomalies(analytics: JiraAnalytics): Promise<JiraAnomaly[]> { async detectAnomalies(analytics: JiraAnalytics): Promise<JiraAnomaly[]> {
const anomalies: JiraAnomaly[] = []; const anomalies: JiraAnomaly[] = [];
const timestamp = new Date().toISOString(); const timestamp = getToday().toISOString();
// 1. Détection d'anomalies de vélocité // 1. Détection d'anomalies de vélocité
const velocityAnomalies = this.detectVelocityAnomalies(analytics.velocityMetrics, timestamp); const velocityAnomalies = this.detectVelocityAnomalies(analytics.velocityMetrics, timestamp);

View File

@@ -1,5 +1,6 @@
import { userPreferencesService } from './user-preferences'; import { userPreferencesService } from './user-preferences';
import { JiraService } from './jira'; import { JiraService } from './jira';
import { addMinutes, getToday } from '@/lib/date-utils';
export interface JiraSchedulerConfig { export interface JiraSchedulerConfig {
enabled: boolean; enabled: boolean;
@@ -143,7 +144,7 @@ export class JiraScheduler {
const config = await this.getConfig(); const config = await this.getConfig();
const intervalMs = this.getIntervalMs(config.interval); const intervalMs = this.getIntervalMs(config.interval);
return new Date(Date.now() + intervalMs); return addMinutes(getToday(), Math.floor(intervalMs / (1000 * 60)));
} }
/** /**

View File

@@ -1,5 +1,6 @@
import { prisma } from './database'; import { prisma } from './database';
import { startOfWeek, endOfWeek } from 'date-fns'; import { startOfWeek, endOfWeek } from 'date-fns';
import { getToday } from '@/lib/date-utils';
type TaskType = { type TaskType = {
id: string; id: string;
@@ -84,7 +85,7 @@ export class ManagerSummaryService {
/** /**
* Génère un résumé orienté manager pour la semaine * Génère un résumé orienté manager pour la semaine
*/ */
static async getManagerSummary(date: Date = new Date()): Promise<ManagerSummary> { static async getManagerSummary(date: Date = getToday()): Promise<ManagerSummary> {
const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi
const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche
@@ -249,7 +250,7 @@ export class ManagerSummaryService {
description: task.description || undefined, description: task.description || undefined,
tags: task.taskTags?.map(tt => tt.tag.name) || [], tags: task.taskTags?.map(tt => tt.tag.name) || [],
impact, impact,
completedAt: task.completedAt || new Date(), completedAt: task.completedAt || getToday(),
relatedItems: [task.id, ...relatedTodos.map(t => t.id)], relatedItems: [task.id, ...relatedTodos.map(t => t.id)],
todosCount: relatedTodos.length // Nombre réel de todos associés todosCount: relatedTodos.length // Nombre réel de todos associés
}); });
@@ -322,7 +323,7 @@ export class ManagerSummaryService {
where: { where: {
isChecked: false, isChecked: false,
date: { date: {
gte: new Date() gte: getToday()
}, },
OR: [ OR: [
{ type: 'meeting' }, { type: 'meeting' },

View File

@@ -1,7 +1,7 @@
import { prisma } from './database'; import { prisma } from './database';
import { startOfWeek, endOfWeek, eachDayOfInterval, format, startOfDay, endOfDay } from 'date-fns'; import { startOfWeek, endOfWeek, eachDayOfInterval, format, startOfDay, endOfDay } from 'date-fns';
import { fr } from 'date-fns/locale'; import { fr } from 'date-fns/locale';
import { formatDateForAPI, getDayName, getToday } from '@/lib/date-utils'; import { formatDateForAPI, getDayName, getToday, subtractDays } from '@/lib/date-utils';
export interface DailyMetrics { export interface DailyMetrics {
date: string; // Format ISO date: string; // Format ISO
@@ -326,7 +326,7 @@ export class MetricsService {
const trends = []; const trends = [];
for (let i = weeksBack - 1; i >= 0; i--) { for (let i = weeksBack - 1; i >= 0; i--) {
const weekStart = startOfWeek(new Date(Date.now() - i * 7 * 24 * 60 * 60 * 1000), { weekStartsOn: 1 }); const weekStart = startOfWeek(subtractDays(getToday(), i * 7), { weekStartsOn: 1 });
const weekEnd = endOfWeek(weekStart, { weekStartsOn: 1 }); const weekEnd = endOfWeek(weekStart, { weekStartsOn: 1 });
const [completed, created] = await Promise.all([ const [completed, created] = await Promise.all([

View File

@@ -1,6 +1,7 @@
import { prisma } from './database'; import { prisma } from './database';
import { readFile } from 'fs/promises'; import { readFile } from 'fs/promises';
import { join } from 'path'; import { join } from 'path';
import { getToday, parseDate } from '@/lib/date-utils';
export interface SystemInfo { export interface SystemInfo {
version: string; version: string;
@@ -168,8 +169,8 @@ export class SystemInfoService {
const fs = require('fs'); // eslint-disable-line @typescript-eslint/no-require-imports const fs = require('fs'); // eslint-disable-line @typescript-eslint/no-require-imports
const packagePath = join(process.cwd(), 'package.json'); const packagePath = join(process.cwd(), 'package.json');
const stats = fs.statSync(packagePath); const stats = fs.statSync(packagePath);
const now = new Date(); const now = getToday();
const lastModified = new Date(stats.mtime); const lastModified = parseDate(stats.mtime.toISOString());
const diffTime = Math.abs(now.getTime() - lastModified.getTime()); const diffTime = Math.abs(now.getTime() - lastModified.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));