feat: implement Daily management features and update UI
- Marked tasks as completed in TODO for Daily management service, data model, and interactive checkboxes. - Added a new link to the Daily page in the Header component for easy navigation. - Introduced DailyCheckbox model in Prisma schema and corresponding TypeScript interfaces for better data handling. - Updated database schema to include daily checkboxes, enhancing task management capabilities.
This commit is contained in:
18
TODO.md
18
TODO.md
@@ -99,16 +99,16 @@
|
|||||||
## 📊 Phase 3: Intégrations et analytics (Priorité 3)
|
## 📊 Phase 3: Intégrations et analytics (Priorité 3)
|
||||||
|
|
||||||
### 3.1 Gestion du Daily
|
### 3.1 Gestion du Daily
|
||||||
- [ ] Créer `services/daily.ts` - Service de gestion des daily notes
|
- [x] Créer `services/daily.ts` - Service de gestion des daily notes
|
||||||
- [ ] Modèle de données Daily (date, checkboxes hier/aujourd'hui)
|
- [x] Modèle de données Daily (date, checkboxes hier/aujourd'hui)
|
||||||
- [ ] Interface Daily avec sections "Hier" et "Aujourd'hui"
|
- [x] Interface Daily avec sections "Hier" et "Aujourd'hui"
|
||||||
- [ ] Checkboxes interactives avec état coché/non-coché
|
- [x] Checkboxes interactives avec état coché/non-coché
|
||||||
- [ ] Liaison optionnelle checkbox ↔ tâche existante
|
- [x] Liaison optionnelle checkbox ↔ tâche existante
|
||||||
- [ ] Cocher une checkbox NE change PAS le statut de la tâche liée
|
- [x] Cocher une checkbox NE change PAS le statut de la tâche liée
|
||||||
- [ ] Navigation par date (daily précédent/suivant)
|
- [x] Navigation par date (daily précédent/suivant)
|
||||||
- [ ] Auto-création du daily du jour si inexistant
|
- [x] Auto-création du daily du jour si inexistant
|
||||||
|
- [x] UX améliorée : édition au clic, focus persistant, input large
|
||||||
- [ ] Vue calendar/historique des dailies
|
- [ ] Vue calendar/historique des dailies
|
||||||
- [ ] Export/import depuis Confluence (optionnel)
|
|
||||||
- [ ] Templates de daily personnalisables
|
- [ ] Templates de daily personnalisables
|
||||||
- [ ] Recherche dans l'historique des dailies
|
- [ ] Recherche dans l'historique des dailies
|
||||||
|
|
||||||
|
|||||||
153
clients/daily-client.ts
Normal file
153
clients/daily-client.ts
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import { httpClient } from './base/http-client';
|
||||||
|
import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData } from '@/lib/types';
|
||||||
|
|
||||||
|
export interface DailyHistoryFilters {
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DailySearchFilters {
|
||||||
|
query: string;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReorderCheckboxesData {
|
||||||
|
date: Date;
|
||||||
|
checkboxIds: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DailyClient {
|
||||||
|
/**
|
||||||
|
* Récupère la vue daily d'aujourd'hui (hier + aujourd'hui)
|
||||||
|
*/
|
||||||
|
async getTodaysDailyView(): Promise<DailyView> {
|
||||||
|
return httpClient.get('/daily');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la vue daily pour une date donnée
|
||||||
|
*/
|
||||||
|
async getDailyView(date: Date): Promise<DailyView> {
|
||||||
|
const dateStr = this.formatDateForAPI(date);
|
||||||
|
return httpClient.get(`/daily?date=${dateStr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'historique des checkboxes
|
||||||
|
*/
|
||||||
|
async getCheckboxHistory(filters?: DailyHistoryFilters): Promise<{ date: Date; checkboxes: DailyCheckbox[] }[]> {
|
||||||
|
const params = new URLSearchParams({ action: 'history' });
|
||||||
|
|
||||||
|
if (filters?.limit) params.append('limit', filters.limit.toString());
|
||||||
|
|
||||||
|
return httpClient.get(`/daily?${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche dans les checkboxes
|
||||||
|
*/
|
||||||
|
async searchCheckboxes(filters: DailySearchFilters): Promise<DailyCheckbox[]> {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
action: 'search',
|
||||||
|
q: filters.query
|
||||||
|
});
|
||||||
|
|
||||||
|
if (filters.limit) params.append('limit', filters.limit.toString());
|
||||||
|
|
||||||
|
return httpClient.get(`/daily?${params}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une checkbox
|
||||||
|
*/
|
||||||
|
async addCheckbox(data: CreateDailyCheckboxData): Promise<DailyCheckbox> {
|
||||||
|
return httpClient.post('/daily', {
|
||||||
|
...data,
|
||||||
|
date: this.formatDateForAPI(data.date)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une checkbox pour aujourd'hui
|
||||||
|
*/
|
||||||
|
async addTodayCheckbox(text: string, taskId?: string): Promise<DailyCheckbox> {
|
||||||
|
return this.addCheckbox({
|
||||||
|
date: new Date(),
|
||||||
|
text,
|
||||||
|
taskId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une checkbox pour hier
|
||||||
|
*/
|
||||||
|
async addYesterdayCheckbox(text: string, taskId?: string): Promise<DailyCheckbox> {
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
return this.addCheckbox({
|
||||||
|
date: yesterday,
|
||||||
|
text,
|
||||||
|
taskId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une checkbox
|
||||||
|
*/
|
||||||
|
async updateCheckbox(checkboxId: string, data: UpdateDailyCheckboxData): Promise<DailyCheckbox> {
|
||||||
|
return httpClient.patch(`/daily/checkboxes/${checkboxId}`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une checkbox
|
||||||
|
*/
|
||||||
|
async deleteCheckbox(checkboxId: string): Promise<void> {
|
||||||
|
return httpClient.delete(`/daily/checkboxes/${checkboxId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réordonne les checkboxes d'une date
|
||||||
|
*/
|
||||||
|
async reorderCheckboxes(data: ReorderCheckboxesData): Promise<void> {
|
||||||
|
return httpClient.post('/daily/checkboxes', {
|
||||||
|
date: this.formatDateForAPI(data.date),
|
||||||
|
checkboxIds: data.checkboxIds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coche/décoche une checkbox (raccourci)
|
||||||
|
*/
|
||||||
|
async toggleCheckbox(checkboxId: string, isChecked: boolean): Promise<DailyCheckbox> {
|
||||||
|
return this.updateCheckbox(checkboxId, { isChecked });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formate une date pour l'API
|
||||||
|
*/
|
||||||
|
formatDateForAPI(date: Date): string {
|
||||||
|
return date.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la vue daily d'une date relative (hier, aujourd'hui, demain)
|
||||||
|
*/
|
||||||
|
async getDailyViewByRelativeDate(relative: 'yesterday' | 'today' | 'tomorrow'): Promise<DailyView> {
|
||||||
|
const date = new Date();
|
||||||
|
|
||||||
|
switch (relative) {
|
||||||
|
case 'yesterday':
|
||||||
|
date.setDate(date.getDate() - 1);
|
||||||
|
break;
|
||||||
|
case 'tomorrow':
|
||||||
|
date.setDate(date.getDate() + 1);
|
||||||
|
break;
|
||||||
|
// 'today' ne change rien
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getDailyView(date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance singleton du client
|
||||||
|
export const dailyClient = new DailyClient();
|
||||||
@@ -43,6 +43,12 @@ export function Header({ title, subtitle, stats, syncing = false }: HeaderProps)
|
|||||||
>
|
>
|
||||||
Kanban
|
Kanban
|
||||||
</Link>
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/daily"
|
||||||
|
className="text-[var(--muted-foreground)] hover:text-[var(--primary)] transition-colors font-mono text-sm uppercase tracking-wider"
|
||||||
|
>
|
||||||
|
Daily
|
||||||
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/tags"
|
href="/tags"
|
||||||
className="text-[var(--muted-foreground)] hover:text-[var(--accent)] transition-colors font-mono text-sm uppercase tracking-wider"
|
className="text-[var(--muted-foreground)] hover:text-[var(--accent)] transition-colors font-mono text-sm uppercase tracking-wider"
|
||||||
|
|||||||
290
hooks/useDaily.ts
Normal file
290
hooks/useDaily.ts
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
|
import { dailyClient, DailyHistoryFilters, DailySearchFilters, ReorderCheckboxesData } from '@/clients/daily-client';
|
||||||
|
import { DailyView, DailyCheckbox, UpdateDailyCheckboxData } from '@/lib/types';
|
||||||
|
|
||||||
|
interface UseDailyState {
|
||||||
|
dailyView: DailyView | null;
|
||||||
|
loading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
saving: boolean; // Pour indiquer les opérations en cours
|
||||||
|
}
|
||||||
|
|
||||||
|
interface UseDailyActions {
|
||||||
|
refreshDaily: () => Promise<void>;
|
||||||
|
addTodayCheckbox: (text: string, taskId?: string) => Promise<DailyCheckbox | null>;
|
||||||
|
addYesterdayCheckbox: (text: string, taskId?: string) => Promise<DailyCheckbox | null>;
|
||||||
|
updateCheckbox: (checkboxId: string, data: UpdateDailyCheckboxData) => Promise<DailyCheckbox | null>;
|
||||||
|
deleteCheckbox: (checkboxId: string) => Promise<void>;
|
||||||
|
toggleCheckbox: (checkboxId: string) => 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): UseDailyState & UseDailyActions & { currentDate: Date } {
|
||||||
|
const [currentDate, setCurrentDate] = useState<Date>(initialDate || new Date());
|
||||||
|
const [dailyView, setDailyView] = useState<DailyView | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
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 addTodayCheckbox = useCallback(async (text: string, taskId?: string): Promise<DailyCheckbox | null> => {
|
||||||
|
if (!dailyView) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const newCheckbox = await dailyClient.addTodayCheckbox(text, taskId);
|
||||||
|
|
||||||
|
// Mise à jour optimiste
|
||||||
|
setDailyView(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
today: [...prev.today, newCheckbox].sort((a, b) => a.order - b.order)
|
||||||
|
} : null);
|
||||||
|
|
||||||
|
return newCheckbox;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erreur lors de l\'ajout de la checkbox');
|
||||||
|
console.error('Erreur addTodayCheckbox:', err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}, [dailyView]);
|
||||||
|
|
||||||
|
const addYesterdayCheckbox = useCallback(async (text: string, taskId?: string): Promise<DailyCheckbox | null> => {
|
||||||
|
if (!dailyView) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const newCheckbox = await dailyClient.addYesterdayCheckbox(text, taskId);
|
||||||
|
|
||||||
|
// Mise à jour optimiste
|
||||||
|
setDailyView(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
yesterday: [...prev.yesterday, newCheckbox].sort((a, b) => a.order - b.order)
|
||||||
|
} : null);
|
||||||
|
|
||||||
|
return newCheckbox;
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erreur lors de l\'ajout de la checkbox');
|
||||||
|
console.error('Erreur addYesterdayCheckbox:', err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}, [dailyView]);
|
||||||
|
|
||||||
|
const updateCheckbox = useCallback(async (checkboxId: string, data: UpdateDailyCheckboxData): Promise<DailyCheckbox | null> => {
|
||||||
|
if (!dailyView) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
const updatedCheckbox = await dailyClient.updateCheckbox(checkboxId, data);
|
||||||
|
|
||||||
|
// Mise à jour optimiste
|
||||||
|
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) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erreur lors de la mise à jour de la checkbox');
|
||||||
|
console.error('Erreur updateCheckbox:', err);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}, [dailyView]);
|
||||||
|
|
||||||
|
const deleteCheckbox = useCallback(async (checkboxId: string): Promise<void> => {
|
||||||
|
if (!dailyView) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
await dailyClient.deleteCheckbox(checkboxId);
|
||||||
|
|
||||||
|
// Mise à jour optimiste
|
||||||
|
setDailyView(prev => prev ? {
|
||||||
|
...prev,
|
||||||
|
yesterday: prev.yesterday.filter(cb => cb.id !== checkboxId),
|
||||||
|
today: prev.today.filter(cb => cb.id !== checkboxId)
|
||||||
|
} : null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : 'Erreur lors de la suppression de la checkbox');
|
||||||
|
console.error('Erreur deleteCheckbox:', err);
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}, [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;
|
||||||
|
|
||||||
|
await updateCheckbox(checkboxId, { isChecked: !checkbox.isChecked });
|
||||||
|
}, [dailyView, updateCheckbox]);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Charger le daily quand la date change
|
||||||
|
useEffect(() => {
|
||||||
|
refreshDaily();
|
||||||
|
}, [refreshDaily]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
// State
|
||||||
|
dailyView,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
saving,
|
||||||
|
currentDate,
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
refreshDaily,
|
||||||
|
addTodayCheckbox,
|
||||||
|
addYesterdayCheckbox,
|
||||||
|
updateCheckbox,
|
||||||
|
deleteCheckbox,
|
||||||
|
toggleCheckbox,
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
36
lib/types.ts
36
lib/types.ts
@@ -162,3 +162,39 @@ export class ValidationError extends Error {
|
|||||||
this.name = 'ValidationError';
|
this.name = 'ValidationError';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Types pour les dailies
|
||||||
|
export interface DailyCheckbox {
|
||||||
|
id: string;
|
||||||
|
date: Date;
|
||||||
|
text: string;
|
||||||
|
isChecked: boolean;
|
||||||
|
order: number;
|
||||||
|
taskId?: string;
|
||||||
|
task?: Task; // Relation optionnelle vers une tâche
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface pour créer/modifier une checkbox
|
||||||
|
export interface CreateDailyCheckboxData {
|
||||||
|
date: Date;
|
||||||
|
text: string;
|
||||||
|
taskId?: string;
|
||||||
|
order?: number;
|
||||||
|
isChecked?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateDailyCheckboxData {
|
||||||
|
text?: string;
|
||||||
|
isChecked?: boolean;
|
||||||
|
taskId?: string;
|
||||||
|
order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interface pour récupérer les checkboxes d'une journée
|
||||||
|
export interface DailyView {
|
||||||
|
date: Date;
|
||||||
|
yesterday: DailyCheckbox[]; // Checkboxes de la veille
|
||||||
|
today: DailyCheckbox[]; // Checkboxes du jour
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "dailies" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"date" DATETIME NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "daily_checkboxes" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"dailyId" TEXT NOT NULL,
|
||||||
|
"section" TEXT NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"isChecked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"order" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"taskId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "daily_checkboxes_dailyId_fkey" FOREIGN KEY ("dailyId") REFERENCES "dailies" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "daily_checkboxes_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "tasks" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "dailies_date_key" ON "dailies"("date");
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `dailies` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- You are about to drop the column `dailyId` on the `daily_checkboxes` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `section` on the `daily_checkboxes` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `date` to the `daily_checkboxes` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "dailies_date_key";
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
PRAGMA foreign_keys=off;
|
||||||
|
DROP TABLE "dailies";
|
||||||
|
PRAGMA foreign_keys=on;
|
||||||
|
|
||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_daily_checkboxes" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"date" DATETIME NOT NULL,
|
||||||
|
"text" TEXT NOT NULL,
|
||||||
|
"isChecked" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
"order" INTEGER NOT NULL DEFAULT 0,
|
||||||
|
"taskId" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL,
|
||||||
|
CONSTRAINT "daily_checkboxes_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "tasks" ("id") ON DELETE SET NULL ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO "new_daily_checkboxes" ("createdAt", "id", "isChecked", "order", "taskId", "text", "updatedAt") SELECT "createdAt", "id", "isChecked", "order", "taskId", "text", "updatedAt" FROM "daily_checkboxes";
|
||||||
|
DROP TABLE "daily_checkboxes";
|
||||||
|
ALTER TABLE "new_daily_checkboxes" RENAME TO "daily_checkboxes";
|
||||||
|
CREATE INDEX "daily_checkboxes_date_idx" ON "daily_checkboxes"("date");
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@@ -30,6 +30,7 @@ model Task {
|
|||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
taskTags TaskTag[]
|
taskTags TaskTag[]
|
||||||
|
dailyCheckboxes DailyCheckbox[]
|
||||||
|
|
||||||
@@unique([source, sourceId])
|
@@unique([source, sourceId])
|
||||||
@@map("tasks")
|
@@map("tasks")
|
||||||
@@ -65,3 +66,20 @@ model SyncLog {
|
|||||||
|
|
||||||
@@map("sync_logs")
|
@@map("sync_logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model DailyCheckbox {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
date DateTime // Date de la checkbox (YYYY-MM-DD)
|
||||||
|
text String // Texte de la checkbox
|
||||||
|
isChecked Boolean @default(false)
|
||||||
|
order Int @default(0) // Ordre d'affichage pour cette date
|
||||||
|
taskId String? // Liaison optionnelle vers une tâche
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
// Relations
|
||||||
|
task Task? @relation(fields: [taskId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@index([date])
|
||||||
|
@@map("daily_checkboxes")
|
||||||
|
}
|
||||||
|
|||||||
244
services/daily.ts
Normal file
244
services/daily.ts
Normal file
@@ -0,0 +1,244 @@
|
|||||||
|
import { prisma } from './database';
|
||||||
|
import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError } from '@/lib/types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service pour la gestion des checkboxes daily
|
||||||
|
*/
|
||||||
|
export class DailyService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la vue daily pour une date donnée (checkboxes d'hier et d'aujourd'hui)
|
||||||
|
*/
|
||||||
|
async getDailyView(date: Date): Promise<DailyView> {
|
||||||
|
// Normaliser la date (début de journée)
|
||||||
|
const today = new Date(date);
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const yesterday = new Date(today);
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
// Récupérer les checkboxes des deux jours
|
||||||
|
const [yesterdayCheckboxes, todayCheckboxes] = await Promise.all([
|
||||||
|
this.getCheckboxesByDate(yesterday),
|
||||||
|
this.getCheckboxesByDate(today)
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
date: today,
|
||||||
|
yesterday: yesterdayCheckboxes,
|
||||||
|
today: todayCheckboxes
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère toutes les checkboxes d'une date donnée
|
||||||
|
*/
|
||||||
|
async getCheckboxesByDate(date: Date): Promise<DailyCheckbox[]> {
|
||||||
|
// Normaliser la date (début de journée)
|
||||||
|
const normalizedDate = new Date(date);
|
||||||
|
normalizedDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||||
|
where: { date: normalizedDate },
|
||||||
|
include: { task: true },
|
||||||
|
orderBy: { order: 'asc' }
|
||||||
|
});
|
||||||
|
|
||||||
|
return checkboxes.map(this.mapPrismaCheckbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une checkbox à une date donnée
|
||||||
|
*/
|
||||||
|
async addCheckbox(data: CreateDailyCheckboxData): Promise<DailyCheckbox> {
|
||||||
|
// Normaliser la date
|
||||||
|
const normalizedDate = new Date(data.date);
|
||||||
|
normalizedDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
// Calculer l'ordre suivant pour cette date
|
||||||
|
const maxOrder = await prisma.dailyCheckbox.aggregate({
|
||||||
|
where: { date: normalizedDate },
|
||||||
|
_max: { order: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
const order = data.order ?? ((maxOrder._max.order ?? -1) + 1);
|
||||||
|
|
||||||
|
const checkbox = await prisma.dailyCheckbox.create({
|
||||||
|
data: {
|
||||||
|
date: normalizedDate,
|
||||||
|
text: data.text.trim(),
|
||||||
|
taskId: data.taskId,
|
||||||
|
order,
|
||||||
|
isChecked: data.isChecked ?? false
|
||||||
|
},
|
||||||
|
include: { task: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.mapPrismaCheckbox(checkbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Met à jour une checkbox
|
||||||
|
*/
|
||||||
|
async updateCheckbox(checkboxId: string, data: UpdateDailyCheckboxData): Promise<DailyCheckbox> {
|
||||||
|
const updateData: any = {};
|
||||||
|
|
||||||
|
if (data.text !== undefined) updateData.text = data.text.trim();
|
||||||
|
if (data.isChecked !== undefined) updateData.isChecked = data.isChecked;
|
||||||
|
if (data.taskId !== undefined) updateData.taskId = data.taskId;
|
||||||
|
if (data.order !== undefined) updateData.order = data.order;
|
||||||
|
|
||||||
|
const checkbox = await prisma.dailyCheckbox.update({
|
||||||
|
where: { id: checkboxId },
|
||||||
|
data: updateData,
|
||||||
|
include: { task: true }
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.mapPrismaCheckbox(checkbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supprime une checkbox
|
||||||
|
*/
|
||||||
|
async deleteCheckbox(checkboxId: string): Promise<void> {
|
||||||
|
const checkbox = await prisma.dailyCheckbox.findUnique({
|
||||||
|
where: { id: checkboxId }
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!checkbox) {
|
||||||
|
throw new BusinessError('Checkbox non trouvée');
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.dailyCheckbox.delete({
|
||||||
|
where: { id: checkboxId }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Réordonne les checkboxes d'une date donnée
|
||||||
|
*/
|
||||||
|
async reorderCheckboxes(date: Date, checkboxIds: string[]): Promise<void> {
|
||||||
|
// Normaliser la date
|
||||||
|
const normalizedDate = new Date(date);
|
||||||
|
normalizedDate.setHours(0, 0, 0, 0);
|
||||||
|
|
||||||
|
await prisma.$transaction(async (prisma) => {
|
||||||
|
for (let i = 0; i < checkboxIds.length; i++) {
|
||||||
|
await prisma.dailyCheckbox.update({
|
||||||
|
where: { id: checkboxIds[i] },
|
||||||
|
data: { order: i }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recherche dans les checkboxes
|
||||||
|
*/
|
||||||
|
async searchCheckboxes(query: string, limit: number = 20): Promise<DailyCheckbox[]> {
|
||||||
|
const checkboxes = await prisma.dailyCheckbox.findMany({
|
||||||
|
where: {
|
||||||
|
text: {
|
||||||
|
contains: query,
|
||||||
|
mode: 'insensitive'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
include: { task: true },
|
||||||
|
orderBy: { date: 'desc' },
|
||||||
|
take: limit
|
||||||
|
});
|
||||||
|
|
||||||
|
return checkboxes.map(this.mapPrismaCheckbox);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère l'historique des checkboxes (groupées par date)
|
||||||
|
*/
|
||||||
|
async getCheckboxHistory(limit: number = 30): Promise<{ date: Date; checkboxes: DailyCheckbox[] }[]> {
|
||||||
|
// Récupérer les dates distinctes des dernières checkboxes
|
||||||
|
const distinctDates = await prisma.dailyCheckbox.findMany({
|
||||||
|
select: { date: true },
|
||||||
|
distinct: ['date'],
|
||||||
|
orderBy: { date: 'desc' },
|
||||||
|
take: limit
|
||||||
|
});
|
||||||
|
|
||||||
|
const history = [];
|
||||||
|
for (const { date } of distinctDates) {
|
||||||
|
const checkboxes = await this.getCheckboxesByDate(date);
|
||||||
|
if (checkboxes.length > 0) {
|
||||||
|
history.push({ date, checkboxes });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return history;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupère la vue daily d'aujourd'hui
|
||||||
|
*/
|
||||||
|
async getTodaysDailyView(): Promise<DailyView> {
|
||||||
|
return this.getDailyView(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une checkbox pour aujourd'hui
|
||||||
|
*/
|
||||||
|
async addTodayCheckbox(text: string, taskId?: string): Promise<DailyCheckbox> {
|
||||||
|
return this.addCheckbox({
|
||||||
|
date: new Date(),
|
||||||
|
text,
|
||||||
|
taskId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ajoute une checkbox pour hier
|
||||||
|
*/
|
||||||
|
async addYesterdayCheckbox(text: string, taskId?: string): Promise<DailyCheckbox> {
|
||||||
|
const yesterday = new Date();
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
|
||||||
|
return this.addCheckbox({
|
||||||
|
date: yesterday,
|
||||||
|
text,
|
||||||
|
taskId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mappe une checkbox Prisma vers notre interface
|
||||||
|
*/
|
||||||
|
private mapPrismaCheckbox(checkbox: any): DailyCheckbox {
|
||||||
|
return {
|
||||||
|
id: checkbox.id,
|
||||||
|
date: checkbox.date,
|
||||||
|
text: checkbox.text,
|
||||||
|
isChecked: checkbox.isChecked,
|
||||||
|
order: checkbox.order,
|
||||||
|
taskId: checkbox.taskId,
|
||||||
|
task: checkbox.task ? {
|
||||||
|
id: checkbox.task.id,
|
||||||
|
title: checkbox.task.title,
|
||||||
|
description: checkbox.task.description,
|
||||||
|
status: checkbox.task.status,
|
||||||
|
priority: checkbox.task.priority,
|
||||||
|
source: checkbox.task.source,
|
||||||
|
sourceId: checkbox.task.sourceId,
|
||||||
|
tags: [], // Les tags seront chargés séparément si nécessaire
|
||||||
|
dueDate: checkbox.task.dueDate,
|
||||||
|
completedAt: checkbox.task.completedAt,
|
||||||
|
createdAt: checkbox.task.createdAt,
|
||||||
|
updatedAt: checkbox.task.updatedAt,
|
||||||
|
jiraProject: checkbox.task.jiraProject,
|
||||||
|
jiraKey: checkbox.task.jiraKey,
|
||||||
|
assignee: checkbox.task.assignee
|
||||||
|
} : undefined,
|
||||||
|
createdAt: checkbox.createdAt,
|
||||||
|
updatedAt: checkbox.updatedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance singleton du service
|
||||||
|
export const dailyService = new DailyService();
|
||||||
63
src/app/api/daily/checkboxes/[id]/route.ts
Normal file
63
src/app/api/daily/checkboxes/[id]/route.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { dailyService } from '@/services/daily';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API route pour mettre à jour une checkbox
|
||||||
|
*/
|
||||||
|
export async function PATCH(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
const { id: checkboxId } = await params;
|
||||||
|
|
||||||
|
const checkbox = await dailyService.updateCheckbox(checkboxId, body);
|
||||||
|
return NextResponse.json(checkbox);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la mise à jour de la checkbox:', error);
|
||||||
|
|
||||||
|
if (error instanceof Error && error.message.includes('Record to update not found')) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Checkbox non trouvée' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erreur interne du serveur' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API route pour supprimer une checkbox
|
||||||
|
*/
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const { id: checkboxId } = await params;
|
||||||
|
|
||||||
|
await dailyService.deleteCheckbox(checkboxId);
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la suppression de la checkbox:', error);
|
||||||
|
|
||||||
|
if (error instanceof Error && error.message.includes('Checkbox non trouvée')) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Checkbox non trouvée' },
|
||||||
|
{ status: 404 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erreur interne du serveur' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/app/api/daily/checkboxes/route.ts
Normal file
38
src/app/api/daily/checkboxes/route.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { dailyService } from '@/services/daily';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API route pour réordonner les checkboxes d'une date
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Validation des données
|
||||||
|
if (!body.date || !Array.isArray(body.checkboxIds)) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'date et checkboxIds (array) sont requis' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(body.date);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await dailyService.reorderCheckboxes(date, body.checkboxIds);
|
||||||
|
|
||||||
|
return NextResponse.json({ success: true });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors du réordonnancement:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erreur interne du serveur' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
96
src/app/api/daily/route.ts
Normal file
96
src/app/api/daily/route.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { dailyService } from '@/services/daily';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API route pour récupérer la vue daily (hier + aujourd'hui)
|
||||||
|
*/
|
||||||
|
export async function GET(request: Request) {
|
||||||
|
try {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
|
||||||
|
const action = searchParams.get('action');
|
||||||
|
const date = searchParams.get('date');
|
||||||
|
|
||||||
|
if (action === 'history') {
|
||||||
|
// Récupérer l'historique
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '30');
|
||||||
|
const history = await dailyService.getCheckboxHistory(limit);
|
||||||
|
return NextResponse.json(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'search') {
|
||||||
|
// Recherche dans les checkboxes
|
||||||
|
const query = searchParams.get('q') || '';
|
||||||
|
const limit = parseInt(searchParams.get('limit') || '20');
|
||||||
|
|
||||||
|
if (!query.trim()) {
|
||||||
|
return NextResponse.json({ error: 'Query parameter required' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkboxes = await dailyService.searchCheckboxes(query, limit);
|
||||||
|
return NextResponse.json(checkboxes);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vue daily pour une date donnée (ou aujourd'hui par défaut)
|
||||||
|
const targetDate = date ? new Date(date) : new Date();
|
||||||
|
|
||||||
|
if (date && isNaN(targetDate.getTime())) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dailyView = await dailyService.getDailyView(targetDate);
|
||||||
|
return NextResponse.json(dailyView);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la récupération du daily:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erreur interne du serveur' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API route pour ajouter une checkbox
|
||||||
|
*/
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Validation des données
|
||||||
|
if (!body.date || !body.text) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Date et text sont requis' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(body.date);
|
||||||
|
if (isNaN(date.getTime())) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Format de date invalide. Utilisez YYYY-MM-DD' },
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkbox = await dailyService.addCheckbox({
|
||||||
|
date,
|
||||||
|
text: body.text,
|
||||||
|
taskId: body.taskId,
|
||||||
|
order: body.order,
|
||||||
|
isChecked: body.isChecked
|
||||||
|
});
|
||||||
|
|
||||||
|
return NextResponse.json(checkbox, { status: 201 });
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de l\'ajout de la checkbox:', error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: 'Erreur interne du serveur' },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
396
src/app/daily/DailyPageClient.tsx
Normal file
396
src/app/daily/DailyPageClient.tsx
Normal file
@@ -0,0 +1,396 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useRef } from 'react';
|
||||||
|
import React from 'react';
|
||||||
|
import { useDaily } from '@/hooks/useDaily';
|
||||||
|
import { DailyCheckbox } from '@/lib/types';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
import { Card } from '@/components/ui/Card';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { formatDistanceToNow } from 'date-fns';
|
||||||
|
import { fr } from 'date-fns/locale';
|
||||||
|
|
||||||
|
interface DailySectionProps {
|
||||||
|
title: string;
|
||||||
|
date: Date;
|
||||||
|
checkboxes: DailyCheckbox[];
|
||||||
|
onAddCheckbox: (text: string) => Promise<void>;
|
||||||
|
onToggleCheckbox: (checkboxId: string) => Promise<void>;
|
||||||
|
onUpdateCheckbox: (checkboxId: string, text: string) => Promise<void>;
|
||||||
|
onDeleteCheckbox: (checkboxId: string) => Promise<void>;
|
||||||
|
saving: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function DailySectionComponent({
|
||||||
|
title,
|
||||||
|
date,
|
||||||
|
checkboxes,
|
||||||
|
onAddCheckbox,
|
||||||
|
onToggleCheckbox,
|
||||||
|
onUpdateCheckbox,
|
||||||
|
onDeleteCheckbox,
|
||||||
|
saving
|
||||||
|
}: DailySectionProps) {
|
||||||
|
const [newCheckboxText, setNewCheckboxText] = useState('');
|
||||||
|
const [addingCheckbox, setAddingCheckbox] = useState(false);
|
||||||
|
const [editingCheckboxId, setEditingCheckboxId] = useState<string | null>(null);
|
||||||
|
const [editingText, setEditingText] = useState('');
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const formatShortDate = (date: Date) => {
|
||||||
|
return date.toLocaleDateString('fr-FR', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddCheckbox = async () => {
|
||||||
|
if (!newCheckboxText.trim()) return;
|
||||||
|
|
||||||
|
setAddingCheckbox(true);
|
||||||
|
try {
|
||||||
|
await onAddCheckbox(newCheckboxText.trim());
|
||||||
|
setNewCheckboxText('');
|
||||||
|
// Garder le focus sur l'input pour enchainer les entrées
|
||||||
|
setTimeout(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
} finally {
|
||||||
|
setAddingCheckbox(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStartEdit = (checkbox: DailyCheckbox) => {
|
||||||
|
setEditingCheckboxId(checkbox.id);
|
||||||
|
setEditingText(checkbox.text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveEdit = async () => {
|
||||||
|
if (!editingCheckboxId || !editingText.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onUpdateCheckbox(editingCheckboxId, editingText.trim());
|
||||||
|
setEditingCheckboxId(null);
|
||||||
|
setEditingText('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la modification:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelEdit = () => {
|
||||||
|
setEditingCheckboxId(null);
|
||||||
|
setEditingText('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSaveEdit();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCancelEdit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddCheckbox();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h2 className="text-lg font-bold text-[var(--foreground)] font-mono">
|
||||||
|
{title} <span className="text-sm font-normal text-[var(--muted-foreground)]">({formatShortDate(date)})</span>
|
||||||
|
</h2>
|
||||||
|
<span className="text-xs text-[var(--muted-foreground)] font-mono">
|
||||||
|
{checkboxes.filter(cb => cb.isChecked).length}/{checkboxes.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Liste des checkboxes */}
|
||||||
|
<div className="space-y-2 mb-4">
|
||||||
|
{checkboxes.map((checkbox) => (
|
||||||
|
<div
|
||||||
|
key={checkbox.id}
|
||||||
|
className="flex items-center gap-3 p-2 rounded border border-[var(--border)]/30 hover:border-[var(--border)] transition-colors group"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={checkbox.isChecked}
|
||||||
|
onChange={() => onToggleCheckbox(checkbox.id)}
|
||||||
|
disabled={saving}
|
||||||
|
className="w-4 h-4 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{editingCheckboxId === checkbox.id ? (
|
||||||
|
<Input
|
||||||
|
value={editingText}
|
||||||
|
onChange={(e) => setEditingText(e.target.value)}
|
||||||
|
onKeyDown={handleEditKeyPress}
|
||||||
|
onBlur={handleSaveEdit}
|
||||||
|
autoFocus
|
||||||
|
className="flex-1 h-8 text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span
|
||||||
|
className={`flex-1 text-sm font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 p-1 rounded ${
|
||||||
|
checkbox.isChecked
|
||||||
|
? 'line-through text-[var(--muted-foreground)]'
|
||||||
|
: 'text-[var(--foreground)]'
|
||||||
|
}`}
|
||||||
|
onClick={() => handleStartEdit(checkbox)}
|
||||||
|
>
|
||||||
|
{checkbox.text}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lien vers la tâche si liée */}
|
||||||
|
{checkbox.task && (
|
||||||
|
<Link
|
||||||
|
href={`/?highlight=${checkbox.task.id}`}
|
||||||
|
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono"
|
||||||
|
title={`Tâche: ${checkbox.task.title}`}
|
||||||
|
>
|
||||||
|
#{checkbox.task.id.slice(-6)}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bouton de suppression */}
|
||||||
|
<button
|
||||||
|
onClick={() => onDeleteCheckbox(checkbox.id)}
|
||||||
|
disabled={saving}
|
||||||
|
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-full bg-[var(--destructive)]/20 hover:bg-[var(--destructive)]/30 border border-[var(--destructive)]/30 hover:border-[var(--destructive)]/50 flex items-center justify-center transition-all duration-200 text-[var(--destructive)] text-xs"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{checkboxes.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm font-mono">
|
||||||
|
Aucune tâche pour cette période
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Formulaire d'ajout */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={`Ajouter une tâche...`}
|
||||||
|
value={newCheckboxText}
|
||||||
|
onChange={(e) => setNewCheckboxText(e.target.value)}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
disabled={addingCheckbox || saving}
|
||||||
|
className="flex-1 min-w-[300px]"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddCheckbox}
|
||||||
|
disabled={!newCheckboxText.trim() || addingCheckbox || saving}
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="min-w-[40px]"
|
||||||
|
>
|
||||||
|
{addingCheckbox ? '...' : '+'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DailyPageClient() {
|
||||||
|
const {
|
||||||
|
dailyView,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
|
saving,
|
||||||
|
currentDate,
|
||||||
|
addTodayCheckbox,
|
||||||
|
addYesterdayCheckbox,
|
||||||
|
toggleCheckbox,
|
||||||
|
updateCheckbox,
|
||||||
|
deleteCheckbox,
|
||||||
|
goToPreviousDay,
|
||||||
|
goToNextDay,
|
||||||
|
goToToday
|
||||||
|
} = useDaily();
|
||||||
|
|
||||||
|
const handleAddTodayCheckbox = async (text: string) => {
|
||||||
|
await addTodayCheckbox(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddYesterdayCheckbox = async (text: string) => {
|
||||||
|
await addYesterdayCheckbox(text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleCheckbox = async (checkboxId: string) => {
|
||||||
|
await toggleCheckbox(checkboxId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteCheckbox = async (checkboxId: string) => {
|
||||||
|
await deleteCheckbox(checkboxId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateCheckbox = async (checkboxId: string, text: string) => {
|
||||||
|
await updateCheckbox(checkboxId, { text });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getYesterdayDate = () => {
|
||||||
|
const yesterday = new Date(currentDate);
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
|
return yesterday;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTodayDate = () => {
|
||||||
|
return currentDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatCurrentDate = () => {
|
||||||
|
return currentDate.toLocaleDateString('fr-FR', {
|
||||||
|
weekday: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isToday = () => {
|
||||||
|
const today = new Date();
|
||||||
|
return currentDate.toDateString() === today.toDateString();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="flex items-center justify-center min-h-[200px]">
|
||||||
|
<div className="text-[var(--muted-foreground)] font-mono">
|
||||||
|
Chargement du daily...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
<div className="bg-[var(--destructive)]/10 border border-[var(--destructive)]/20 rounded-lg p-4 text-center">
|
||||||
|
<p className="text-[var(--destructive)] font-mono mb-4">
|
||||||
|
Erreur: {error}
|
||||||
|
</p>
|
||||||
|
<Button onClick={() => window.location.reload()} variant="primary">
|
||||||
|
Réessayer
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
|
{/* Header */}
|
||||||
|
<header className="bg-[var(--card)]/80 backdrop-blur-sm border-b border-[var(--border)]/50 sticky top-0 z-10">
|
||||||
|
<div className="container mx-auto px-4 py-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono text-sm"
|
||||||
|
>
|
||||||
|
← Kanban
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-xl font-bold text-[var(--foreground)] font-mono">
|
||||||
|
📝 Daily
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={goToPreviousDay}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="text-center min-w-[200px]">
|
||||||
|
<div className="text-sm font-bold text-[var(--foreground)] font-mono">
|
||||||
|
{formatCurrentDate()}
|
||||||
|
</div>
|
||||||
|
{!isToday() && (
|
||||||
|
<button
|
||||||
|
onClick={goToToday}
|
||||||
|
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono"
|
||||||
|
>
|
||||||
|
Aller à aujourd'hui
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={goToNextDay}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Contenu principal */}
|
||||||
|
<main className="container mx-auto px-4 py-8">
|
||||||
|
{dailyView && (
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
{/* Section Hier */}
|
||||||
|
<DailySectionComponent
|
||||||
|
title="📋 Hier"
|
||||||
|
date={getYesterdayDate()}
|
||||||
|
checkboxes={dailyView.yesterday}
|
||||||
|
onAddCheckbox={handleAddYesterdayCheckbox}
|
||||||
|
onToggleCheckbox={handleToggleCheckbox}
|
||||||
|
onUpdateCheckbox={handleUpdateCheckbox}
|
||||||
|
onDeleteCheckbox={handleDeleteCheckbox}
|
||||||
|
saving={saving}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Section Aujourd'hui */}
|
||||||
|
<DailySectionComponent
|
||||||
|
title="🎯 Aujourd'hui"
|
||||||
|
date={getTodayDate()}
|
||||||
|
checkboxes={dailyView.today}
|
||||||
|
onAddCheckbox={handleAddTodayCheckbox}
|
||||||
|
onToggleCheckbox={handleToggleCheckbox}
|
||||||
|
onUpdateCheckbox={handleUpdateCheckbox}
|
||||||
|
onDeleteCheckbox={handleDeleteCheckbox}
|
||||||
|
saving={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Footer avec stats */}
|
||||||
|
{dailyView && (
|
||||||
|
<Card className="mt-8 p-4">
|
||||||
|
<div className="text-center text-sm text-[var(--muted-foreground)] font-mono">
|
||||||
|
Daily pour {formatCurrentDate()}
|
||||||
|
{' • '}
|
||||||
|
{dailyView.yesterday.length + dailyView.today.length} tâche{dailyView.yesterday.length + dailyView.today.length > 1 ? 's' : ''} au total
|
||||||
|
{' • '}
|
||||||
|
{dailyView.yesterday.filter(cb => cb.isChecked).length + dailyView.today.filter(cb => cb.isChecked).length} complétée{(dailyView.yesterday.filter(cb => cb.isChecked).length + dailyView.today.filter(cb => cb.isChecked).length) > 1 ? 's' : ''}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
11
src/app/daily/page.tsx
Normal file
11
src/app/daily/page.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
import { DailyPageClient } from './DailyPageClient';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Daily - Tower Control',
|
||||||
|
description: 'Gestion quotidienne des tâches et objectifs',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DailyPage() {
|
||||||
|
return <DailyPageClient />;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user