- Added DnD functionality to `DailySection` for reordering checkboxes using `@dnd-kit/core` and `@dnd-kit/sortable`. - Introduced `onReorderCheckboxes` prop to handle server updates after reordering. - Updated `useDaily` hook to streamline error handling during reordering. - Cleaned up `Header` component by removing unnecessary syncing text. - Adjusted `DailyPageClient` to pass reorder function to `DailySection`.
468 lines
16 KiB
TypeScript
468 lines
16 KiB
TypeScript
'use client';
|
|
|
|
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 {
|
|
toggleCheckbox as toggleCheckboxAction,
|
|
addTodayCheckbox as addTodayCheckboxAction,
|
|
addYesterdayCheckbox as addYesterdayCheckboxAction,
|
|
updateCheckbox as updateCheckboxAction,
|
|
deleteCheckbox as deleteCheckboxAction,
|
|
reorderCheckboxes as reorderCheckboxesAction
|
|
} from '@/actions/daily';
|
|
|
|
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
|
|
isPending: boolean; // Pour indiquer les server actions 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 [isPending, startTransition] = useTransition();
|
|
|
|
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((text: string, type?: DailyCheckboxType, taskId?: string): Promise<DailyCheckbox | null> => {
|
|
if (!dailyView) return Promise.resolve(null);
|
|
|
|
return new Promise((resolve) => {
|
|
startTransition(async () => {
|
|
try {
|
|
setError(null);
|
|
|
|
const result = await addTodayCheckboxAction(text, taskId);
|
|
|
|
if (result.success && result.data) {
|
|
// Mise à jour optimiste
|
|
setDailyView(prev => prev ? {
|
|
...prev,
|
|
today: [...prev.today, result.data!].sort((a, b) => a.order - b.order)
|
|
} : null);
|
|
resolve(result.data);
|
|
} else {
|
|
setError(result.error || 'Erreur lors de l\'ajout de la checkbox');
|
|
resolve(null);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur lors de l\'ajout de la checkbox');
|
|
console.error('Erreur addTodayCheckbox:', err);
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
}, [dailyView]);
|
|
|
|
const addYesterdayCheckbox = useCallback((text: string, type?: DailyCheckboxType, taskId?: string): Promise<DailyCheckbox | null> => {
|
|
if (!dailyView) return Promise.resolve(null);
|
|
|
|
return new Promise((resolve) => {
|
|
startTransition(async () => {
|
|
try {
|
|
setError(null);
|
|
|
|
const result = await addYesterdayCheckboxAction(text, taskId);
|
|
|
|
if (result.success && result.data) {
|
|
// Mise à jour optimiste
|
|
setDailyView(prev => prev ? {
|
|
...prev,
|
|
yesterday: [...prev.yesterday, result.data!].sort((a, b) => a.order - b.order)
|
|
} : null);
|
|
resolve(result.data);
|
|
} else {
|
|
setError(result.error || 'Erreur lors de l\'ajout de la checkbox');
|
|
resolve(null);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur lors de l\'ajout de la checkbox');
|
|
console.error('Erreur addYesterdayCheckbox:', err);
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
}, [dailyView]);
|
|
|
|
const updateCheckbox = useCallback((checkboxId: string, data: UpdateDailyCheckboxData): Promise<DailyCheckbox | null> => {
|
|
if (!dailyView) return Promise.resolve(null);
|
|
|
|
return new Promise((resolve) => {
|
|
startTransition(async () => {
|
|
try {
|
|
setError(null);
|
|
|
|
const result = await updateCheckboxAction(checkboxId, data);
|
|
|
|
if (result.success && result.data) {
|
|
// Mise à jour optimiste
|
|
setDailyView(prev => prev ? {
|
|
...prev,
|
|
yesterday: prev.yesterday.map(cb => cb.id === checkboxId ? result.data! : cb),
|
|
today: prev.today.map(cb => cb.id === checkboxId ? result.data! : cb)
|
|
} : null);
|
|
resolve(result.data);
|
|
} else {
|
|
setError(result.error || 'Erreur lors de la mise à jour de la checkbox');
|
|
resolve(null);
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox');
|
|
console.error('Erreur updateCheckbox:', err);
|
|
resolve(null);
|
|
}
|
|
});
|
|
});
|
|
}, [dailyView]);
|
|
|
|
const deleteCheckbox = useCallback((checkboxId: string): Promise<void> => {
|
|
if (!dailyView) return Promise.resolve();
|
|
|
|
return new Promise((resolve) => {
|
|
startTransition(async () => {
|
|
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 {
|
|
const result = await deleteCheckboxAction(checkboxId);
|
|
|
|
if (!result.success) {
|
|
// Rollback en cas d'erreur
|
|
setDailyView(previousDailyView);
|
|
setError(result.error || 'Erreur lors de la suppression de la checkbox');
|
|
}
|
|
resolve();
|
|
} catch (err) {
|
|
// Rollback en cas d'erreur
|
|
setDailyView(previousDailyView);
|
|
setError(err instanceof Error ? err.message : 'Erreur lors de la suppression de la checkbox');
|
|
console.error('Erreur deleteCheckbox:', err);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}, [dailyView]);
|
|
|
|
const toggleCheckbox = useCallback((checkboxId: string): Promise<void> => {
|
|
if (!dailyView) return Promise.resolve();
|
|
|
|
return new Promise((resolve) => {
|
|
startTransition(async () => {
|
|
// 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) {
|
|
resolve();
|
|
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 {
|
|
const result = await toggleCheckboxAction(checkboxId);
|
|
|
|
if (!result.success) {
|
|
// Rollback en cas d'erreur
|
|
setDailyView(previousDailyView);
|
|
setError(result.error || 'Erreur lors de la mise à jour de la checkbox');
|
|
}
|
|
resolve();
|
|
} 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);
|
|
resolve();
|
|
}
|
|
});
|
|
});
|
|
}, [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 =>
|
|
updateCheckboxAction(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 =>
|
|
updateCheckboxAction(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);
|
|
|
|
// Convertir la date en string format YYYY-MM-DD pour la server action
|
|
const dailyId = data.date.toISOString().split('T')[0];
|
|
const result = await reorderCheckboxesAction(dailyId, data.checkboxIds);
|
|
|
|
if (!result.success) {
|
|
setError(result.error || 'Erreur lors du réordonnancement');
|
|
}
|
|
} catch (err) {
|
|
setError(err instanceof Error ? err.message : 'Erreur lors du réordonnancement');
|
|
console.error('Erreur reorderCheckboxes:', err);
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
}, []);
|
|
|
|
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,
|
|
isPending,
|
|
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
|
|
};
|
|
} |