refactor: enhance date handling across components

- Replaced direct date manipulations with utility functions for consistency and readability.
- Updated date formatting in `DailyCalendar`, `RecentTasks`, `CompletionRateChart`, and other components to use `formatDateShort` and `formatDateForDisplay`.
- Improved date parsing in `JiraLogs`, `JiraSchedulerConfig`, and `BackupSettingsPageClient` to ensure proper handling of date strings.
- Streamlined date initialization in `useDaily` and `DailyService` to utilize `getToday` and `getYesterday` for better clarity.
This commit is contained in:
Julien Froidefond
2025-09-21 12:02:06 +02:00
parent 557cdebc13
commit c3c1d24fa2
15 changed files with 39 additions and 46 deletions

View File

@@ -4,6 +4,8 @@ import React, { useState } from 'react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { formatDateForAPI, createDate, getToday } from '@/lib/date-utils'; import { formatDateForAPI, createDate, getToday } from '@/lib/date-utils';
import { format } from 'date-fns';
import { fr } from 'date-fns/locale';
interface DailyCalendarProps { interface DailyCalendarProps {
currentDate: Date; currentDate: Date;
@@ -97,10 +99,7 @@ export function DailyCalendar({
}; };
const formatMonthYear = () => { const formatMonthYear = () => {
return viewDate.toLocaleDateString('fr-FR', { return format(viewDate, 'MMMM yyyy', { locale: fr });
month: 'long',
year: 'numeric',
});
}; };
const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']; const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];

View File

@@ -3,6 +3,7 @@
import { Task } from '@/lib/types'; import { Task } from '@/lib/types';
import { Card } from '@/components/ui/Card'; import { Card } from '@/components/ui/Card';
import { TagDisplay } from '@/components/ui/TagDisplay'; import { TagDisplay } from '@/components/ui/TagDisplay';
import { formatDateShort } from '@/lib/date-utils';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { useTasksContext } from '@/contexts/TasksContext'; import { useTasksContext } from '@/contexts/TasksContext';
import { getPriorityConfig, getPriorityColorHex, getStatusBadgeClasses, getStatusLabel } from '@/lib/status-config'; import { getPriorityConfig, getPriorityColorHex, getStatusBadgeClasses, getStatusLabel } from '@/lib/status-config';
@@ -18,7 +19,7 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
// Prendre les 5 tâches les plus récentes (créées ou modifiées) // Prendre les 5 tâches les plus récentes (créées ou modifiées)
const recentTasks = tasks const recentTasks = tasks
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()) .sort((a, b) => b.updatedAt.getTime() - a.updatedAt.getTime())
.slice(0, 5); .slice(0, 5);
// Fonctions simplifiées utilisant la configuration centralisée // Fonctions simplifiées utilisant la configuration centralisée
@@ -116,10 +117,7 @@ export function RecentTasks({ tasks }: RecentTasksProps) {
</div> </div>
<div className="text-xs text-[var(--muted-foreground)] whitespace-nowrap"> <div className="text-xs text-[var(--muted-foreground)] whitespace-nowrap">
{new Date(task.updatedAt).toLocaleDateString('fr-FR', { {formatDateShort(task.updatedAt)}
day: 'numeric',
month: 'short'
})}
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,6 +2,7 @@
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { DailyMetrics } from '@/services/metrics'; import { DailyMetrics } from '@/services/metrics';
import { parseDate, formatDateShort } from '@/lib/date-utils';
interface CompletionRateChartProps { interface CompletionRateChartProps {
data: DailyMetrics[]; data: DailyMetrics[];
@@ -12,7 +13,7 @@ export function CompletionRateChart({ data, className }: CompletionRateChartProp
// Transformer les données pour le graphique // Transformer les données pour le graphique
const chartData = data.map(day => ({ const chartData = data.map(day => ({
day: day.dayName.substring(0, 3), // Lun, Mar, etc. day: day.dayName.substring(0, 3), // Lun, Mar, etc.
date: new Date(day.date).toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit' }), date: formatDateShort(parseDate(day.date)),
completionRate: day.completionRate, completionRate: day.completionRate,
completed: day.completed, completed: day.completed,
total: day.totalTasks total: day.totalTasks

View File

@@ -4,7 +4,7 @@ import { useState, useEffect, useCallback, useTransition } from 'react';
import { DailyCheckbox } from '@/lib/types'; import { DailyCheckbox } from '@/lib/types';
import { tasksClient } from '@/clients/tasks-client'; import { tasksClient } from '@/clients/tasks-client';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { formatDateSmart } from '@/lib/date-utils'; import { formatDateSmart, parseDate } from '@/lib/date-utils';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { addTodoToTask, toggleCheckbox } from '@/actions/daily'; import { addTodoToTask, toggleCheckbox } from '@/actions/daily';
@@ -42,7 +42,7 @@ export function RelatedTodos({ taskId }: RelatedTodosProps) {
startTransition(async () => { startTransition(async () => {
try { try {
// Si une date est spécifiée, l'utiliser, sinon undefined (aujourd'hui par défaut) // Si une date est spécifiée, l'utiliser, sinon undefined (aujourd'hui par défaut)
const targetDate = newTodoDate ? new Date(newTodoDate) : undefined; const targetDate = newTodoDate ? parseDate(newTodoDate) : undefined;
const result = await addTodoToTask(taskId, newTodoText, targetDate); const result = await addTodoToTask(taskId, newTodoText, targetDate);
@@ -79,7 +79,7 @@ export function RelatedTodos({ taskId }: RelatedTodosProps) {
const formatDate = (date: Date | string) => { const formatDate = (date: Date | string) => {
try { try {
const dateObj = typeof date === 'string' ? new Date(date) : date; const dateObj = typeof date === 'string' ? parseDate(date) : date;
if (isNaN(dateObj.getTime())) { if (isNaN(dateObj.getTime())) {
return 'Date invalide'; return 'Date invalide';
} }

View File

@@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { formatDistanceToNow } from 'date-fns'; import { formatDistanceToNow } from 'date-fns';
import { fr } from 'date-fns/locale'; import { fr } from 'date-fns/locale';
import { parseDate } from '@/lib/date-utils';
interface SyncLog { interface SyncLog {
id: string; id: string;
@@ -111,7 +112,7 @@ export function JiraLogs({ className = "" }: JiraLogsProps) {
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
{getStatusBadge(log.status)} {getStatusBadge(log.status)}
<span className="text-xs text-[var(--muted-foreground)]"> <span className="text-xs text-[var(--muted-foreground)]">
{formatDistanceToNow(new Date(log.createdAt), { {formatDistanceToNow(parseDate(log.createdAt), {
addSuffix: true, addSuffix: true,
locale: fr locale: fr
})} })}

View File

@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { jiraClient, JiraSchedulerStatus } from '@/clients/jira-client'; import { jiraClient, JiraSchedulerStatus } from '@/clients/jira-client';
import { parseDate, getToday } from '@/lib/date-utils';
interface JiraSchedulerConfigProps { interface JiraSchedulerConfigProps {
className?: string; className?: string;
@@ -85,8 +86,8 @@ export function JiraSchedulerConfig({ className = "" }: JiraSchedulerConfigProps
const getNextSyncText = () => { const getNextSyncText = () => {
if (!schedulerStatus?.nextSync) return 'Aucune synchronisation planifiée'; if (!schedulerStatus?.nextSync) return 'Aucune synchronisation planifiée';
const nextSync = new Date(schedulerStatus.nextSync); const nextSync = parseDate(schedulerStatus.nextSync);
const now = new Date(); const now = getToday();
const diffMs = nextSync.getTime() - now.getTime(); const diffMs = nextSync.getTime() - now.getTime();
if (diffMs <= 0) return 'Synchronisation en cours...'; if (diffMs <= 0) return 'Synchronisation en cours...';

View File

@@ -4,6 +4,7 @@ import { useState } from 'react';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { getToday } from '@/lib/date-utils';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { jiraClient } from '@/clients/jira-client'; import { jiraClient } from '@/clients/jira-client';
import { JiraSyncResult, JiraSyncAction } from '@/services/jira'; import { JiraSyncResult, JiraSyncAction } from '@/services/jira';
@@ -79,7 +80,7 @@ export function JiraSync({ onSyncComplete, className = "" }: JiraSyncProps) {
{success ? "✓ Succès" : "⚠ Erreurs"} {success ? "✓ Succès" : "⚠ Erreurs"}
</Badge> </Badge>
<span className="text-[var(--muted-foreground)] text-xs"> <span className="text-[var(--muted-foreground)] text-xs">
{new Date().toLocaleTimeString()} {getToday().toLocaleTimeString()}
</span> </span>
</div> </div>
<div className="text-xs text-[var(--muted-foreground)]"> <div className="text-xs text-[var(--muted-foreground)]">

View File

@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback } from 'react';
import { SprintVelocity, JiraTask, AssigneeDistribution, StatusDistribution } from '@/lib/types'; import { SprintVelocity, JiraTask, AssigneeDistribution, StatusDistribution } from '@/lib/types';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { parseDate, formatDateForDisplay } from '@/lib/date-utils';
import { Badge } from '@/components/ui/Badge'; import { Badge } from '@/components/ui/Badge';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
@@ -144,7 +145,7 @@ export default function SprintDetailModal({
<div className="text-center"> <div className="text-center">
<div className="text-sm text-gray-600">Période</div> <div className="text-sm text-gray-600">Période</div>
<div className="text-xs text-gray-500"> <div className="text-xs text-gray-500">
{new Date(sprint.startDate).toLocaleDateString('fr-FR')} - {new Date(sprint.endDate).toLocaleDateString('fr-FR')} {formatDateForDisplay(parseDate(sprint.startDate))} - {formatDateForDisplay(parseDate(sprint.endDate))}
</div> </div>
</div> </div>
</div> </div>
@@ -318,7 +319,7 @@ export default function SprintDetailModal({
<div className="flex items-center gap-4 text-xs text-gray-500"> <div className="flex items-center gap-4 text-xs text-gray-500">
<span>📋 {issue.issuetype.name}</span> <span>📋 {issue.issuetype.name}</span>
<span>👤 {issue.assignee?.displayName || 'Non assigné'}</span> <span>👤 {issue.assignee?.displayName || 'Non assigné'}</span>
<span>📅 {new Date(issue.created).toLocaleDateString('fr-FR')}</span> <span>📅 {formatDateForDisplay(parseDate(issue.created))}</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -427,7 +427,7 @@ export function TaskCard({ task, onEdit, compactView = false }: TaskCardProps) {
{task.dueDate ? ( {task.dueDate ? (
<span className="flex items-center gap-1 text-[var(--muted-foreground)] font-mono"> <span className="flex items-center gap-1 text-[var(--muted-foreground)] font-mono">
<span className="text-[var(--primary)]"></span> <span className="text-[var(--primary)]"></span>
{formatDistanceToNow(new Date(task.dueDate), { {formatDistanceToNow(task.dueDate, {
addSuffix: true, addSuffix: true,
locale: fr locale: fr
})} })}

View File

@@ -8,6 +8,7 @@ import { Button } from '@/components/ui/Button';
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext'; import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
import { backupClient, BackupListResponse } from '@/clients/backup-client'; import { backupClient, BackupListResponse } from '@/clients/backup-client';
import Link from 'next/link'; import Link from 'next/link';
import { parseDate, getToday, formatDateForDisplay } from '@/lib/date-utils';
interface DatabaseStats { interface DatabaseStats {
taskCount: number; taskCount: number;
@@ -86,22 +87,14 @@ export function AdvancedSettingsPageClient({
const formatTimeAgo = (date: Date): string => { const formatTimeAgo = (date: Date): string => {
// Format fixe pour éviter les erreurs d'hydratation // Format fixe pour éviter les erreurs d'hydratation
const d = new Date(date); return formatDateForDisplay(date, 'DISPLAY_MEDIUM');
return d.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
hour12: false
});
}; };
const getNextBackupTime = (): string => { const getNextBackupTime = (): string => {
if (!backupData.scheduler.nextBackup) return 'Non planifiée'; if (!backupData.scheduler.nextBackup) return 'Non planifiée';
const nextBackup = new Date(backupData.scheduler.nextBackup); const nextBackup = parseDate(backupData.scheduler.nextBackup);
const now = new Date(); const now = getToday();
const diffMs = nextBackup.getTime() - now.getTime(); const diffMs = nextBackup.getTime() - now.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60)); const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMins / 60); const diffHours = Math.floor(diffMins / 60);

View File

@@ -8,7 +8,7 @@ import { Card, CardHeader, CardContent } from '@/components/ui/Card';
import { Input } from '@/components/ui/Input'; import { Input } from '@/components/ui/Input';
import { Modal } from '@/components/ui/Modal'; import { Modal } from '@/components/ui/Modal';
import { Header } from '@/components/ui/Header'; import { Header } from '@/components/ui/Header';
import { formatDateForDisplay } from '@/lib/date-utils'; import { formatDateForDisplay, parseDate, getToday } from '@/lib/date-utils';
import Link from 'next/link'; import Link from 'next/link';
interface BackupSettingsPageClientProps { interface BackupSettingsPageClientProps {
@@ -209,8 +209,8 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
const getNextBackupTime = (): string => { const getNextBackupTime = (): string => {
if (!data?.scheduler.nextBackup) return 'Non planifiée'; if (!data?.scheduler.nextBackup) return 'Non planifiée';
const nextBackup = new Date(data.scheduler.nextBackup); const nextBackup = parseDate(data.scheduler.nextBackup);
const now = new Date(); const now = getToday();
const diffMs = nextBackup.getTime() - now.getTime(); const diffMs = nextBackup.getTime() - now.getTime();
const diffMins = Math.floor(diffMs / (1000 * 60)); const diffMins = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMins / 60); const diffHours = Math.floor(diffMins / 60);

View File

@@ -10,6 +10,7 @@ import { Input } from '@/components/ui/Input';
import { TagForm } from '@/components/forms/TagForm'; import { TagForm } from '@/components/forms/TagForm';
import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext'; import { UserPreferencesProvider } from '@/contexts/UserPreferencesContext';
import Link from 'next/link'; import Link from 'next/link';
import { formatDateForDisplay } from '@/lib/date-utils';
interface GeneralSettingsPageClientProps { interface GeneralSettingsPageClientProps {
initialPreferences: UserPreferences; initialPreferences: UserPreferences;
@@ -294,7 +295,7 @@ export function GeneralSettingsPageClient({ initialPreferences, initialTags }: G
</div> </div>
{('createdAt' in tag && (tag as Tag & { createdAt: Date }).createdAt) && ( {('createdAt' in tag && (tag as Tag & { createdAt: Date }).createdAt) && (
<div className="text-xs text-[var(--muted-foreground)]"> <div className="text-xs text-[var(--muted-foreground)]">
Créé le {new Date((tag as Tag & { createdAt: Date }).createdAt).toLocaleDateString('fr-FR')} Créé le {formatDateForDisplay((tag as Tag & { createdAt: Date }).createdAt)}
</div> </div>
)} )}
</div> </div>

View File

@@ -43,7 +43,7 @@ interface UseDailyActions {
* Hook pour la gestion d'une vue daily spécifique * Hook pour la gestion d'une vue daily spécifique
*/ */
export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseDailyState & UseDailyActions & { currentDate: Date } { export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseDailyState & UseDailyActions & { currentDate: Date } {
const [currentDate, setCurrentDate] = useState<Date>(initialDate || new Date()); const [currentDate, setCurrentDate] = useState<Date>(initialDate || getToday());
const [dailyView, setDailyView] = useState<DailyView | null>(initialDailyView || null); const [dailyView, setDailyView] = useState<DailyView | null>(initialDailyView || null);
const [loading, setLoading] = useState(!initialDailyView); // Pas de loading si on a des données SSR const [loading, setLoading] = useState(!initialDailyView); // Pas de loading si on a des données SSR
const [refreshing, setRefreshing] = useState(false); // Pour les refresh silencieux const [refreshing, setRefreshing] = useState(false); // Pour les refresh silencieux

View File

@@ -1,7 +1,7 @@
import { prisma } from './database'; import { prisma } from './database';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType, TaskStatus, TaskPriority, TaskSource } from '@/lib/types'; import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType, TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
import { getPreviousWorkday, normalizeDate, formatDateForAPI } from '@/lib/date-utils'; import { getPreviousWorkday, normalizeDate, formatDateForAPI, getToday, getYesterday } from '@/lib/date-utils';
/** /**
* Service pour la gestion des checkboxes daily * Service pour la gestion des checkboxes daily
@@ -180,7 +180,7 @@ export class DailyService {
* Récupère la vue daily d'aujourd'hui * Récupère la vue daily d'aujourd'hui
*/ */
async getTodaysDailyView(): Promise<DailyView> { async getTodaysDailyView(): Promise<DailyView> {
return this.getDailyView(new Date()); return this.getDailyView(getToday());
} }
/** /**
@@ -188,7 +188,7 @@ export class DailyService {
*/ */
async addTodayCheckbox(text: string, taskId?: string): Promise<DailyCheckbox> { async addTodayCheckbox(text: string, taskId?: string): Promise<DailyCheckbox> {
return this.addCheckbox({ return this.addCheckbox({
date: new Date(), date: getToday(),
text, text,
taskId taskId
}); });
@@ -198,11 +198,8 @@ export class DailyService {
* Ajoute une checkbox pour hier * Ajoute une checkbox pour hier
*/ */
async addYesterdayCheckbox(text: string, taskId?: string): Promise<DailyCheckbox> { async addYesterdayCheckbox(text: string, taskId?: string): Promise<DailyCheckbox> {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
return this.addCheckbox({ return this.addCheckbox({
date: yesterday, date: getYesterday(),
text, text,
taskId taskId
}); });

View File

@@ -5,7 +5,7 @@
import { JiraTask } from '@/lib/types'; import { JiraTask } from '@/lib/types';
import { prisma } from './database'; import { prisma } from './database';
import { parseDate } from '@/lib/date-utils'; import { parseDate, formatDateForDisplay } from '@/lib/date-utils';
export interface JiraConfig { export interface JiraConfig {
baseUrl: string; baseUrl: string;
@@ -389,8 +389,8 @@ export class JiraService {
changes.push(`Priorité: préservée localement (${existingTask.priority})`); changes.push(`Priorité: préservée localement (${existingTask.priority})`);
} }
if ((existingTask.dueDate?.getTime() || null) !== (taskData.dueDate?.getTime() || null)) { if ((existingTask.dueDate?.getTime() || null) !== (taskData.dueDate?.getTime() || null)) {
const oldDate = existingTask.dueDate ? existingTask.dueDate.toLocaleDateString() : 'Aucune'; const oldDate = existingTask.dueDate ? formatDateForDisplay(existingTask.dueDate) : 'Aucune';
const newDate = taskData.dueDate ? taskData.dueDate.toLocaleDateString() : 'Aucune'; const newDate = taskData.dueDate ? formatDateForDisplay(taskData.dueDate) : 'Aucune';
changes.push(`Échéance: ${oldDate}${newDate}`); changes.push(`Échéance: ${oldDate}${newDate}`);
} }
if (existingTask.jiraProject !== taskData.jiraProject) { if (existingTask.jiraProject !== taskData.jiraProject) {