- Implemented `toggleAllToday` and `toggleAllYesterday` methods in `useDaily` for batch checkbox state management. - Updated `DailySection` to include a button for toggling all checkboxes, enhancing user interaction. - Integrated new toggle functions in `DailyPageClient` to support the updated checkbox handling.
490 lines
16 KiB
TypeScript
490 lines
16 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { dailyClient, DailyHistoryFilters, DailySearchFilters, ReorderCheckboxesData } from '@/clients/daily-client';
|
|
import { DailyView, DailyCheckbox, UpdateDailyCheckboxData, DailyCheckboxType } from '@/lib/types';
|
|
|
|
interface UseDailyState {
|
|
dailyView: DailyView | null;
|
|
loading: boolean;
|
|
refreshing: boolean; // Pour les refresh silencieux
|
|
error: string | null;
|
|
saving: boolean; // Pour indiquer les opérations en cours
|
|
}
|
|
|
|
interface UseDailyActions {
|
|
refreshDaily: () => Promise<void>;
|
|
refreshDailySilent: () => Promise<void>;
|
|
addTodayCheckbox: (text: string, type?: DailyCheckboxType, taskId?: string) => Promise<DailyCheckbox | null>;
|
|
addYesterdayCheckbox: (text: string, type?: DailyCheckboxType, taskId?: string) => Promise<DailyCheckbox | null>;
|
|
updateCheckbox: (checkboxId: string, data: UpdateDailyCheckboxData) => Promise<DailyCheckbox | null>;
|
|
deleteCheckbox: (checkboxId: string) => Promise<void>;
|
|
toggleCheckbox: (checkboxId: string) => Promise<void>;
|
|
toggleAllToday: () => Promise<void>;
|
|
toggleAllYesterday: () => Promise<void>;
|
|
reorderCheckboxes: (data: ReorderCheckboxesData) => Promise<void>;
|
|
goToPreviousDay: () => Promise<void>;
|
|
goToNextDay: () => Promise<void>;
|
|
goToToday: () => Promise<void>;
|
|
setDate: (date: Date) => Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Hook pour la gestion d'une vue daily spécifique
|
|
*/
|
|
export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseDailyState & UseDailyActions & { currentDate: Date } {
|
|
const [currentDate, setCurrentDate] = useState<Date>(initialDate || new Date());
|
|
const [dailyView, setDailyView] = useState<DailyView | null>(initialDailyView || null);
|
|
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 [error, setError] = useState<string | null>(null);
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
const refreshDaily = useCallback(async () => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const view = await dailyClient.getDailyView(currentDate);
|
|
setDailyView(view);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur lors du chargement du daily');
|
|
console.error('Erreur refreshDaily:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [currentDate]);
|
|
|
|
const refreshDailySilent = useCallback(async () => {
|
|
try {
|
|
setRefreshing(true);
|
|
// Refresh silencieux sans setLoading(true) pour éviter le clignotement
|
|
const view = await dailyClient.getDailyView(currentDate);
|
|
setDailyView(view);
|
|
} catch (err) {
|
|
console.error('Erreur refreshDailySilent:', err);
|
|
// On n'affiche pas l'erreur pour ne pas perturber l'UX
|
|
} finally {
|
|
setRefreshing(false);
|
|
}
|
|
}, [currentDate]);
|
|
|
|
const addTodayCheckbox = useCallback(async (text: string, type?: DailyCheckboxType, taskId?: string): Promise<DailyCheckbox | null> => {
|
|
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 {
|
|
// Appel API en arrière-plan
|
|
const newCheckbox = await dailyClient.addCheckbox({
|
|
date: currentDate,
|
|
text,
|
|
type: type || 'task',
|
|
taskId
|
|
});
|
|
|
|
// Remplacer la checkbox temporaire par la vraie
|
|
setDailyView(prev => prev ? {
|
|
...prev,
|
|
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;
|
|
}
|
|
}, [dailyView, currentDate]);
|
|
|
|
const addYesterdayCheckbox = useCallback(async (text: string, type?: DailyCheckboxType, taskId?: string): Promise<DailyCheckbox | null> => {
|
|
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 {
|
|
// Appel API en arrière-plan
|
|
const newCheckbox = await dailyClient.addCheckbox({
|
|
date: yesterday,
|
|
text,
|
|
type: type || 'task',
|
|
taskId
|
|
});
|
|
|
|
// Remplacer la checkbox temporaire par la vraie
|
|
setDailyView(prev => prev ? {
|
|
...prev,
|
|
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;
|
|
}
|
|
}, [dailyView, currentDate]);
|
|
|
|
const updateCheckbox = useCallback(async (checkboxId: string, data: UpdateDailyCheckboxData): Promise<DailyCheckbox | null> => {
|
|
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 {
|
|
// Appel API en arrière-plan
|
|
const updatedCheckbox = await dailyClient.updateCheckbox(checkboxId, data);
|
|
|
|
// Remplacer par la vraie checkbox retournée par l'API
|
|
setDailyView(prev => prev ? {
|
|
...prev,
|
|
yesterday: prev.yesterday.map(cb => cb.id === checkboxId ? updatedCheckbox : cb),
|
|
today: prev.today.map(cb => cb.id === checkboxId ? updatedCheckbox : cb)
|
|
} : null);
|
|
|
|
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;
|
|
}
|
|
}, [dailyView]);
|
|
|
|
const deleteCheckbox = useCallback(async (checkboxId: string): Promise<void> => {
|
|
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 {
|
|
// Appel API en arrière-plan
|
|
await dailyClient.deleteCheckbox(checkboxId);
|
|
// 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);
|
|
}
|
|
}, [dailyView]);
|
|
|
|
const toggleCheckbox = useCallback(async (checkboxId: string): Promise<void> => {
|
|
if (!dailyView) return;
|
|
|
|
// Trouver la checkbox dans yesterday ou today
|
|
let checkbox = dailyView.yesterday.find(cb => cb.id === checkboxId);
|
|
if (!checkbox) {
|
|
checkbox = dailyView.today.find(cb => cb.id === checkboxId);
|
|
}
|
|
|
|
if (!checkbox) return;
|
|
|
|
// 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 toggleAllToday = useCallback(async (): Promise<void> => {
|
|
if (!dailyView) return;
|
|
|
|
const todayCheckboxes = dailyView.today;
|
|
if (todayCheckboxes.length === 0) return;
|
|
|
|
// Déterminer si on coche tout ou on décoche tout
|
|
const allChecked = todayCheckboxes.every(cb => cb.isChecked);
|
|
const newCheckedState = !allChecked;
|
|
|
|
const previousDailyView = dailyView;
|
|
|
|
// Mise à jour optimiste IMMÉDIATE
|
|
setDailyView(prev => prev ? {
|
|
...prev,
|
|
today: prev.today.map(cb => ({ ...cb, isChecked: newCheckedState }))
|
|
} : null);
|
|
|
|
try {
|
|
// Appeler l'API pour chaque checkbox en parallèle
|
|
await Promise.all(
|
|
todayCheckboxes.map(checkbox =>
|
|
dailyClient.updateCheckbox(checkbox.id, { isChecked: newCheckedState })
|
|
)
|
|
);
|
|
} catch (err) {
|
|
// Rollback en cas d'erreur
|
|
setDailyView(previousDailyView);
|
|
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour des checkboxes');
|
|
console.error('Erreur toggleAllToday:', err);
|
|
}
|
|
}, [dailyView]);
|
|
|
|
const toggleAllYesterday = useCallback(async (): Promise<void> => {
|
|
if (!dailyView) return;
|
|
|
|
const yesterdayCheckboxes = dailyView.yesterday;
|
|
if (yesterdayCheckboxes.length === 0) return;
|
|
|
|
// Déterminer si on coche tout ou on décoche tout
|
|
const allChecked = yesterdayCheckboxes.every(cb => cb.isChecked);
|
|
const newCheckedState = !allChecked;
|
|
|
|
const previousDailyView = dailyView;
|
|
|
|
// Mise à jour optimiste IMMÉDIATE
|
|
setDailyView(prev => prev ? {
|
|
...prev,
|
|
yesterday: prev.yesterday.map(cb => ({ ...cb, isChecked: newCheckedState }))
|
|
} : null);
|
|
|
|
try {
|
|
// Appeler l'API pour chaque checkbox en parallèle
|
|
await Promise.all(
|
|
yesterdayCheckboxes.map(checkbox =>
|
|
dailyClient.updateCheckbox(checkbox.id, { isChecked: newCheckedState })
|
|
)
|
|
);
|
|
} catch (err) {
|
|
// Rollback en cas d'erreur
|
|
setDailyView(previousDailyView);
|
|
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour des checkboxes');
|
|
console.error('Erreur toggleAllYesterday:', err);
|
|
}
|
|
}, [dailyView]);
|
|
|
|
const reorderCheckboxes = useCallback(async (data: ReorderCheckboxesData): Promise<void> => {
|
|
try {
|
|
setSaving(true);
|
|
setError(null);
|
|
|
|
await dailyClient.reorderCheckboxes(data);
|
|
|
|
// Rafraîchir pour obtenir l'ordre correct
|
|
await refreshDaily();
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur lors du réordonnancement');
|
|
console.error('Erreur reorderCheckboxes:', err);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, [refreshDaily]);
|
|
|
|
const goToPreviousDay = useCallback(async (): Promise<void> => {
|
|
const previousDay = new Date(currentDate);
|
|
previousDay.setDate(previousDay.getDate() - 1);
|
|
setCurrentDate(previousDay);
|
|
}, [currentDate]);
|
|
|
|
const goToNextDay = useCallback(async (): Promise<void> => {
|
|
const nextDay = new Date(currentDate);
|
|
nextDay.setDate(nextDay.getDate() + 1);
|
|
setCurrentDate(nextDay);
|
|
}, [currentDate]);
|
|
|
|
const goToToday = useCallback(async (): Promise<void> => {
|
|
setCurrentDate(new Date());
|
|
}, []);
|
|
|
|
const setDate = useCallback(async (date: Date): Promise<void> => {
|
|
setCurrentDate(date);
|
|
}, []);
|
|
|
|
// État pour savoir si c'est le premier chargement
|
|
const [isInitialLoad, setIsInitialLoad] = useState(!initialDailyView);
|
|
|
|
// Charger le daily quand la date change
|
|
useEffect(() => {
|
|
if (isInitialLoad) {
|
|
// Premier chargement : utiliser refreshDaily normal seulement si pas de données SSR
|
|
if (!initialDailyView) {
|
|
refreshDaily().finally(() => setIsInitialLoad(false));
|
|
} else {
|
|
setIsInitialLoad(false);
|
|
}
|
|
} else {
|
|
// Changements suivants : utiliser refreshDailySilent
|
|
refreshDailySilent();
|
|
}
|
|
}, [refreshDaily, refreshDailySilent, isInitialLoad, initialDailyView]);
|
|
|
|
return {
|
|
// State
|
|
dailyView,
|
|
loading,
|
|
refreshing,
|
|
error,
|
|
saving,
|
|
currentDate,
|
|
|
|
// Actions
|
|
refreshDaily,
|
|
refreshDailySilent,
|
|
addTodayCheckbox,
|
|
addYesterdayCheckbox,
|
|
updateCheckbox,
|
|
deleteCheckbox,
|
|
toggleCheckbox,
|
|
toggleAllToday,
|
|
toggleAllYesterday,
|
|
reorderCheckboxes,
|
|
goToPreviousDay,
|
|
goToNextDay,
|
|
goToToday,
|
|
setDate
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Hook pour l'historique des checkboxes
|
|
*/
|
|
export function useDailyHistory() {
|
|
const [history, setHistory] = useState<{ date: Date; checkboxes: DailyCheckbox[] }[]>([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
const loadHistory = useCallback(async (filters?: DailyHistoryFilters) => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const historyData = await dailyClient.getCheckboxHistory(filters);
|
|
setHistory(historyData);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur lors du chargement de l\'historique');
|
|
console.error('Erreur loadHistory:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const searchCheckboxes = useCallback(async (filters: DailySearchFilters) => {
|
|
try {
|
|
setLoading(true);
|
|
setError(null);
|
|
|
|
const checkboxes = await dailyClient.searchCheckboxes(filters);
|
|
// Grouper par date pour l'affichage
|
|
const groupedHistory = checkboxes.reduce((acc, checkbox) => {
|
|
const dateKey = checkbox.date.toDateString();
|
|
const existing = acc.find(item => item.date.toDateString() === dateKey);
|
|
|
|
if (existing) {
|
|
existing.checkboxes.push(checkbox);
|
|
} else {
|
|
acc.push({ date: checkbox.date, checkboxes: [checkbox] });
|
|
}
|
|
|
|
return acc;
|
|
}, [] as { date: Date; checkboxes: DailyCheckbox[] }[]);
|
|
|
|
setHistory(groupedHistory);
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur lors de la recherche');
|
|
console.error('Erreur searchCheckboxes:', err);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
history,
|
|
loading,
|
|
error,
|
|
loadHistory,
|
|
searchCheckboxes
|
|
};
|
|
} |