From 4b27047e63ede0f34e9fd8e4bd173c738dc0094b Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 15 Sep 2025 22:30:56 +0200 Subject: [PATCH] feat: enhance DailyClient and useDaily hook for improved checkbox handling - Added new API response types (`ApiCheckbox`, `ApiDailyView`, `ApiHistoryItem`) for better type safety. - Updated `getTodaysDailyView`, `getDailyView`, and `getHistory` methods to utilize new types and transform date strings into Date objects. - Refactored `addCheckbox` and `updateCheckbox` methods to handle checkbox creation and updates with improved error handling. - Optimized `DailyAddForm` for better UX by removing unnecessary loading states and implementing optimistic UI updates. - Enhanced `useDaily` hook to support checkbox type management and rollback on errors during updates. - Updated `DailyPageClient` to leverage new checkbox handling methods for adding tasks. --- clients/daily-client.ts | 80 +++++++++++-- components/daily/DailyAddForm.tsx | 38 +++--- hooks/useDaily.ts | 184 +++++++++++++++++++++++------- src/app/daily/DailyPageClient.tsx | 44 ++----- 4 files changed, 239 insertions(+), 107 deletions(-) diff --git a/clients/daily-client.ts b/clients/daily-client.ts index 343cdc5..e9047a2 100644 --- a/clients/daily-client.ts +++ b/clients/daily-client.ts @@ -1,5 +1,30 @@ import { httpClient } from './base/http-client'; -import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData } from '@/lib/types'; +import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, Task } from '@/lib/types'; + +// Types pour les réponses API (avec dates en string) +interface ApiCheckbox { + id: string; + date: string; + text: string; + isChecked: boolean; + type: 'task' | 'meeting'; + order: number; + taskId?: string; + task?: Task; + createdAt: string; + updatedAt: string; +} + +interface ApiDailyView { + date: string; + yesterday: ApiCheckbox[]; + today: ApiCheckbox[]; +} + +interface ApiHistoryItem { + date: string; + checkboxes: ApiCheckbox[]; +} export interface DailyHistoryFilters { limit?: number; @@ -20,7 +45,8 @@ export class DailyClient { * Récupère la vue daily d'aujourd'hui (hier + aujourd'hui) */ async getTodaysDailyView(): Promise { - return httpClient.get('/daily'); + const result = await httpClient.get('/daily'); + return this.transformDailyViewDates(result); } /** @@ -28,7 +54,8 @@ export class DailyClient { */ async getDailyView(date: Date): Promise { const dateStr = this.formatDateForAPI(date); - return httpClient.get(`/daily?date=${dateStr}`); + const result = await httpClient.get(`/daily?date=${dateStr}`); + return this.transformDailyViewDates(result); } /** @@ -39,7 +66,11 @@ export class DailyClient { if (filters?.limit) params.append('limit', filters.limit.toString()); - return httpClient.get(`/daily?${params}`); + const result = await httpClient.get(`/daily?${params}`); + return result.map(item => ({ + date: new Date(item.date), + checkboxes: item.checkboxes.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)) + })); } /** @@ -53,17 +84,26 @@ export class DailyClient { if (filters.limit) params.append('limit', filters.limit.toString()); - return httpClient.get(`/daily?${params}`); + const result = await httpClient.get(`/daily?${params}`); + return result.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)); } /** * Ajoute une checkbox */ async addCheckbox(data: CreateDailyCheckboxData): Promise { - return httpClient.post('/daily', { + const payload = { ...data, date: this.formatDateForAPI(data.date) - }); + }; + try { + const result = await httpClient.post('/daily', payload); + // Transformer les dates string en objets Date + return this.transformCheckboxDates(result); + } catch (error) { + console.error('❌ DailyClient addCheckbox error:', error); + throw error; + } } /** @@ -95,7 +135,8 @@ export class DailyClient { * Met à jour une checkbox */ async updateCheckbox(checkboxId: string, data: UpdateDailyCheckboxData): Promise { - return httpClient.patch(`/daily/checkboxes/${checkboxId}`, data); + const result = await httpClient.patch(`/daily/checkboxes/${checkboxId}`, data); + return this.transformCheckboxDates(result); } /** @@ -132,6 +173,29 @@ export class DailyClient { return `${year}-${month}-${day}`; // YYYY-MM-DD } + /** + * Transforme les dates string d'une checkbox en objets Date + */ + private transformCheckboxDates(checkbox: ApiCheckbox): DailyCheckbox { + return { + ...checkbox, + date: new Date(checkbox.date), + createdAt: new Date(checkbox.createdAt), + updatedAt: new Date(checkbox.updatedAt) + }; + } + + /** + * Transforme les dates string d'une vue daily en objets Date + */ + private transformDailyViewDates(view: ApiDailyView): DailyView { + return { + date: new Date(view.date), + yesterday: view.yesterday.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)), + today: view.today.map((cb: ApiCheckbox) => this.transformCheckboxDates(cb)) + }; + } + /** * Récupère la vue daily d'une date relative (hier, aujourd'hui, demain) */ diff --git a/components/daily/DailyAddForm.tsx b/components/daily/DailyAddForm.tsx index 2b23112..cae96d9 100644 --- a/components/daily/DailyAddForm.tsx +++ b/components/daily/DailyAddForm.tsx @@ -14,25 +14,23 @@ interface DailyAddFormProps { export function DailyAddForm({ onAdd, disabled = false, placeholder = "Ajouter une tâche..." }: DailyAddFormProps) { const [newCheckboxText, setNewCheckboxText] = useState(''); const [selectedType, setSelectedType] = useState('task'); - const [addingCheckbox, setAddingCheckbox] = useState(false); const inputRef = useRef(null); - const handleAddCheckbox = async () => { + const handleAddCheckbox = () => { if (!newCheckboxText.trim()) return; - setAddingCheckbox(true); - try { - await onAdd(newCheckboxText.trim(), selectedType); // Pas de taskId lors de l'ajout - setNewCheckboxText(''); - // Garder le type sélectionné pour enchaîner les créations du même type - // setSelectedType('task'); // <- Supprimé pour garder la sélection - // Garder le focus sur l'input pour enchainer les entrées - setTimeout(() => { - inputRef.current?.focus(); - }, 100); - } finally { - setAddingCheckbox(false); - } + const text = newCheckboxText.trim(); + + // Vider et refocus IMMÉDIATEMENT pour l'UX optimiste + setNewCheckboxText(''); + inputRef.current?.focus(); + + // Lancer l'ajout en arrière-plan (fire and forget) + onAdd(text, selectedType).catch(error => { + console.error('Erreur lors de l\'ajout:', error); + // En cas d'erreur, on pourrait restaurer le texte + // setNewCheckboxText(text); + }); }; const handleKeyPress = (e: React.KeyboardEvent) => { @@ -61,7 +59,7 @@ export function DailyAddForm({ onAdd, disabled = false, placeholder = "Ajouter u ? 'border-l-green-500 bg-green-100 dark:bg-green-900/40 text-green-900 dark:text-green-100 font-medium' : 'border-l-green-300 hover:border-l-green-400 opacity-70 hover:opacity-90' }`} - disabled={addingCheckbox || disabled} + disabled={disabled} > ✅ Tâche @@ -75,7 +73,7 @@ export function DailyAddForm({ onAdd, disabled = false, placeholder = "Ajouter u ? 'border-l-blue-500 bg-blue-100 dark:bg-blue-900/40 text-blue-900 dark:text-blue-100 font-medium' : 'border-l-blue-300 hover:border-l-blue-400 opacity-70 hover:opacity-90' }`} - disabled={addingCheckbox || disabled} + disabled={disabled} > 🗓️ Réunion @@ -90,17 +88,17 @@ export function DailyAddForm({ onAdd, disabled = false, placeholder = "Ajouter u value={newCheckboxText} onChange={(e) => setNewCheckboxText(e.target.value)} onKeyDown={handleKeyPress} - disabled={addingCheckbox || disabled} + disabled={disabled} className="flex-1 min-w-[300px]" /> diff --git a/hooks/useDaily.ts b/hooks/useDaily.ts index ee73cbe..9e46dff 100644 --- a/hooks/useDaily.ts +++ b/hooks/useDaily.ts @@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'; import { dailyClient, DailyHistoryFilters, DailySearchFilters, ReorderCheckboxesData } from '@/clients/daily-client'; -import { DailyView, DailyCheckbox, UpdateDailyCheckboxData } from '@/lib/types'; +import { DailyView, DailyCheckbox, UpdateDailyCheckboxData, DailyCheckboxType } from '@/lib/types'; interface UseDailyState { dailyView: DailyView | null; @@ -15,8 +15,8 @@ interface UseDailyState { interface UseDailyActions { refreshDaily: () => Promise; refreshDailySilent: () => Promise; - addTodayCheckbox: (text: string, taskId?: string) => Promise; - addYesterdayCheckbox: (text: string, taskId?: string) => Promise; + addTodayCheckbox: (text: string, type?: DailyCheckboxType, taskId?: string) => Promise; + addYesterdayCheckbox: (text: string, type?: DailyCheckboxType, taskId?: string) => Promise; updateCheckbox: (checkboxId: string, data: UpdateDailyCheckboxData) => Promise; deleteCheckbox: (checkboxId: string) => Promise; toggleCheckbox: (checkboxId: string) => Promise; @@ -67,66 +67,141 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD } }, [currentDate]); - const addTodayCheckbox = useCallback(async (text: string, taskId?: string): Promise => { + const addTodayCheckbox = useCallback(async (text: string, type?: DailyCheckboxType, taskId?: string): Promise => { if (!dailyView) return null; + // Créer une checkbox temporaire pour l'affichage optimiste + const tempCheckbox: DailyCheckbox = { + id: `temp-${Date.now()}`, // ID temporaire + date: currentDate, + text, + isChecked: false, + type: type || 'task', // Utilise le type fourni ou 'task' par défaut + order: dailyView.today.length, // Ordre temporaire + taskId, + task: undefined, + createdAt: new Date(), + updatedAt: new Date() + }; + + const previousDailyView = dailyView; + + // Mise à jour optimiste IMMÉDIATE + setDailyView(prev => prev ? { + ...prev, + today: [...prev.today, tempCheckbox] + } : null); + try { - setSaving(true); - setError(null); + // Appel API en arrière-plan + const newCheckbox = await dailyClient.addCheckbox({ + date: currentDate, + text, + type: type || 'task', + taskId + }); - const newCheckbox = await dailyClient.addTodayCheckbox(text, taskId); - - // Mise à jour optimiste + // Remplacer la checkbox temporaire par la vraie setDailyView(prev => prev ? { ...prev, - today: [...prev.today, newCheckbox].sort((a, b) => a.order - b.order) + today: prev.today.map(cb => + cb.id === tempCheckbox.id ? newCheckbox : cb + ).sort((a, b) => a.order - b.order) } : null); return newCheckbox; } catch (err) { + // Rollback en cas d'erreur + setDailyView(previousDailyView); setError(err instanceof Error ? err.message : 'Erreur lors de l\'ajout de la checkbox'); console.error('Erreur addTodayCheckbox:', err); return null; - } finally { - setSaving(false); } - }, [dailyView]); + }, [dailyView, currentDate]); - const addYesterdayCheckbox = useCallback(async (text: string, taskId?: string): Promise => { + const addYesterdayCheckbox = useCallback(async (text: string, type?: DailyCheckboxType, taskId?: string): Promise => { if (!dailyView) return null; + // Créer une checkbox temporaire pour l'affichage optimiste + const yesterday = new Date(currentDate); + yesterday.setDate(yesterday.getDate() - 1); + + const tempCheckbox: DailyCheckbox = { + id: `temp-${Date.now()}`, // ID temporaire + date: yesterday, + text, + isChecked: false, + type: type || 'task', // Utilise le type fourni ou 'task' par défaut + order: dailyView.yesterday.length, // Ordre temporaire + taskId, + task: undefined, + createdAt: new Date(), + updatedAt: new Date() + }; + + const previousDailyView = dailyView; + + // Mise à jour optimiste IMMÉDIATE + setDailyView(prev => prev ? { + ...prev, + yesterday: [...prev.yesterday, tempCheckbox] + } : null); + try { - setSaving(true); - setError(null); + // Appel API en arrière-plan + const newCheckbox = await dailyClient.addCheckbox({ + date: yesterday, + text, + type: type || 'task', + taskId + }); - const newCheckbox = await dailyClient.addYesterdayCheckbox(text, taskId); - - // Mise à jour optimiste + // Remplacer la checkbox temporaire par la vraie setDailyView(prev => prev ? { ...prev, - yesterday: [...prev.yesterday, newCheckbox].sort((a, b) => a.order - b.order) + yesterday: prev.yesterday.map(cb => + cb.id === tempCheckbox.id ? newCheckbox : cb + ).sort((a, b) => a.order - b.order) } : null); return newCheckbox; } catch (err) { + // Rollback en cas d'erreur + setDailyView(previousDailyView); setError(err instanceof Error ? err.message : 'Erreur lors de l\'ajout de la checkbox'); console.error('Erreur addYesterdayCheckbox:', err); return null; - } finally { - setSaving(false); } - }, [dailyView]); + }, [dailyView, currentDate]); const updateCheckbox = useCallback(async (checkboxId: string, data: UpdateDailyCheckboxData): Promise => { if (!dailyView) return null; + // Trouver la checkbox existante + let existingCheckbox = dailyView.yesterday.find(cb => cb.id === checkboxId); + if (!existingCheckbox) { + existingCheckbox = dailyView.today.find(cb => cb.id === checkboxId); + } + + if (!existingCheckbox) return null; + + const previousDailyView = dailyView; + + // Créer la checkbox mise à jour pour l'affichage optimiste + const optimisticCheckbox = { ...existingCheckbox, ...data, updatedAt: new Date() }; + + // Mise à jour optimiste IMMÉDIATE + setDailyView(prev => prev ? { + ...prev, + yesterday: prev.yesterday.map(cb => cb.id === checkboxId ? optimisticCheckbox : cb), + today: prev.today.map(cb => cb.id === checkboxId ? optimisticCheckbox : cb) + } : null); + try { - setSaving(true); - setError(null); - + // Appel API en arrière-plan const updatedCheckbox = await dailyClient.updateCheckbox(checkboxId, data); - // Mise à jour optimiste + // Remplacer par la vraie checkbox retournée par l'API setDailyView(prev => prev ? { ...prev, yesterday: prev.yesterday.map(cb => cb.id === checkboxId ? updatedCheckbox : cb), @@ -135,34 +210,35 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD return updatedCheckbox; } catch (err) { + // Rollback en cas d'erreur + setDailyView(previousDailyView); setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox'); console.error('Erreur updateCheckbox:', err); return null; - } finally { - setSaving(false); } }, [dailyView]); const deleteCheckbox = useCallback(async (checkboxId: string): Promise => { if (!dailyView) return; + const previousDailyView = dailyView; + + // Mise à jour optimiste IMMÉDIATE - supprimer la checkbox + setDailyView(prev => prev ? { + ...prev, + yesterday: prev.yesterday.filter(cb => cb.id !== checkboxId), + today: prev.today.filter(cb => cb.id !== checkboxId) + } : null); + try { - setSaving(true); - setError(null); - + // Appel API en arrière-plan await dailyClient.deleteCheckbox(checkboxId); - - // Mise à jour optimiste - setDailyView(prev => prev ? { - ...prev, - yesterday: prev.yesterday.filter(cb => cb.id !== checkboxId), - today: prev.today.filter(cb => cb.id !== checkboxId) - } : null); + // Pas besoin de mise à jour supplémentaire, la suppression est déjà faite } catch (err) { + // Rollback en cas d'erreur - restaurer la checkbox + setDailyView(previousDailyView); setError(err instanceof Error ? err.message : 'Erreur lors de la suppression de la checkbox'); console.error('Erreur deleteCheckbox:', err); - } finally { - setSaving(false); } }, [dailyView]); @@ -177,8 +253,30 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD if (!checkbox) return; - await updateCheckbox(checkboxId, { isChecked: !checkbox.isChecked }); - }, [dailyView, updateCheckbox]); + // Mise à jour optimiste IMMÉDIATE + const newCheckedState = !checkbox.isChecked; + const previousDailyView = dailyView; + + setDailyView(prev => prev ? { + ...prev, + yesterday: prev.yesterday.map(cb => + cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb + ), + today: prev.today.map(cb => + cb.id === checkboxId ? { ...cb, isChecked: newCheckedState } : cb + ) + } : null); + + try { + // Appel API en arrière-plan + await dailyClient.updateCheckbox(checkboxId, { isChecked: newCheckedState }); + } catch (err) { + // Rollback en cas d'erreur + setDailyView(previousDailyView); + setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox'); + console.error('Erreur toggleCheckbox:', err); + } + }, [dailyView]); const reorderCheckboxes = useCallback(async (data: ReorderCheckboxesData): Promise => { try { diff --git a/src/app/daily/DailyPageClient.tsx b/src/app/daily/DailyPageClient.tsx index ddcf094..bf79f9a 100644 --- a/src/app/daily/DailyPageClient.tsx +++ b/src/app/daily/DailyPageClient.tsx @@ -29,7 +29,8 @@ export function DailyPageClient({ error, saving, currentDate, - refreshDailySilent, + addTodayCheckbox, + addYesterdayCheckbox, toggleCheckbox, updateCheckbox, deleteCheckbox, @@ -61,44 +62,15 @@ export function DailyPageClient({ }, [initialDailyDates.length]); const handleAddTodayCheckbox = async (text: string, type: DailyCheckboxType) => { - try { - const { dailyClient } = await import('@/clients/daily-client'); - await dailyClient.addCheckbox({ - date: currentDate, - text, - type, - // Pas de taskId lors de l'ajout - sera ajouté via l'édition - isChecked: false - }); - // Recharger silencieusement la vue daily (sans clignotement) - refreshDailySilent().catch(console.error); - // Recharger aussi les dates pour le calendrier - await refreshDailyDates(); - } catch (error) { - console.error('Erreur lors de l\'ajout de la tâche:', error); - } + await addTodayCheckbox(text, type); + // Recharger aussi les dates pour le calendrier + await refreshDailyDates(); }; const handleAddYesterdayCheckbox = async (text: string, type: DailyCheckboxType) => { - try { - const yesterday = new Date(currentDate); - yesterday.setDate(yesterday.getDate() - 1); - - const { dailyClient } = await import('@/clients/daily-client'); - await dailyClient.addCheckbox({ - date: yesterday, - text, - type, - // Pas de taskId lors de l'ajout - sera ajouté via l'édition - isChecked: false - }); - // Recharger silencieusement la vue daily (sans clignotement) - refreshDailySilent().catch(console.error); - // Recharger aussi les dates pour le calendrier - await refreshDailyDates(); - } catch (error) { - console.error('Erreur lors de l\'ajout de la tâche:', error); - } + await addYesterdayCheckbox(text, type); + // Recharger aussi les dates pour le calendrier + await refreshDailyDates(); }; const handleToggleCheckbox = async (checkboxId: string) => {