refactor: date utils and all calls
This commit is contained in:
4
TODO.md
4
TODO.md
@@ -2,9 +2,9 @@
|
||||
|
||||
## Autre Todos #2
|
||||
- [x] Synchro Jira auto en background timé comme pour la synchro de sauvegarde
|
||||
- [ ] refacto des allpreferences : ca devrait eter un contexte dans le layout qui balance serverside dans le hook
|
||||
- [ ] refacto des getallpreferences en frontend : ca devrait eter un contexte dans le layout qui balance serverside dans le hook
|
||||
- [x] backups : ne backuper que si il y a eu un changement entre le dernier backup et la base actuelle
|
||||
- [ ] refacto des dates avec le utils qui pour l'instant n'est pas utilisé
|
||||
- [x] refacto des dates avec le utils qui pour l'instant n'est pas utilisé
|
||||
- [ ] split de certains gros composants.
|
||||
- [x] Page jira-dashboard : onglets analytics avancés et Qualité et collaboration : les charts sortent des cards; il faut reprendre la UI pour que ce soit consistant.
|
||||
- [x] Page Daily : les mots aujourd'hui et hier ne fonctionnent dans les titres que si c'est vraiment aujourd'hui :)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
import { backupService, BackupConfig } from '../src/services/backup';
|
||||
import { backupScheduler } from '../src/services/backup-scheduler';
|
||||
import { formatDateForDisplay } from '../src/lib/date-utils';
|
||||
|
||||
interface CliOptions {
|
||||
command: string;
|
||||
@@ -92,7 +93,7 @@ OPTIONS:
|
||||
}
|
||||
|
||||
private formatDate(date: Date): string {
|
||||
return new Date(date).toLocaleString('fr-FR');
|
||||
return formatDateForDisplay(date, 'DISPLAY_LONG');
|
||||
}
|
||||
|
||||
async run(args: string[]): Promise<void> {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { dailyService } from '@/services/daily';
|
||||
import { UpdateDailyCheckboxData, DailyCheckbox, CreateDailyCheckboxData } from '@/lib/types';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { getToday, getPreviousWorkday, parseDate, normalizeDate } from '@/lib/date-utils';
|
||||
|
||||
/**
|
||||
* Toggle l'état d'une checkbox
|
||||
@@ -19,7 +20,7 @@ export async function toggleCheckbox(checkboxId: string): Promise<{
|
||||
// (le front-end gère déjà l'état optimiste)
|
||||
|
||||
// Récupérer toutes les checkboxes d'aujourd'hui et hier pour trouver celle à toggle
|
||||
const today = new Date();
|
||||
const today = getToday();
|
||||
const dailyView = await dailyService.getDailyView(today);
|
||||
|
||||
let checkbox = dailyView.today.find(cb => cb.id === checkboxId);
|
||||
@@ -57,7 +58,7 @@ export async function addCheckboxToDaily(dailyId: string, content: string, taskI
|
||||
}> {
|
||||
try {
|
||||
// Le dailyId correspond à la date au format YYYY-MM-DD
|
||||
const date = new Date(dailyId);
|
||||
const date = parseDate(dailyId);
|
||||
|
||||
const newCheckbox = await dailyService.addCheckbox({
|
||||
date,
|
||||
@@ -86,7 +87,7 @@ export async function addTodayCheckbox(content: string, type?: 'task' | 'meeting
|
||||
}> {
|
||||
try {
|
||||
const newCheckbox = await dailyService.addCheckbox({
|
||||
date: new Date(),
|
||||
date: getToday(),
|
||||
text: content,
|
||||
type: type || 'task',
|
||||
taskId
|
||||
@@ -112,8 +113,7 @@ export async function addYesterdayCheckbox(content: string, type?: 'task' | 'mee
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
const yesterday = getPreviousWorkday(getToday());
|
||||
|
||||
const newCheckbox = await dailyService.addCheckbox({
|
||||
date: yesterday,
|
||||
@@ -209,8 +209,7 @@ export async function addTodoToTask(taskId: string, text: string, date?: Date):
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const targetDate = date || new Date();
|
||||
targetDate.setHours(0, 0, 0, 0);
|
||||
const targetDate = normalizeDate(date || getToday());
|
||||
|
||||
const checkboxData: CreateDailyCheckboxData = {
|
||||
date: targetDate,
|
||||
@@ -243,7 +242,7 @@ export async function reorderCheckboxes(dailyId: string, checkboxIds: string[]):
|
||||
}> {
|
||||
try {
|
||||
// Le dailyId correspond à la date au format YYYY-MM-DD
|
||||
const date = new Date(dailyId);
|
||||
const date = parseDate(dailyId);
|
||||
|
||||
await dailyService.reorderCheckboxes(date, checkboxIds);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { getJiraAnalytics } from './jira-analytics';
|
||||
import { formatDateForDisplay, getToday } from '@/lib/date-utils';
|
||||
|
||||
export type ExportFormat = 'csv' | 'json';
|
||||
|
||||
@@ -142,7 +143,7 @@ function generateCSV(analytics: JiraAnalytics): string {
|
||||
// Header du rapport
|
||||
lines.push('# Rapport Analytics Jira');
|
||||
lines.push(`# Projet: ${analytics.project.name} (${analytics.project.key})`);
|
||||
lines.push(`# Généré le: ${new Date().toLocaleString('fr-FR')}`);
|
||||
lines.push(`# Généré le: ${formatDateForDisplay(getToday(), 'DISPLAY_LONG')}`);
|
||||
lines.push(`# Total tickets: ${analytics.project.totalIssues}`);
|
||||
lines.push('');
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use server';
|
||||
|
||||
import { MetricsService, WeeklyMetricsOverview, VelocityTrend } from '@/services/metrics';
|
||||
import { getToday } from '@/lib/date-utils';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
|
||||
/**
|
||||
@@ -12,7 +13,7 @@ export async function getWeeklyMetrics(date?: Date): Promise<{
|
||||
error?: string;
|
||||
}> {
|
||||
try {
|
||||
const targetDate = date || new Date();
|
||||
const targetDate = date || getToday();
|
||||
const metrics = await MetricsService.getWeeklyMetrics(targetDate);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { dailyService } from '@/services/daily';
|
||||
import { getToday, parseDate, isValidAPIDate } from '@/lib/date-utils';
|
||||
|
||||
/**
|
||||
* API route pour récupérer la vue daily (hier + aujourd'hui)
|
||||
@@ -32,13 +33,18 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
// Vue daily pour une date donnée (ou aujourd'hui par défaut)
|
||||
const targetDate = date ? new Date(date) : new Date();
|
||||
let targetDate: Date;
|
||||
|
||||
if (date && isNaN(targetDate.getTime())) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
|
||||
{ status: 400 }
|
||||
);
|
||||
if (date) {
|
||||
if (!isValidAPIDate(date)) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
targetDate = parseDate(date);
|
||||
} else {
|
||||
targetDate = getToday();
|
||||
}
|
||||
|
||||
const dailyView = await dailyService.getDailyView(targetDate);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { DailyCalendar } from '@/components/daily/DailyCalendar';
|
||||
import { DailySection } from '@/components/daily/DailySection';
|
||||
import { dailyClient } from '@/clients/daily-client';
|
||||
import { Header } from '@/components/ui/Header';
|
||||
import { getPreviousWorkday } from '@/lib/workday-utils';
|
||||
import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils';
|
||||
|
||||
interface DailyPageClientProps {
|
||||
initialDailyView?: DailyView;
|
||||
@@ -112,37 +112,23 @@ export function DailyPageClient({
|
||||
};
|
||||
|
||||
const formatCurrentDate = () => {
|
||||
return currentDate.toLocaleDateString('fr-FR', {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
return formatDateLong(currentDate);
|
||||
};
|
||||
|
||||
const isToday = () => {
|
||||
const today = new Date();
|
||||
return currentDate.toDateString() === today.toDateString();
|
||||
const isTodayDate = () => {
|
||||
return isToday(currentDate);
|
||||
};
|
||||
|
||||
const getTodayTitle = () => {
|
||||
const today = new Date();
|
||||
if (currentDate.toDateString() === today.toDateString()) {
|
||||
return "🎯 Aujourd'hui";
|
||||
}
|
||||
return `🎯 ${currentDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit' })}`;
|
||||
return generateDateTitle(currentDate, '🎯');
|
||||
};
|
||||
|
||||
const getYesterdayTitle = () => {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const yesterdayDate = getYesterdayDate();
|
||||
if (yesterdayDate.toDateString() === yesterday.toDateString()) {
|
||||
if (isYesterday(yesterdayDate)) {
|
||||
return "📋 Hier";
|
||||
}
|
||||
return `📋 ${yesterdayDate.toLocaleDateString('fr-FR', { day: '2-digit', month: '2-digit', year: '2-digit' })}`;
|
||||
return `📋 ${formatDateShort(yesterdayDate)}`;
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
@@ -198,7 +184,7 @@ export function DailyPageClient({
|
||||
<div className="text-sm font-bold text-[var(--foreground)] font-mono">
|
||||
{formatCurrentDate()}
|
||||
</div>
|
||||
{!isToday() && (
|
||||
{!isTodayDate() && (
|
||||
<button
|
||||
onClick={goToToday}
|
||||
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { httpClient } from './base/http-client';
|
||||
import { DailyCheckbox, DailyView, Task } from '@/lib/types';
|
||||
import { formatDateForAPI, parseDate } from '@/lib/date-utils';
|
||||
|
||||
// Types pour les réponses API (avec dates en string)
|
||||
interface ApiCheckbox {
|
||||
@@ -97,10 +98,7 @@ export class DailyClient {
|
||||
* Formate une date pour l'API (évite les décalages timezone)
|
||||
*/
|
||||
formatDateForAPI(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`; // YYYY-MM-DD
|
||||
return formatDateForAPI(date);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,9 +107,9 @@ export class DailyClient {
|
||||
private transformCheckboxDates(checkbox: ApiCheckbox): DailyCheckbox {
|
||||
return {
|
||||
...checkbox,
|
||||
date: new Date(checkbox.date),
|
||||
createdAt: new Date(checkbox.createdAt),
|
||||
updatedAt: new Date(checkbox.updatedAt)
|
||||
date: parseDate(checkbox.date),
|
||||
createdAt: parseDate(checkbox.createdAt),
|
||||
updatedAt: parseDate(checkbox.updatedAt)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,7 +118,7 @@ export class DailyClient {
|
||||
*/
|
||||
private transformDailyViewDates(view: ApiDailyView): DailyView {
|
||||
return {
|
||||
date: new Date(view.date),
|
||||
date: parseDate(view.date),
|
||||
yesterday: view.yesterday.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)),
|
||||
today: view.today.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb))
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { parseDate, formatDateShort } from '@/lib/date-utils';
|
||||
|
||||
interface CompletionTrendData {
|
||||
date: string;
|
||||
@@ -18,11 +19,11 @@ interface CompletionTrendChartProps {
|
||||
export function CompletionTrendChart({ data, title = "Tendance de Completion" }: CompletionTrendChartProps) {
|
||||
// Formatter pour les dates
|
||||
const formatDate = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
return date.toLocaleDateString('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'short'
|
||||
});
|
||||
try {
|
||||
return formatDateShort(parseDate(dateStr));
|
||||
} catch {
|
||||
return dateStr;
|
||||
}
|
||||
};
|
||||
|
||||
// Tooltip personnalisé
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { formatDateForAPI, createDate, getToday } from '@/lib/date-utils';
|
||||
|
||||
interface DailyCalendarProps {
|
||||
currentDate: Date;
|
||||
@@ -15,33 +16,30 @@ export function DailyCalendar({
|
||||
onDateSelect,
|
||||
dailyDates,
|
||||
}: DailyCalendarProps) {
|
||||
const [viewDate, setViewDate] = useState(new Date(currentDate));
|
||||
const [viewDate, setViewDate] = useState(createDate(currentDate));
|
||||
|
||||
// Formatage des dates pour comparaison (éviter le décalage timezone)
|
||||
const formatDateKey = (date: Date) => {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
return formatDateForAPI(date);
|
||||
};
|
||||
|
||||
const currentDateKey = formatDateKey(currentDate);
|
||||
|
||||
// Navigation mois
|
||||
const goToPreviousMonth = () => {
|
||||
const newDate = new Date(viewDate);
|
||||
const newDate = createDate(viewDate);
|
||||
newDate.setMonth(newDate.getMonth() - 1);
|
||||
setViewDate(newDate);
|
||||
};
|
||||
|
||||
const goToNextMonth = () => {
|
||||
const newDate = new Date(viewDate);
|
||||
const newDate = createDate(viewDate);
|
||||
newDate.setMonth(newDate.getMonth() + 1);
|
||||
setViewDate(newDate);
|
||||
};
|
||||
|
||||
const goToToday = () => {
|
||||
const today = new Date();
|
||||
const today = getToday();
|
||||
setViewDate(today);
|
||||
onDateSelect(today);
|
||||
};
|
||||
@@ -57,18 +55,18 @@ export function DailyCalendar({
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
|
||||
// Premier lundi de la semaine contenant le premier jour
|
||||
const startDate = new Date(firstDay);
|
||||
const startDate = createDate(firstDay);
|
||||
const dayOfWeek = firstDay.getDay();
|
||||
const daysToSubtract = dayOfWeek === 0 ? 6 : dayOfWeek - 1; // Lundi = 0
|
||||
startDate.setDate(firstDay.getDate() - daysToSubtract);
|
||||
|
||||
// Générer toutes les dates du calendrier (6 semaines)
|
||||
const days = [];
|
||||
const currentDay = new Date(startDate);
|
||||
const currentDay = createDate(startDate);
|
||||
|
||||
for (let i = 0; i < 42; i++) {
|
||||
// 6 semaines × 7 jours
|
||||
days.push(new Date(currentDay));
|
||||
days.push(createDate(currentDay));
|
||||
currentDay.setDate(currentDay.getDate() + 1);
|
||||
}
|
||||
|
||||
@@ -81,8 +79,8 @@ export function DailyCalendar({
|
||||
onDateSelect(date);
|
||||
};
|
||||
|
||||
const isToday = (date: Date) => {
|
||||
const today = new Date();
|
||||
const isTodayDate = (date: Date) => {
|
||||
const today = getToday();
|
||||
return formatDateKey(date) === formatDateKey(today);
|
||||
};
|
||||
|
||||
@@ -157,7 +155,7 @@ export function DailyCalendar({
|
||||
<div className="grid grid-cols-7 gap-1">
|
||||
{days.map((date, index) => {
|
||||
const isCurrentMonthDay = isCurrentMonth(date);
|
||||
const isTodayDay = isToday(date);
|
||||
const isTodayDay = isTodayDate(date);
|
||||
const hasCheckboxes = hasDaily(date);
|
||||
const isSelectedDay = isSelected(date);
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useWeeklyMetrics, useVelocityTrends } from '@/hooks/use-metrics';
|
||||
import { getToday } from '@/lib/date-utils';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { DailyStatusChart } from './charts/DailyStatusChart';
|
||||
@@ -19,7 +20,7 @@ interface MetricsTabProps {
|
||||
}
|
||||
|
||||
export function MetricsTab({ className }: MetricsTabProps) {
|
||||
const [selectedDate] = useState<Date>(new Date());
|
||||
const [selectedDate] = useState<Date>(getToday());
|
||||
const [weeksBack, setWeeksBack] = useState(4);
|
||||
|
||||
const { metrics, loading: metricsLoading, error: metricsError, refetch: refetchMetrics } = useWeeklyMetrics(selectedDate);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend } from 'recharts';
|
||||
import { DailyMetrics } from '@/services/metrics';
|
||||
import { parseDate, formatDateShort } from '@/lib/date-utils';
|
||||
|
||||
interface DailyStatusChartProps {
|
||||
data: DailyMetrics[];
|
||||
@@ -12,7 +13,7 @@ export function DailyStatusChart({ data, className }: DailyStatusChartProps) {
|
||||
// Transformer les données pour le graphique
|
||||
const chartData = data.map(day => ({
|
||||
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)),
|
||||
'Complétées': day.completed,
|
||||
'En cours': day.inProgress,
|
||||
'Bloquées': day.blocked,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { DailyMetrics } from '@/services/metrics';
|
||||
import { parseDate, isToday } from '@/lib/date-utils';
|
||||
|
||||
interface WeeklyActivityHeatmapProps {
|
||||
data: DailyMetrics[];
|
||||
@@ -67,7 +68,7 @@ export function WeeklyActivityHeatmap({ data, className }: WeeklyActivityHeatmap
|
||||
</div>
|
||||
|
||||
{/* Indicator si jour actuel */}
|
||||
{new Date(day.date).toDateString() === new Date().toDateString() && (
|
||||
{isToday(parseDate(day.date)) && (
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState, useEffect, useCallback, useTransition } from 'react';
|
||||
import { DailyCheckbox } from '@/lib/types';
|
||||
import { tasksClient } from '@/clients/tasks-client';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { formatDateSmart } from '@/lib/date-utils';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { addTodoToTask, toggleCheckbox } from '@/actions/daily';
|
||||
|
||||
@@ -82,11 +83,7 @@ export function RelatedTodos({ taskId }: RelatedTodosProps) {
|
||||
if (isNaN(dateObj.getTime())) {
|
||||
return 'Date invalide';
|
||||
}
|
||||
return new Intl.DateTimeFormat('fr-FR', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
}).format(dateObj);
|
||||
return formatDateSmart(dateObj);
|
||||
} catch (error) {
|
||||
console.error('Erreur formatage date:', error, date);
|
||||
return 'Date invalide';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { formatDateForDisplay, getToday } from '@/lib/date-utils';
|
||||
|
||||
interface AnomalyDetectionPanelProps {
|
||||
className?: string;
|
||||
@@ -42,7 +43,7 @@ export default function AnomalyDetectionPanel({ className = '' }: AnomalyDetecti
|
||||
|
||||
if (result.success && result.data) {
|
||||
setAnomalies(result.data);
|
||||
setLastUpdate(new Date().toLocaleString('fr-FR'));
|
||||
setLastUpdate(formatDateForDisplay(getToday(), 'DISPLAY_LONG'));
|
||||
} else {
|
||||
setError(result.error || 'Erreur lors de la détection');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||
import { Input } from '@/components/ui/Input';
|
||||
import { Modal } from '@/components/ui/Modal';
|
||||
import { Header } from '@/components/ui/Header';
|
||||
import { formatDateForDisplay } from '@/lib/date-utils';
|
||||
import Link from 'next/link';
|
||||
|
||||
interface BackupSettingsPageClientProps {
|
||||
@@ -193,16 +194,8 @@ export default function BackupSettingsPageClient({ initialData }: BackupSettings
|
||||
|
||||
const formatDate = (date: string | Date): string => {
|
||||
// Format cohérent serveur/client pour éviter les erreurs d'hydratation
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString('fr-FR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
const d = typeof date === 'string' ? new Date(date) : date;
|
||||
return formatDateForDisplay(d, 'DISPLAY_MEDIUM');
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { useState, useEffect, useCallback, useTransition } from 'react';
|
||||
import { dailyClient, DailyHistoryFilters, DailySearchFilters, ReorderCheckboxesData } from '@/clients/daily-client';
|
||||
import { DailyView, DailyCheckbox, UpdateDailyCheckboxData, DailyCheckboxType } from '@/lib/types';
|
||||
import { addDays, subtractDays, getToday } from '@/lib/date-utils';
|
||||
import {
|
||||
toggleCheckbox as toggleCheckboxAction,
|
||||
addTodayCheckbox as addTodayCheckboxAction,
|
||||
@@ -341,19 +342,17 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD
|
||||
}, []);
|
||||
|
||||
const goToPreviousDay = useCallback(async (): Promise<void> => {
|
||||
const previousDay = new Date(currentDate);
|
||||
previousDay.setDate(previousDay.getDate() - 1);
|
||||
const previousDay = subtractDays(currentDate, 1);
|
||||
setCurrentDate(previousDay);
|
||||
}, [currentDate]);
|
||||
|
||||
const goToNextDay = useCallback(async (): Promise<void> => {
|
||||
const nextDay = new Date(currentDate);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
const nextDay = addDays(currentDate, 1);
|
||||
setCurrentDate(nextDay);
|
||||
}, [currentDate]);
|
||||
|
||||
const goToToday = useCallback(async (): Promise<void> => {
|
||||
setCurrentDate(new Date());
|
||||
setCurrentDate(getToday());
|
||||
}, []);
|
||||
|
||||
const setDate = useCallback(async (date: Date): Promise<void> => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import { createHash } from 'crypto';
|
||||
import { formatDateForDisplay, getToday } from './date-utils';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -184,7 +185,7 @@ export class BackupUtils {
|
||||
extra?: { hash?: string; size?: number; previousHash?: string }
|
||||
): Promise<void> {
|
||||
try {
|
||||
const date = new Date().toLocaleString('fr-FR');
|
||||
const date = formatDateForDisplay(getToday(), 'DISPLAY_LONG');
|
||||
|
||||
let logEntry = `[${date}] ${type.toUpperCase()} BACKUP ${action.toUpperCase()}: ${details}`;
|
||||
|
||||
|
||||
206
src/lib/date-utils.ts
Normal file
206
src/lib/date-utils.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Utilitaires centralisés pour la gestion des dates
|
||||
* Regroupe toutes les fonctions de formatage, manipulation et validation de dates
|
||||
*/
|
||||
|
||||
import { format, startOfDay, endOfDay, isValid } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
|
||||
// Re-export des utilitaires workday existants
|
||||
export { getPreviousWorkday, getNextWorkday, isWorkday, getDayName } from './workday-utils';
|
||||
|
||||
/**
|
||||
* Formats de dates standardisés
|
||||
*/
|
||||
export const DATE_FORMATS = {
|
||||
API: 'yyyy-MM-dd', // Format API (YYYY-MM-DD)
|
||||
DISPLAY_SHORT: 'dd/MM/yy', // Format court (01/12/25)
|
||||
DISPLAY_LONG: 'EEEE d MMMM yyyy', // Format long (lundi 1 décembre 2025)
|
||||
DISPLAY_MEDIUM: 'dd/MM/yyyy', // Format moyen (01/12/2025)
|
||||
ISO: "yyyy-MM-dd'T'HH:mm:ss.SSSxxx" // Format ISO complet
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Normalise une date au début de la journée (00:00:00.000)
|
||||
*/
|
||||
export function normalizeDate(date: Date): Date {
|
||||
const normalized = new Date(date);
|
||||
normalized.setHours(0, 0, 0, 0);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date pour l'API (évite les décalages timezone)
|
||||
* @param date - Date à formater
|
||||
* @returns Format YYYY-MM-DD
|
||||
*/
|
||||
export function formatDateForAPI(date: Date): string {
|
||||
if (!isValid(date)) {
|
||||
throw new Error('Date invalide fournie à formatDateForAPI');
|
||||
}
|
||||
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date pour l'affichage en français
|
||||
*/
|
||||
export function formatDateForDisplay(date: Date, formatType: keyof typeof DATE_FORMATS = 'DISPLAY_MEDIUM'): string {
|
||||
if (!isValid(date)) {
|
||||
throw new Error('Date invalide fournie à formatDateForDisplay');
|
||||
}
|
||||
|
||||
return format(date, DATE_FORMATS[formatType], { locale: fr });
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date courte pour l'affichage (dd/MM/yy)
|
||||
*/
|
||||
export function formatDateShort(date: Date): string {
|
||||
return formatDateForDisplay(date, 'DISPLAY_SHORT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date longue pour l'affichage (lundi 1 décembre 2025)
|
||||
*/
|
||||
export function formatDateLong(date: Date): string {
|
||||
return formatDateForDisplay(date, 'DISPLAY_LONG');
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une date est aujourd'hui
|
||||
*/
|
||||
export function isToday(date: Date): boolean {
|
||||
const today = new Date();
|
||||
return normalizeDate(date).getTime() === normalizeDate(today).getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si une date est hier
|
||||
*/
|
||||
export function isYesterday(date: Date): boolean {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return normalizeDate(date).getTime() === normalizeDate(yesterday).getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare deux dates (sans tenir compte de l'heure)
|
||||
*/
|
||||
export function isSameDay(date1: Date, date2: Date): boolean {
|
||||
return normalizeDate(date1).getTime() === normalizeDate(date2).getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient la date d'aujourd'hui normalisée
|
||||
*/
|
||||
export function getToday(): Date {
|
||||
return normalizeDate(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient la date d'hier normalisée
|
||||
*/
|
||||
export function getYesterday(): Date {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
return normalizeDate(yesterday);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crée une nouvelle date à partir d'une date existante
|
||||
*/
|
||||
export function createDate(date: Date): Date {
|
||||
return new Date(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ajoute des jours à une date
|
||||
*/
|
||||
export function addDays(date: Date, days: number): Date {
|
||||
const result = createDate(date);
|
||||
result.setDate(result.getDate() + days);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Soustrait des jours à une date
|
||||
*/
|
||||
export function subtractDays(date: Date, days: number): Date {
|
||||
return addDays(date, -days);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse une date depuis une string avec validation
|
||||
*/
|
||||
export function parseDate(dateString: string): Date {
|
||||
const parsed = new Date(dateString);
|
||||
if (!isValid(parsed)) {
|
||||
throw new Error(`Date invalide: ${dateString}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Valide qu'une string est une date valide au format API (YYYY-MM-DD)
|
||||
*/
|
||||
export function isValidAPIDate(dateString: string): boolean {
|
||||
const regex = /^\d{4}-\d{2}-\d{2}$/;
|
||||
if (!regex.test(dateString)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const date = parseDate(dateString);
|
||||
return formatDateForAPI(date) === dateString;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient le début de la journée pour une date
|
||||
*/
|
||||
export function getStartOfDay(date: Date): Date {
|
||||
return startOfDay(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtient la fin de la journée pour une date
|
||||
*/
|
||||
export function getEndOfDay(date: Date): Date {
|
||||
return endOfDay(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formate une date pour l'affichage avec gestion des cas spéciaux (aujourd'hui, hier)
|
||||
*/
|
||||
export function formatDateSmart(date: Date): string {
|
||||
if (isToday(date)) {
|
||||
return "Aujourd'hui";
|
||||
}
|
||||
|
||||
if (isYesterday(date)) {
|
||||
return "Hier";
|
||||
}
|
||||
|
||||
return formatDateForDisplay(date, 'DISPLAY_MEDIUM');
|
||||
}
|
||||
|
||||
/**
|
||||
* Génère un titre intelligent pour une date (avec emojis)
|
||||
*/
|
||||
export function generateDateTitle(date: Date, emoji: string = '📅'): string {
|
||||
if (isToday(date)) {
|
||||
return `${emoji} Aujourd'hui`;
|
||||
}
|
||||
|
||||
if (isYesterday(date)) {
|
||||
return `${emoji} Hier`;
|
||||
}
|
||||
|
||||
return `${emoji} ${formatDateShort(date)}`;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { prisma } from './database';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType, TaskStatus, TaskPriority, TaskSource } from '@/lib/types';
|
||||
import { getPreviousWorkday } from '@/lib/workday-utils';
|
||||
import { getPreviousWorkday, normalizeDate, formatDateForAPI } from '@/lib/date-utils';
|
||||
|
||||
/**
|
||||
* Service pour la gestion des checkboxes daily
|
||||
@@ -13,8 +13,7 @@ export class DailyService {
|
||||
*/
|
||||
async getDailyView(date: Date): Promise<DailyView> {
|
||||
// Normaliser la date (début de journée)
|
||||
const today = new Date(date);
|
||||
today.setHours(0, 0, 0, 0);
|
||||
const today = normalizeDate(date);
|
||||
|
||||
// Utiliser la logique de jour de travail précédent au lieu de jour-1
|
||||
const yesterday = getPreviousWorkday(today);
|
||||
@@ -37,8 +36,7 @@ export class DailyService {
|
||||
*/
|
||||
async getCheckboxesByDate(date: Date): Promise<DailyCheckbox[]> {
|
||||
// Normaliser la date (début de journée)
|
||||
const normalizedDate = new Date(date);
|
||||
normalizedDate.setHours(0, 0, 0, 0);
|
||||
const normalizedDate = normalizeDate(date);
|
||||
|
||||
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||
where: { date: normalizedDate },
|
||||
@@ -54,8 +52,7 @@ export class DailyService {
|
||||
*/
|
||||
async addCheckbox(data: CreateDailyCheckboxData): Promise<DailyCheckbox> {
|
||||
// Normaliser la date
|
||||
const normalizedDate = new Date(data.date);
|
||||
normalizedDate.setHours(0, 0, 0, 0);
|
||||
const normalizedDate = normalizeDate(data.date);
|
||||
|
||||
// Calculer l'ordre suivant pour cette date
|
||||
const maxOrder = await prisma.dailyCheckbox.aggregate({
|
||||
@@ -128,10 +125,6 @@ export class DailyService {
|
||||
* Réordonne les checkboxes d'une date donnée
|
||||
*/
|
||||
async reorderCheckboxes(date: Date, checkboxIds: string[]): Promise<void> {
|
||||
// Normaliser la date
|
||||
const normalizedDate = new Date(date);
|
||||
normalizedDate.setHours(0, 0, 0, 0);
|
||||
|
||||
await prisma.$transaction(async (prisma) => {
|
||||
for (let i = 0; i < checkboxIds.length; i++) {
|
||||
await prisma.dailyCheckbox.update({
|
||||
@@ -264,11 +257,7 @@ export class DailyService {
|
||||
});
|
||||
|
||||
return checkboxes.map(checkbox => {
|
||||
const date = checkbox.date;
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
return formatDateForAPI(checkbox.date);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { JiraTask } from '@/lib/types';
|
||||
import { prisma } from './database';
|
||||
import { parseDate } from '@/lib/date-utils';
|
||||
|
||||
export interface JiraConfig {
|
||||
baseUrl: string;
|
||||
@@ -339,12 +340,12 @@ export class JiraService {
|
||||
priority: this.mapJiraPriorityToInternal(jiraTask.priority?.name),
|
||||
source: 'jira' as const,
|
||||
sourceId: jiraTask.id,
|
||||
dueDate: jiraTask.duedate ? new Date(jiraTask.duedate) : null,
|
||||
dueDate: jiraTask.duedate ? parseDate(jiraTask.duedate) : null,
|
||||
jiraProject: jiraTask.project.key,
|
||||
jiraKey: jiraTask.key,
|
||||
jiraType: this.mapJiraTypeToDisplay(jiraTask.issuetype.name),
|
||||
assignee: jiraTask.assignee?.displayName || null,
|
||||
updatedAt: new Date(jiraTask.updated)
|
||||
updatedAt: parseDate(jiraTask.updated)
|
||||
};
|
||||
|
||||
if (!existingTask) {
|
||||
@@ -352,7 +353,7 @@ export class JiraService {
|
||||
const newTask = await prisma.task.create({
|
||||
data: {
|
||||
...taskData,
|
||||
createdAt: new Date(jiraTask.created)
|
||||
createdAt: parseDate(jiraTask.created)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { prisma } from './database';
|
||||
import { startOfWeek, endOfWeek, eachDayOfInterval, format, startOfDay, endOfDay } from 'date-fns';
|
||||
import { fr } from 'date-fns/locale';
|
||||
import { formatDateForAPI, getDayName, getToday } from '@/lib/date-utils';
|
||||
|
||||
export interface DailyMetrics {
|
||||
date: string; // Format ISO
|
||||
@@ -58,7 +59,7 @@ export class MetricsService {
|
||||
/**
|
||||
* Récupère les métriques journalières de la semaine
|
||||
*/
|
||||
static async getWeeklyMetrics(date: Date = new Date()): Promise<WeeklyMetricsOverview> {
|
||||
static async getWeeklyMetrics(date: Date = getToday()): Promise<WeeklyMetricsOverview> {
|
||||
const weekStart = startOfWeek(date, { weekStartsOn: 1 }); // Lundi
|
||||
const weekEnd = endOfWeek(date, { weekStartsOn: 1 }); // Dimanche
|
||||
|
||||
@@ -163,8 +164,8 @@ export class MetricsService {
|
||||
const completionRate = totalTasks > 0 ? (completed / totalTasks) * 100 : 0;
|
||||
|
||||
return {
|
||||
date: date.toISOString(),
|
||||
dayName: format(date, 'EEEE', { locale: fr }),
|
||||
date: formatDateForAPI(date),
|
||||
dayName: getDayName(date),
|
||||
completed,
|
||||
inProgress,
|
||||
blocked,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { prisma } from './database';
|
||||
import { Task, TaskStatus, TaskPriority, TaskSource, BusinessError, DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { getToday } from '@/lib/date-utils';
|
||||
|
||||
/**
|
||||
* Service pour la gestion des tâches (version standalone)
|
||||
@@ -126,12 +127,12 @@ export class TasksService {
|
||||
status: updates.status,
|
||||
priority: updates.priority,
|
||||
dueDate: updates.dueDate,
|
||||
updatedAt: new Date()
|
||||
updatedAt: getToday()
|
||||
};
|
||||
|
||||
|
||||
if (updates.status === 'done' && !task.completedAt) {
|
||||
updateData.completedAt = new Date();
|
||||
updateData.completedAt = getToday();
|
||||
} else if (updates.status && updates.status !== 'done' && task.completedAt) {
|
||||
updateData.completedAt = null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user