From cf2e360ce9d3e5744d1cc640b18c33570a120fc1 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Mon, 15 Sep 2025 18:04:46 +0200 Subject: [PATCH] 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. --- TODO.md | 18 +- clients/daily-client.ts | 153 +++++++ components/ui/Header.tsx | 6 + hooks/useDaily.ts | 290 +++++++++++++ lib/types.ts | 36 ++ .../migration.sql | 25 ++ .../migration.sql | 37 ++ prisma/schema.prisma | 20 +- services/daily.ts | 244 +++++++++++ src/app/api/daily/checkboxes/[id]/route.ts | 63 +++ src/app/api/daily/checkboxes/route.ts | 38 ++ src/app/api/daily/route.ts | 96 +++++ src/app/daily/DailyPageClient.tsx | 396 ++++++++++++++++++ src/app/daily/page.tsx | 11 + 14 files changed, 1423 insertions(+), 10 deletions(-) create mode 100644 clients/daily-client.ts create mode 100644 hooks/useDaily.ts create mode 100644 prisma/migrations/20250915153302_add_daily_management/migration.sql create mode 100644 prisma/migrations/20250915154752_simplify_daily_model/migration.sql create mode 100644 services/daily.ts create mode 100644 src/app/api/daily/checkboxes/[id]/route.ts create mode 100644 src/app/api/daily/checkboxes/route.ts create mode 100644 src/app/api/daily/route.ts create mode 100644 src/app/daily/DailyPageClient.tsx create mode 100644 src/app/daily/page.tsx diff --git a/TODO.md b/TODO.md index 6107d26..dda01c3 100644 --- a/TODO.md +++ b/TODO.md @@ -99,16 +99,16 @@ ## 📊 Phase 3: IntĂ©grations et analytics (PrioritĂ© 3) ### 3.1 Gestion du Daily -- [ ] CrĂ©er `services/daily.ts` - Service de gestion des daily notes -- [ ] ModĂšle de donnĂ©es Daily (date, checkboxes hier/aujourd'hui) -- [ ] Interface Daily avec sections "Hier" et "Aujourd'hui" -- [ ] Checkboxes interactives avec Ă©tat cochĂ©/non-cochĂ© -- [ ] Liaison optionnelle checkbox ↔ tĂąche existante -- [ ] Cocher une checkbox NE change PAS le statut de la tĂąche liĂ©e -- [ ] Navigation par date (daily prĂ©cĂ©dent/suivant) -- [ ] Auto-crĂ©ation du daily du jour si inexistant +- [x] CrĂ©er `services/daily.ts` - Service de gestion des daily notes +- [x] ModĂšle de donnĂ©es Daily (date, checkboxes hier/aujourd'hui) +- [x] Interface Daily avec sections "Hier" et "Aujourd'hui" +- [x] Checkboxes interactives avec Ă©tat cochĂ©/non-cochĂ© +- [x] Liaison optionnelle checkbox ↔ tĂąche existante +- [x] Cocher une checkbox NE change PAS le statut de la tĂąche liĂ©e +- [x] Navigation par date (daily prĂ©cĂ©dent/suivant) +- [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 -- [ ] Export/import depuis Confluence (optionnel) - [ ] Templates de daily personnalisables - [ ] Recherche dans l'historique des dailies diff --git a/clients/daily-client.ts b/clients/daily-client.ts new file mode 100644 index 0000000..708fa34 --- /dev/null +++ b/clients/daily-client.ts @@ -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 { + return httpClient.get('/daily'); + } + + /** + * RĂ©cupĂšre la vue daily pour une date donnĂ©e + */ + async getDailyView(date: Date): Promise { + 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 { + 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 { + return httpClient.post('/daily', { + ...data, + date: this.formatDateForAPI(data.date) + }); + } + + /** + * Ajoute une checkbox pour aujourd'hui + */ + async addTodayCheckbox(text: string, taskId?: string): Promise { + return this.addCheckbox({ + date: new Date(), + text, + taskId + }); + } + + /** + * Ajoute une checkbox pour hier + */ + async addYesterdayCheckbox(text: string, taskId?: string): Promise { + 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 { + return httpClient.patch(`/daily/checkboxes/${checkboxId}`, data); + } + + /** + * Supprime une checkbox + */ + async deleteCheckbox(checkboxId: string): Promise { + return httpClient.delete(`/daily/checkboxes/${checkboxId}`); + } + + /** + * RĂ©ordonne les checkboxes d'une date + */ + async reorderCheckboxes(data: ReorderCheckboxesData): Promise { + 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 { + 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 { + 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(); \ No newline at end of file diff --git a/components/ui/Header.tsx b/components/ui/Header.tsx index 1818bf6..177826c 100644 --- a/components/ui/Header.tsx +++ b/components/ui/Header.tsx @@ -43,6 +43,12 @@ export function Header({ title, subtitle, stats, syncing = false }: HeaderProps) > Kanban + + Daily + Promise; + addTodayCheckbox: (text: string, taskId?: string) => Promise; + addYesterdayCheckbox: (text: string, taskId?: string) => Promise; + updateCheckbox: (checkboxId: string, data: UpdateDailyCheckboxData) => Promise; + deleteCheckbox: (checkboxId: string) => Promise; + toggleCheckbox: (checkboxId: string) => Promise; + reorderCheckboxes: (data: ReorderCheckboxesData) => Promise; + goToPreviousDay: () => Promise; + goToNextDay: () => Promise; + goToToday: () => Promise; + setDate: (date: Date) => Promise; +} + +/** + * Hook pour la gestion d'une vue daily spĂ©cifique + */ +export function useDaily(initialDate?: Date): UseDailyState & UseDailyActions & { currentDate: Date } { + const [currentDate, setCurrentDate] = useState(initialDate || new Date()); + const [dailyView, setDailyView] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + const previousDay = new Date(currentDate); + previousDay.setDate(previousDay.getDate() - 1); + setCurrentDate(previousDay); + }, [currentDate]); + + const goToNextDay = useCallback(async (): Promise => { + const nextDay = new Date(currentDate); + nextDay.setDate(nextDay.getDate() + 1); + setCurrentDate(nextDay); + }, [currentDate]); + + const goToToday = useCallback(async (): Promise => { + setCurrentDate(new Date()); + }, []); + + const setDate = useCallback(async (date: Date): Promise => { + 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(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 + }; +} \ No newline at end of file diff --git a/lib/types.ts b/lib/types.ts index d7f28c9..9e9630c 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -162,3 +162,39 @@ export class ValidationError extends Error { 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 +} diff --git a/prisma/migrations/20250915153302_add_daily_management/migration.sql b/prisma/migrations/20250915153302_add_daily_management/migration.sql new file mode 100644 index 0000000..f2ce1b2 --- /dev/null +++ b/prisma/migrations/20250915153302_add_daily_management/migration.sql @@ -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"); diff --git a/prisma/migrations/20250915154752_simplify_daily_model/migration.sql b/prisma/migrations/20250915154752_simplify_daily_model/migration.sql new file mode 100644 index 0000000..62a1eb1 --- /dev/null +++ b/prisma/migrations/20250915154752_simplify_daily_model/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 492de22..41269a1 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -29,7 +29,8 @@ model Task { assignee String? // Relations - taskTags TaskTag[] + taskTags TaskTag[] + dailyCheckboxes DailyCheckbox[] @@unique([source, sourceId]) @@map("tasks") @@ -65,3 +66,20 @@ model SyncLog { @@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") +} diff --git a/services/daily.ts b/services/daily.ts new file mode 100644 index 0000000..4dc66a2 --- /dev/null +++ b/services/daily.ts @@ -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 { + // 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 { + // 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 { + // 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 { + 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 { + 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 { + // 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 { + 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 { + return this.getDailyView(new Date()); + } + + /** + * Ajoute une checkbox pour aujourd'hui + */ + async addTodayCheckbox(text: string, taskId?: string): Promise { + return this.addCheckbox({ + date: new Date(), + text, + taskId + }); + } + + /** + * Ajoute une checkbox pour hier + */ + async addYesterdayCheckbox(text: string, taskId?: string): Promise { + 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(); \ No newline at end of file diff --git a/src/app/api/daily/checkboxes/[id]/route.ts b/src/app/api/daily/checkboxes/[id]/route.ts new file mode 100644 index 0000000..6e7961b --- /dev/null +++ b/src/app/api/daily/checkboxes/[id]/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/daily/checkboxes/route.ts b/src/app/api/daily/checkboxes/route.ts new file mode 100644 index 0000000..eee0444 --- /dev/null +++ b/src/app/api/daily/checkboxes/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/src/app/api/daily/route.ts b/src/app/api/daily/route.ts new file mode 100644 index 0000000..9cacabd --- /dev/null +++ b/src/app/api/daily/route.ts @@ -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 } + ); + } +} \ No newline at end of file diff --git a/src/app/daily/DailyPageClient.tsx b/src/app/daily/DailyPageClient.tsx new file mode 100644 index 0000000..1ac61b2 --- /dev/null +++ b/src/app/daily/DailyPageClient.tsx @@ -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; + onToggleCheckbox: (checkboxId: string) => Promise; + onUpdateCheckbox: (checkboxId: string, text: string) => Promise; + onDeleteCheckbox: (checkboxId: string) => Promise; + 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(null); + const [editingText, setEditingText] = useState(''); + const inputRef = useRef(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 ( + +
+

+ {title} ({formatShortDate(date)}) +

+ + {checkboxes.filter(cb => cb.isChecked).length}/{checkboxes.length} + +
+ + {/* Liste des checkboxes */} +
+ {checkboxes.map((checkbox) => ( +
+ 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 ? ( + setEditingText(e.target.value)} + onKeyDown={handleEditKeyPress} + onBlur={handleSaveEdit} + autoFocus + className="flex-1 h-8 text-sm" + /> + ) : ( + handleStartEdit(checkbox)} + > + {checkbox.text} + + )} + + {/* Lien vers la tùche si liée */} + {checkbox.task && ( + + #{checkbox.task.id.slice(-6)} + + )} + + {/* Bouton de suppression */} + +
+ ))} + + {checkboxes.length === 0 && ( +
+ Aucune tùche pour cette période +
+ )} +
+ + {/* Formulaire d'ajout */} +
+ setNewCheckboxText(e.target.value)} + onKeyDown={handleKeyPress} + disabled={addingCheckbox || saving} + className="flex-1 min-w-[300px]" + /> + +
+
+ ); +} + +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 ( +
+
+
+ Chargement du daily... +
+
+
+ ); + } + + if (error) { + return ( +
+
+

+ Erreur: {error} +

+ +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+
+ + ← Kanban + +

+ 📝 Daily +

+
+ +
+ + +
+
+ {formatCurrentDate()} +
+ {!isToday() && ( + + )} +
+ + +
+
+
+
+ + {/* Contenu principal */} +
+ {dailyView && ( +
+ {/* Section Hier */} + + + {/* Section Aujourd'hui */} + +
+ )} + + {/* Footer avec stats */} + {dailyView && ( + +
+ 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' : ''} +
+
+ )} +
+
+ ); +} \ No newline at end of file diff --git a/src/app/daily/page.tsx b/src/app/daily/page.tsx new file mode 100644 index 0000000..f778794 --- /dev/null +++ b/src/app/daily/page.tsx @@ -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 ; +}