Files
towercontrol/hooks/useDaily.ts
Julien Froidefond 11200f85ac feat: add toggle all functionality for daily checkboxes
- 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.
2025-09-16 08:09:13 +02:00

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
};
}