chore: refactor project structure and clean up unused components
- Updated `TODO.md` to reflect new testing tasks and final structure expectations. - Simplified TypeScript path mappings in `tsconfig.json` for better clarity. - Revised business logic separation rules in `.cursor/rules` to align with new directory structure. - Deleted unused client components and services to streamline the codebase. - Adjusted import paths in scripts to match the new structure.
This commit is contained in:
63
src/hooks/use-metrics.ts
Normal file
63
src/hooks/use-metrics.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useState, useEffect, useTransition, useCallback } from 'react';
|
||||
import { getWeeklyMetrics, getVelocityTrends } from '@/actions/metrics';
|
||||
import { WeeklyMetricsOverview, VelocityTrend } from '@/services/metrics';
|
||||
|
||||
export function useWeeklyMetrics(date?: Date) {
|
||||
const [metrics, setMetrics] = useState<WeeklyMetricsOverview | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const fetchMetrics = useCallback(() => {
|
||||
startTransition(async () => {
|
||||
setError(null);
|
||||
const result = await getWeeklyMetrics(date);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setMetrics(result.data);
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch metrics');
|
||||
}
|
||||
});
|
||||
}, [date, startTransition]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchMetrics();
|
||||
}, [date, fetchMetrics]);
|
||||
|
||||
return {
|
||||
metrics,
|
||||
loading: isPending,
|
||||
error,
|
||||
refetch: fetchMetrics
|
||||
};
|
||||
}
|
||||
|
||||
export function useVelocityTrends(weeksBack: number = 4) {
|
||||
const [trends, setTrends] = useState<VelocityTrend[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const fetchTrends = useCallback(() => {
|
||||
startTransition(async () => {
|
||||
setError(null);
|
||||
const result = await getVelocityTrends(weeksBack);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setTrends(result.data);
|
||||
} else {
|
||||
setError(result.error || 'Failed to fetch velocity trends');
|
||||
}
|
||||
});
|
||||
}, [weeksBack, startTransition]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchTrends();
|
||||
}, [weeksBack, fetchTrends]);
|
||||
|
||||
return {
|
||||
trends,
|
||||
loading: isPending,
|
||||
error,
|
||||
refetch: fetchTrends
|
||||
};
|
||||
}
|
||||
468
src/hooks/useDaily.ts
Normal file
468
src/hooks/useDaily.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
'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, type, 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, type, 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
|
||||
};
|
||||
}
|
||||
28
src/hooks/useDragAndDrop.ts
Normal file
28
src/hooks/useDragAndDrop.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useSensors, useSensor, PointerSensor } from '@dnd-kit/core';
|
||||
|
||||
/**
|
||||
* Hook pour gérer le drag & drop de manière safe avec SSR
|
||||
* Désactive le DnD jusqu'à l'hydratation pour éviter les erreurs d'hydratation
|
||||
*/
|
||||
export function useDragAndDrop() {
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Activer le drag & drop après l'hydratation
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: {
|
||||
distance: 8,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
isMounted,
|
||||
sensors
|
||||
};
|
||||
}
|
||||
43
src/hooks/useJiraAnalytics.ts
Normal file
43
src/hooks/useJiraAnalytics.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition, useCallback } from 'react';
|
||||
import { getJiraAnalytics } from '@/actions/jira-analytics';
|
||||
import { JiraAnalytics } from '@/lib/types';
|
||||
|
||||
export function useJiraAnalytics() {
|
||||
const [analytics, setAnalytics] = useState<JiraAnalytics | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const loadAnalytics = useCallback((forceRefresh = false) => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const result = await getJiraAnalytics(forceRefresh);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setAnalytics(result.data);
|
||||
} else {
|
||||
setError(result.error || 'Erreur lors du chargement des analytics');
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Erreur lors du chargement des analytics';
|
||||
setError(errorMessage);
|
||||
console.error('Erreur analytics Jira:', err);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const refreshAnalytics = useCallback(() => {
|
||||
loadAnalytics(true); // Force refresh quand on actualise manuellement
|
||||
}, [loadAnalytics]);
|
||||
|
||||
return {
|
||||
analytics,
|
||||
isLoading: isPending,
|
||||
error,
|
||||
loadAnalytics,
|
||||
refreshAnalytics
|
||||
};
|
||||
}
|
||||
82
src/hooks/useJiraConfig.ts
Normal file
82
src/hooks/useJiraConfig.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { jiraConfigClient, SaveJiraConfigRequest } from '@/clients/jira-config-client';
|
||||
import { JiraConfig } from '@/lib/types';
|
||||
|
||||
export function useJiraConfig() {
|
||||
const [config, setConfig] = useState<JiraConfig | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Charger la config au montage
|
||||
useEffect(() => {
|
||||
loadConfig();
|
||||
}, []);
|
||||
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
const jiraConfig = await jiraConfigClient.getJiraConfig();
|
||||
setConfig(jiraConfig);
|
||||
} catch (err) {
|
||||
console.error('Erreur lors du chargement de la config Jira:', err);
|
||||
setError('Erreur lors du chargement de la configuration');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const saveConfig = async (configData: SaveJiraConfigRequest) => {
|
||||
try {
|
||||
setError(null);
|
||||
const response = await jiraConfigClient.saveJiraConfig(configData);
|
||||
|
||||
if (response.success) {
|
||||
setConfig(response.jiraConfig);
|
||||
return { success: true, message: response.message };
|
||||
} else {
|
||||
setError('Erreur lors de la sauvegarde');
|
||||
return { success: false, message: 'Erreur lors de la sauvegarde' };
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la sauvegarde de la configuration';
|
||||
setError(errorMessage);
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
const deleteConfig = async () => {
|
||||
try {
|
||||
setError(null);
|
||||
const response = await jiraConfigClient.deleteJiraConfig();
|
||||
|
||||
if (response.success) {
|
||||
setConfig({
|
||||
baseUrl: '',
|
||||
email: '',
|
||||
apiToken: '',
|
||||
enabled: false,
|
||||
projectKey: '',
|
||||
ignoredProjects: []
|
||||
});
|
||||
return { success: true, message: response.message };
|
||||
} else {
|
||||
setError('Erreur lors de la suppression');
|
||||
return { success: false, message: 'Erreur lors de la suppression' };
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la suppression de la configuration';
|
||||
setError(errorMessage);
|
||||
return { success: false, message: errorMessage };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
config,
|
||||
isLoading,
|
||||
error,
|
||||
saveConfig,
|
||||
deleteConfig,
|
||||
refetch: loadConfig
|
||||
};
|
||||
}
|
||||
58
src/hooks/useJiraExport.ts
Normal file
58
src/hooks/useJiraExport.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { exportJiraAnalytics, ExportFormat } from '@/actions/jira-export';
|
||||
|
||||
export function useJiraExport() {
|
||||
const [isExporting, startTransition] = useTransition();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const exportAnalytics = (format: ExportFormat = 'csv') => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setError(null);
|
||||
|
||||
const result = await exportJiraAnalytics(format);
|
||||
|
||||
if (result.success && result.data && result.filename) {
|
||||
// Créer un blob et déclencher le téléchargement
|
||||
const mimeType = format === 'json' ? 'application/json' : 'text/csv';
|
||||
const blob = new Blob([result.data], { type: mimeType });
|
||||
|
||||
// Créer un lien temporaire pour le téléchargement
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = result.filename;
|
||||
|
||||
// Déclencher le téléchargement
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
// Nettoyer
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
console.log(`✅ Export ${format.toUpperCase()} réussi: ${result.filename}`);
|
||||
} else {
|
||||
setError(result.error || 'Erreur lors de l\'export');
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Erreur lors de l\'export';
|
||||
setError(errorMessage);
|
||||
console.error('Erreur export analytics:', err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const exportCSV = () => exportAnalytics('csv');
|
||||
const exportJSON = () => exportAnalytics('json');
|
||||
|
||||
return {
|
||||
isExporting,
|
||||
error,
|
||||
exportCSV,
|
||||
exportJSON,
|
||||
exportAnalytics
|
||||
};
|
||||
}
|
||||
98
src/hooks/useJiraFilters.ts
Normal file
98
src/hooks/useJiraFilters.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { getAvailableJiraFilters, getFilteredJiraAnalytics } from '@/actions/jira-filters';
|
||||
import { AvailableFilters, JiraAnalyticsFilters, JiraAnalytics } from '@/lib/types';
|
||||
import { JiraAdvancedFiltersService } from '@/services/jira-advanced-filters';
|
||||
|
||||
export function useJiraFilters() {
|
||||
const [availableFilters, setAvailableFilters] = useState<AvailableFilters>({
|
||||
components: [],
|
||||
fixVersions: [],
|
||||
issueTypes: [],
|
||||
statuses: [],
|
||||
assignees: [],
|
||||
labels: [],
|
||||
priorities: []
|
||||
});
|
||||
|
||||
const [activeFilters, setActiveFilters] = useState<Partial<JiraAnalyticsFilters>>(
|
||||
JiraAdvancedFiltersService.createEmptyFilters()
|
||||
);
|
||||
|
||||
const [filteredAnalytics, setFilteredAnalytics] = useState<JiraAnalytics | null>(null);
|
||||
const [isLoadingFilters, setIsLoadingFilters] = useState(false);
|
||||
const [isLoadingAnalytics, setIsLoadingAnalytics] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// Charger les filtres disponibles
|
||||
const loadAvailableFilters = useCallback(async () => {
|
||||
setIsLoadingFilters(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await getAvailableJiraFilters();
|
||||
|
||||
if (result.success && result.data) {
|
||||
setAvailableFilters(result.data);
|
||||
} else {
|
||||
setError(result.error || 'Erreur lors du chargement des filtres');
|
||||
}
|
||||
} catch {
|
||||
setError('Erreur de connexion');
|
||||
} finally {
|
||||
setIsLoadingFilters(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Appliquer les filtres et récupérer les analytics filtrées
|
||||
const applyFilters = useCallback(async (filters: Partial<JiraAnalyticsFilters>) => {
|
||||
setIsLoadingAnalytics(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const result = await getFilteredJiraAnalytics(filters);
|
||||
|
||||
if (result.success && result.data) {
|
||||
setFilteredAnalytics(result.data);
|
||||
setActiveFilters(filters);
|
||||
} else {
|
||||
setError(result.error || 'Erreur lors du filtrage');
|
||||
}
|
||||
} catch {
|
||||
setError('Erreur de connexion');
|
||||
} finally {
|
||||
setIsLoadingAnalytics(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Effacer tous les filtres
|
||||
const clearFilters = useCallback(() => {
|
||||
const emptyFilters = JiraAdvancedFiltersService.createEmptyFilters();
|
||||
setActiveFilters(emptyFilters);
|
||||
setFilteredAnalytics(null);
|
||||
}, []);
|
||||
|
||||
// Chargement initial des filtres disponibles
|
||||
useEffect(() => {
|
||||
loadAvailableFilters();
|
||||
}, [loadAvailableFilters]);
|
||||
|
||||
return {
|
||||
// État
|
||||
availableFilters,
|
||||
activeFilters,
|
||||
filteredAnalytics,
|
||||
isLoadingFilters,
|
||||
isLoadingAnalytics,
|
||||
error,
|
||||
|
||||
// Actions
|
||||
loadAvailableFilters,
|
||||
applyFilters,
|
||||
clearFilters,
|
||||
|
||||
// Helpers
|
||||
hasActiveFilters: JiraAdvancedFiltersService.hasActiveFilters(activeFilters),
|
||||
activeFiltersCount: JiraAdvancedFiltersService.countActiveFilters(activeFilters),
|
||||
filtersSummary: JiraAdvancedFiltersService.getFiltersSummary(activeFilters)
|
||||
};
|
||||
}
|
||||
250
src/hooks/useTags.ts
Normal file
250
src/hooks/useTags.ts
Normal file
@@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback, useTransition } from 'react';
|
||||
import { tagsClient, TagFilters, TagsClient } from '@/clients/tags-client';
|
||||
import { createTag, updateTag, deleteTag as deleteTagAction } from '@/actions/tags';
|
||||
import { Tag } from '@/lib/types';
|
||||
|
||||
interface UseTagsState {
|
||||
tags: Array<Tag & { usage: number }>;
|
||||
popularTags: Array<Tag & { usage: number }>;
|
||||
loading: boolean;
|
||||
isPending: boolean; // Loading state for server actions
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface UseTagsActions {
|
||||
refreshTags: () => Promise<void>;
|
||||
searchTags: (query: string, limit?: number) => Promise<Tag[]>;
|
||||
createTag: (name: string, color: string) => Promise<void>;
|
||||
updateTag: (tagId: string, data: { name?: string; color?: string; isPinned?: boolean }) => Promise<void>;
|
||||
deleteTag: (tagId: string) => Promise<void>;
|
||||
getPopularTags: (limit?: number) => Promise<void>;
|
||||
setFilters: (filters: TagFilters) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook pour la gestion des tags
|
||||
*/
|
||||
export function useTags(
|
||||
initialData?: (Tag & { usage: number })[],
|
||||
initialFilters?: TagFilters
|
||||
): UseTagsState & UseTagsActions {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [state, setState] = useState<UseTagsState>({
|
||||
tags: initialData || [],
|
||||
popularTags: initialData || [],
|
||||
loading: !initialData,
|
||||
isPending: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<TagFilters>(initialFilters || {});
|
||||
|
||||
/**
|
||||
* Récupère les tags depuis l'API
|
||||
*/
|
||||
const refreshTags = useCallback(async () => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tagsClient.getTags(filters);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
tags: response.data,
|
||||
loading: false
|
||||
}));
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||
}));
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
/**
|
||||
* Recherche des tags par nom (pour autocomplete)
|
||||
*/
|
||||
const searchTags = useCallback(async (query: string, limit: number = 10): Promise<Tag[]> => {
|
||||
try {
|
||||
const response = await tagsClient.searchTags(query, limit);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la recherche de tags:', error);
|
||||
return [];
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Récupère les tags populaires
|
||||
*/
|
||||
const getPopularTags = useCallback(async (limit: number = 10) => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tagsClient.getPopularTags(limit);
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
popularTags: response.data,
|
||||
loading: false
|
||||
}));
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Crée un nouveau tag avec server action
|
||||
*/
|
||||
const createTagAction = useCallback(async (name: string, color: string): Promise<void> => {
|
||||
// Validation côté client
|
||||
const errors = TagsClient.validateTagData({ name, color });
|
||||
if (errors.length > 0) {
|
||||
setState(prev => ({ ...prev, error: errors[0] }));
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
const result = await createTag(name, color);
|
||||
|
||||
if (!result.success) {
|
||||
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la création' }));
|
||||
}
|
||||
// Note: revalidatePath in server action handles cache refresh automatically
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la création'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Met à jour un tag avec server action
|
||||
*/
|
||||
const updateTagAction = useCallback(async (
|
||||
tagId: string,
|
||||
data: { name?: string; color?: string; isPinned?: boolean }
|
||||
): Promise<void> => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
const result = await updateTag(tagId, data);
|
||||
|
||||
if (!result.success) {
|
||||
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la mise à jour' }));
|
||||
}
|
||||
// Note: revalidatePath in server action handles cache refresh automatically
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour'
|
||||
}));
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Supprime un tag avec server action
|
||||
*/
|
||||
const deleteTagActionHandler = useCallback(async (tagId: string): Promise<void> => {
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setState(prev => ({ ...prev, error: null }));
|
||||
const result = await deleteTagAction(tagId);
|
||||
|
||||
if (!result.success) {
|
||||
setState(prev => ({ ...prev, error: result.error || 'Erreur lors de la suppression' }));
|
||||
throw new Error(result.error);
|
||||
}
|
||||
// Note: revalidatePath in server action handles cache refresh automatically
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la suppression'
|
||||
}));
|
||||
throw error; // Re-throw pour que l'UI puisse gérer l'erreur
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Charger les tags au montage et quand les filtres changent (seulement si pas de données initiales)
|
||||
useEffect(() => {
|
||||
if (!initialData) {
|
||||
refreshTags();
|
||||
}
|
||||
}, [refreshTags, initialData]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
isPending, // Expose loading state from useTransition
|
||||
refreshTags,
|
||||
searchTags,
|
||||
createTag: createTagAction,
|
||||
updateTag: updateTagAction,
|
||||
deleteTag: deleteTagActionHandler,
|
||||
getPopularTags,
|
||||
setFilters
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook simplifié pour l'autocomplete des tags
|
||||
*/
|
||||
export function useTagsAutocomplete() {
|
||||
const [suggestions, setSuggestions] = useState<Tag[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const searchTags = useCallback(async (query: string) => {
|
||||
if (!query.trim()) {
|
||||
setSuggestions([]);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await tagsClient.searchTags(query, 5);
|
||||
setSuggestions(response.data);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la recherche de tags:', error);
|
||||
setSuggestions([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const clearSuggestions = useCallback(() => {
|
||||
setSuggestions([]);
|
||||
}, []);
|
||||
|
||||
const loadPopularTags = useCallback(async (limit: number = 20) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await tagsClient.getPopularTags(limit);
|
||||
setSuggestions(response.data);
|
||||
} catch (error) {
|
||||
console.error('Erreur lors du chargement des tags populaires:', error);
|
||||
setSuggestions([]);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return {
|
||||
suggestions,
|
||||
loading,
|
||||
searchTags,
|
||||
clearSuggestions,
|
||||
loadPopularTags
|
||||
};
|
||||
}
|
||||
206
src/hooks/useTasks.ts
Normal file
206
src/hooks/useTasks.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { tasksClient, TaskFilters, CreateTaskData } from '@/clients/tasks-client';
|
||||
import { updateTaskStatus, createTask as createTaskAction } from '@/actions/tasks';
|
||||
import { Task, TaskStats, TaskStatus } from '@/lib/types';
|
||||
|
||||
interface UseTasksState {
|
||||
tasks: Task[];
|
||||
stats: TaskStats;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
syncing: boolean; // Pour indiquer les opérations optimistes en cours
|
||||
}
|
||||
|
||||
interface UseTasksActions {
|
||||
refreshTasks: () => Promise<void>;
|
||||
createTask: (data: CreateTaskData) => Promise<Task | null>;
|
||||
updateTaskOptimistic: (taskId: string, status: TaskStatus) => Promise<Task | null>;
|
||||
setFilters: (filters: TaskFilters) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook pour la gestion des tâches
|
||||
*/
|
||||
export function useTasks(
|
||||
initialFilters?: TaskFilters,
|
||||
initialData?: { tasks: Task[]; stats?: TaskStats }
|
||||
): UseTasksState & UseTasksActions {
|
||||
const [state, setState] = useState<UseTasksState>({
|
||||
tasks: initialData?.tasks || [],
|
||||
stats: initialData?.stats || {
|
||||
total: 0,
|
||||
completed: 0,
|
||||
inProgress: 0,
|
||||
todo: 0,
|
||||
backlog: 0,
|
||||
cancelled: 0,
|
||||
freeze: 0,
|
||||
archived: 0,
|
||||
completionRate: 0
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
syncing: false
|
||||
});
|
||||
|
||||
const [filters, setFilters] = useState<TaskFilters>(initialFilters || {});
|
||||
|
||||
/**
|
||||
* Récupère les tâches depuis l'API
|
||||
*/
|
||||
const refreshTasks = useCallback(async () => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const response = await tasksClient.getTasks({ ...filters, limit: undefined });
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
tasks: response.data,
|
||||
stats: response.stats,
|
||||
loading: false
|
||||
}));
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur inconnue'
|
||||
}));
|
||||
}
|
||||
}, [filters]);
|
||||
|
||||
/**
|
||||
* Crée une nouvelle tâche
|
||||
*/
|
||||
const createTask = useCallback(async (data: CreateTaskData): Promise<Task | null> => {
|
||||
setState(prev => ({ ...prev, loading: true, error: null }));
|
||||
|
||||
try {
|
||||
const result = await createTaskAction({
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
status: data.status,
|
||||
priority: data.priority,
|
||||
tags: data.tags
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
// Rafraîchir la liste après création
|
||||
await refreshTasks();
|
||||
return result.data as Task;
|
||||
} else {
|
||||
throw new Error(result.error || 'Erreur lors de la création');
|
||||
}
|
||||
} catch (error) {
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la création'
|
||||
}));
|
||||
return null;
|
||||
}
|
||||
}, [refreshTasks]);
|
||||
|
||||
// Note: updateTask et deleteTask ont été migrés vers Server Actions
|
||||
// Voir /src/actions/tasks.ts pour updateTaskTitle, updateTaskStatus, deleteTask
|
||||
|
||||
/**
|
||||
* Met à jour le statut d'une tâche de manière optimiste (pour drag & drop)
|
||||
*/
|
||||
const updateTaskOptimistic = useCallback(async (taskId: string, status: TaskStatus): Promise<Task | null> => {
|
||||
// 1. Sauvegarder l'état actuel pour rollback
|
||||
const currentTasks = state.tasks;
|
||||
const taskToUpdate = currentTasks.find(t => t.id === taskId);
|
||||
|
||||
if (!taskToUpdate) {
|
||||
console.error('Tâche non trouvée pour mise à jour optimiste:', taskId);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 2. Mise à jour optimiste immédiate de l'état local
|
||||
const updatedTask = { ...taskToUpdate, status };
|
||||
const updatedTasks = currentTasks.map(task =>
|
||||
task.id === taskId ? updatedTask : task
|
||||
);
|
||||
|
||||
// Recalculer les stats
|
||||
const newStats = {
|
||||
total: updatedTasks.length,
|
||||
completed: updatedTasks.filter(t => t.status === 'done').length,
|
||||
inProgress: updatedTasks.filter(t => t.status === 'in_progress').length,
|
||||
todo: updatedTasks.filter(t => t.status === 'todo').length,
|
||||
backlog: updatedTasks.filter(t => t.status === 'backlog').length,
|
||||
cancelled: updatedTasks.filter(t => t.status === 'cancelled').length,
|
||||
freeze: updatedTasks.filter(t => t.status === 'freeze').length,
|
||||
archived: updatedTasks.filter(t => t.status === 'archived').length,
|
||||
completionRate: updatedTasks.length > 0
|
||||
? Math.round((updatedTasks.filter(t => t.status === 'done').length / updatedTasks.length) * 100)
|
||||
: 0
|
||||
};
|
||||
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
tasks: updatedTasks,
|
||||
stats: newStats,
|
||||
error: null,
|
||||
syncing: true // Indiquer qu'une synchronisation est en cours
|
||||
}));
|
||||
|
||||
// 3. Appel Server Action en arrière-plan
|
||||
try {
|
||||
const result = await updateTaskStatus(taskId, status);
|
||||
|
||||
// Si l'action réussit, la revalidation automatique se charge du reste
|
||||
if (result.success) {
|
||||
setState(prev => ({ ...prev, syncing: false }));
|
||||
return result.data as Task;
|
||||
} else {
|
||||
throw new Error(result.error || 'Erreur lors de la mise à jour');
|
||||
}
|
||||
} catch (error) {
|
||||
// 4. Rollback en cas d'erreur
|
||||
setState(prev => ({
|
||||
...prev,
|
||||
tasks: currentTasks,
|
||||
stats: {
|
||||
total: currentTasks.length,
|
||||
completed: currentTasks.filter(t => t.status === 'done').length,
|
||||
inProgress: currentTasks.filter(t => t.status === 'in_progress').length,
|
||||
todo: currentTasks.filter(t => t.status === 'todo').length,
|
||||
backlog: currentTasks.filter(t => t.status === 'backlog').length,
|
||||
cancelled: currentTasks.filter(t => t.status === 'cancelled').length,
|
||||
freeze: currentTasks.filter(t => t.status === 'freeze').length,
|
||||
archived: currentTasks.filter(t => t.status === 'archived').length,
|
||||
completionRate: currentTasks.length > 0
|
||||
? Math.round((currentTasks.filter(t => t.status === 'done').length / currentTasks.length) * 100)
|
||||
: 0
|
||||
},
|
||||
error: error instanceof Error ? error.message : 'Erreur lors de la mise à jour',
|
||||
syncing: false // Arrêter l'indicateur de synchronisation
|
||||
}));
|
||||
|
||||
console.error('Erreur lors de la mise à jour optimiste:', error);
|
||||
return null;
|
||||
}
|
||||
}, [state.tasks]);
|
||||
|
||||
// Note: deleteTask a été migré vers Server Actions
|
||||
// Utilisez directement deleteTask depuis /src/actions/tasks.ts dans les composants
|
||||
|
||||
// Charger les tâches au montage seulement si pas de données initiales
|
||||
useEffect(() => {
|
||||
if (!initialData?.tasks?.length) {
|
||||
refreshTasks();
|
||||
}
|
||||
}, [refreshTasks, initialData?.tasks?.length]);
|
||||
|
||||
return {
|
||||
...state,
|
||||
refreshTasks,
|
||||
createTask,
|
||||
updateTaskOptimistic,
|
||||
setFilters
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user