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:
@@ -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') {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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())) {
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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' },
|
||||||
|
|||||||
@@ -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([
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user