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:
Julien Froidefond
2025-09-15 18:04:46 +02:00
parent 74ef79eb70
commit cf2e360ce9
14 changed files with 1423 additions and 10 deletions

18
TODO.md
View File

@@ -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

153
clients/daily-client.ts Normal file
View 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();

View File

@@ -43,6 +43,12 @@ export function Header({ title, subtitle, stats, syncing = false }: HeaderProps)
>
Kanban
</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
href="/tags"
className="text-[var(--muted-foreground)] hover:text-[var(--accent)] transition-colors font-mono text-sm uppercase tracking-wider"

290
hooks/useDaily.ts Normal file
View 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
};
}

View File

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

View File

@@ -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");

View File

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

View File

@@ -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")
}

244
services/daily.ts Normal file
View 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();

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

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

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

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