feat: enhance DailyCheckbox model and service for type management

- Added `DailyCheckboxType` to define checkbox types ('task' | 'meeting') in TypeScript.
- Updated `DailyCheckbox` model in Prisma schema to include `type` field with a default value of 'task'.
- Modified `DailyService` to handle checkbox type during creation and updates.
- Adjusted API route to accept checkbox type in requests.
- Refactored `DailyPageClient` to support type management in checkbox operations.
This commit is contained in:
Julien Froidefond
2025-09-15 22:16:34 +02:00
parent 08d344652f
commit adfef551ab
11 changed files with 744 additions and 211 deletions

View File

@@ -0,0 +1,108 @@
'use client';
import { useState, useRef } from 'react';
import { DailyCheckboxType } from '@/lib/types';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
interface DailyAddFormProps {
onAdd: (text: string, type: DailyCheckboxType) => Promise<void>;
disabled?: boolean;
placeholder?: string;
}
export function DailyAddForm({ onAdd, disabled = false, placeholder = "Ajouter une tâche..." }: DailyAddFormProps) {
const [newCheckboxText, setNewCheckboxText] = useState('');
const [selectedType, setSelectedType] = useState<DailyCheckboxType>('task');
const [addingCheckbox, setAddingCheckbox] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const handleAddCheckbox = async () => {
if (!newCheckboxText.trim()) return;
setAddingCheckbox(true);
try {
await onAdd(newCheckboxText.trim(), selectedType); // Pas de taskId lors de l'ajout
setNewCheckboxText('');
// Garder le type sélectionné pour enchaîner les créations du même type
// setSelectedType('task'); // <- Supprimé pour garder la sélection
// Garder le focus sur l'input pour enchainer les entrées
setTimeout(() => {
inputRef.current?.focus();
}, 100);
} finally {
setAddingCheckbox(false);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddCheckbox();
}
};
const getPlaceholder = () => {
if (placeholder !== "Ajouter une tâche...") return placeholder;
return selectedType === 'meeting' ? 'Ajouter une réunion...' : 'Ajouter une tâche...';
};
return (
<div className="space-y-2">
{/* Sélecteur de type */}
<div className="flex gap-2">
<Button
type="button"
onClick={() => setSelectedType('task')}
variant="ghost"
size="sm"
className={`flex items-center gap-1 text-xs border-l-4 ${
selectedType === 'task'
? 'border-l-green-500 bg-green-100 dark:bg-green-900/40 text-green-900 dark:text-green-100 font-medium'
: 'border-l-green-300 hover:border-l-green-400 opacity-70 hover:opacity-90'
}`}
disabled={addingCheckbox || disabled}
>
Tâche
</Button>
<Button
type="button"
onClick={() => setSelectedType('meeting')}
variant="ghost"
size="sm"
className={`flex items-center gap-1 text-xs border-l-4 ${
selectedType === 'meeting'
? 'border-l-blue-500 bg-blue-100 dark:bg-blue-900/40 text-blue-900 dark:text-blue-100 font-medium'
: 'border-l-blue-300 hover:border-l-blue-400 opacity-70 hover:opacity-90'
}`}
disabled={addingCheckbox || disabled}
>
🗓 Réunion
</Button>
</div>
{/* Champ de saisie et options */}
<div className="flex gap-2">
<Input
ref={inputRef}
type="text"
placeholder={getPlaceholder()}
value={newCheckboxText}
onChange={(e) => setNewCheckboxText(e.target.value)}
onKeyDown={handleKeyPress}
disabled={addingCheckbox || disabled}
className="flex-1 min-w-[300px]"
/>
<Button
onClick={handleAddCheckbox}
disabled={!newCheckboxText.trim() || addingCheckbox || disabled}
variant="primary"
size="sm"
className="min-w-[40px]"
>
{addingCheckbox ? '...' : '+'}
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,162 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
import { Input } from '@/components/ui/Input';
import { EditCheckboxModal } from './EditCheckboxModal';
interface DailyCheckboxItemProps {
checkbox: DailyCheckbox;
onToggle: (checkboxId: string) => Promise<void>;
onUpdate: (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => Promise<void>;
onDelete: (checkboxId: string) => Promise<void>;
saving?: boolean;
}
export function DailyCheckboxItem({
checkbox,
onToggle,
onUpdate,
onDelete,
saving = false
}: DailyCheckboxItemProps) {
const [inlineEditingId, setInlineEditingId] = useState<string | null>(null);
const [inlineEditingText, setInlineEditingText] = useState('');
const [editingCheckbox, setEditingCheckbox] = useState<DailyCheckbox | null>(null);
// Édition inline simple
const handleStartInlineEdit = () => {
setInlineEditingId(checkbox.id);
setInlineEditingText(checkbox.text);
};
const handleSaveInlineEdit = async () => {
if (!inlineEditingText.trim()) return;
try {
await onUpdate(checkbox.id, inlineEditingText.trim(), checkbox.type, checkbox.taskId);
setInlineEditingId(null);
setInlineEditingText('');
} catch (error) {
console.error('Erreur lors de la modification:', error);
}
};
const handleCancelInlineEdit = () => {
setInlineEditingId(null);
setInlineEditingText('');
};
const handleInlineEditKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSaveInlineEdit();
} else if (e.key === 'Escape') {
e.preventDefault();
handleCancelInlineEdit();
}
};
// Modal d'édition avancée
const handleStartAdvancedEdit = () => {
setEditingCheckbox(checkbox);
};
const handleSaveAdvancedEdit = async (text: string, type: DailyCheckboxType, taskId?: string) => {
await onUpdate(checkbox.id, text, type, taskId);
setEditingCheckbox(null);
};
const handleCloseAdvancedEdit = () => {
setEditingCheckbox(null);
};
return (
<>
<div className={`flex items-center gap-3 p-2 rounded border transition-colors group ${
checkbox.type === 'meeting'
? 'border-l-4 border-l-blue-500 border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)]'
: 'border-l-4 border-l-green-500 border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)]'
}`}>
{/* Checkbox */}
<input
type="checkbox"
checked={checkbox.isChecked}
onChange={() => onToggle(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"
/>
{/* Contenu principal */}
{inlineEditingId === checkbox.id ? (
<Input
value={inlineEditingText}
onChange={(e) => setInlineEditingText(e.target.value)}
onKeyDown={handleInlineEditKeyPress}
onBlur={handleSaveInlineEdit}
autoFocus
className="flex-1 h-8 text-sm"
/>
) : (
<div className="flex-1 flex items-center gap-2">
{/* Texte cliquable pour édition inline */}
<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={handleStartInlineEdit}
title="Cliquer pour éditer le texte"
>
{checkbox.text}
</span>
{/* Icône d'édition avancée */}
<button
onClick={handleStartAdvancedEdit}
disabled={saving}
className="opacity-0 group-hover:opacity-100 w-6 h-6 rounded-full bg-[var(--muted)]/50 hover:bg-[var(--muted)] border border-[var(--border)]/30 hover:border-[var(--border)] flex items-center justify-center transition-all duration-200 text-[var(--foreground)] text-xs"
title="Éditer les options (type, liaison tâche)"
>
</button>
</div>
)}
{/* 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={() => onDelete(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>
{/* Modal d'édition avancée */}
{editingCheckbox && (
<EditCheckboxModal
checkbox={editingCheckbox}
isOpen={true}
onClose={handleCloseAdvancedEdit}
onSave={handleSaveAdvancedEdit}
saving={saving}
/>
)}
</>
);
}

View File

@@ -0,0 +1,87 @@
'use client';
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
import { Card } from '@/components/ui/Card';
import { DailyCheckboxItem } from './DailyCheckboxItem';
import { DailyAddForm } from './DailyAddForm';
interface DailySectionProps {
title: string;
date: Date;
checkboxes: DailyCheckbox[];
onAddCheckbox: (text: string, type: DailyCheckboxType) => Promise<void>;
onToggleCheckbox: (checkboxId: string) => Promise<void>;
onUpdateCheckbox: (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => Promise<void>;
onDeleteCheckbox: (checkboxId: string) => Promise<void>;
saving: boolean;
refreshing?: boolean;
}
export function DailySection({
title,
date,
checkboxes,
onAddCheckbox,
onToggleCheckbox,
onUpdateCheckbox,
onDeleteCheckbox,
saving,
refreshing = false
}: DailySectionProps) {
const formatShortDate = (date: Date) => {
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
return (
<Card className="p-0 flex flex-col h-[600px]">
{/* Header */}
<div className="p-4 pb-0">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-bold text-[var(--foreground)] font-mono flex items-center gap-2">
{title} <span className="text-sm font-normal text-[var(--muted-foreground)]">({formatShortDate(date)})</span>
{refreshing && (
<div className="w-4 h-4 border-2 border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div>
)}
</h2>
<span className="text-xs text-[var(--muted-foreground)] font-mono">
{checkboxes.filter(cb => cb.isChecked).length}/{checkboxes.length}
</span>
</div>
</div>
{/* Liste des checkboxes - zone scrollable */}
<div className="flex-1 px-4 overflow-y-auto min-h-0">
<div className="space-y-2 pb-4">
{checkboxes.map((checkbox) => (
<DailyCheckboxItem
key={checkbox.id}
checkbox={checkbox}
onToggle={onToggleCheckbox}
onUpdate={onUpdateCheckbox}
onDelete={onDeleteCheckbox}
saving={saving}
/>
))}
{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>
</div>
{/* Footer - Formulaire d'ajout toujours en bas */}
<div className="p-4 pt-2 border-t border-[var(--border)]/30 bg-[var(--card)]/50">
<DailyAddForm
onAdd={onAddCheckbox}
disabled={saving}
/>
</div>
</Card>
);
}

View File

@@ -0,0 +1,154 @@
'use client';
import { useState } from 'react';
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
import { Button } from '@/components/ui/Button';
import { Input } from '@/components/ui/Input';
import { Modal } from '@/components/ui/Modal';
import { TaskSelector } from './TaskSelector';
interface EditCheckboxModalProps {
checkbox: DailyCheckbox;
isOpen: boolean;
onClose: () => void;
onSave: (text: string, type: DailyCheckboxType, taskId?: string) => Promise<void>;
saving?: boolean;
}
export function EditCheckboxModal({
checkbox,
isOpen,
onClose,
onSave,
saving = false
}: EditCheckboxModalProps) {
const [text, setText] = useState(checkbox.text);
const [type, setType] = useState<DailyCheckboxType>(checkbox.type);
const [taskId, setTaskId] = useState<string | undefined>(checkbox.taskId);
const handleSave = async () => {
if (!text.trim()) return;
try {
await onSave(text.trim(), type, taskId);
onClose();
} catch (error) {
console.error('Erreur lors de la sauvegarde:', error);
}
};
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSave();
}
};
const resetForm = () => {
setText(checkbox.text);
setType(checkbox.type);
setTaskId(checkbox.taskId);
};
const handleClose = () => {
resetForm();
onClose();
};
return (
<Modal isOpen={isOpen} onClose={handleClose} title="Modifier la tâche">
<div className="space-y-4">
{/* Texte */}
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Description
</label>
<Input
value={text}
onChange={(e) => setText(e.target.value)}
onKeyDown={handleKeyPress}
placeholder="Description de la tâche..."
className="w-full"
autoFocus
/>
</div>
{/* Type */}
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Type
</label>
<div className="flex gap-2">
<Button
type="button"
onClick={() => setType('task')}
variant="ghost"
size="sm"
className={`flex items-center gap-2 border-l-4 ${
type === 'task'
? 'border-l-green-500 bg-green-100 dark:bg-green-900/40 text-green-900 dark:text-green-100 font-medium'
: 'border-l-green-300 hover:border-l-green-400 opacity-70 hover:opacity-90'
}`}
>
Tâche
</Button>
<Button
type="button"
onClick={() => setType('meeting')}
variant="ghost"
size="sm"
className={`flex items-center gap-2 border-l-4 ${
type === 'meeting'
? 'border-l-blue-500 bg-blue-100 dark:bg-blue-900/40 text-blue-900 dark:text-blue-100 font-medium'
: 'border-l-blue-300 hover:border-l-blue-400 opacity-70 hover:opacity-90'
}`}
>
🗓 Réunion
</Button>
</div>
</div>
{/* Liaison tâche (seulement pour les tâches) */}
{type === 'task' && (
<div>
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
Lier à une tâche (optionnel)
</label>
<div className="border border-[var(--border)] rounded-lg p-3">
<TaskSelector
selectedTaskId={taskId}
onTaskSelect={setTaskId}
disabled={saving}
/>
{taskId && (
<div className="mt-2 text-xs text-[var(--muted-foreground)]">
Tâche liée : #{taskId.slice(-6)}
</div>
)}
</div>
</div>
)}
{/* Actions */}
<div className="flex gap-2 justify-end pt-4">
<Button
type="button"
onClick={handleClose}
variant="ghost"
disabled={saving}
>
Annuler
</Button>
<Button
type="button"
onClick={handleSave}
variant="primary"
disabled={!text.trim() || saving}
>
{saving ? 'Sauvegarde...' : 'Sauvegarder'}
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,183 @@
'use client';
import { useState, useEffect } from 'react';
import { Task } from '@/lib/types';
import { tasksClient } from '@/clients/tasks-client';
import { Button } from '@/components/ui/Button';
interface TaskSelectorProps {
selectedTaskId?: string;
onTaskSelect: (taskId: string | undefined) => void;
disabled?: boolean;
}
export function TaskSelector({ selectedTaskId, onTaskSelect, disabled }: TaskSelectorProps) {
const [tasks, setTasks] = useState<Task[]>([]);
const [loading, setLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [search, setSearch] = useState('');
const selectedTask = tasks.find(task => task.id === selectedTaskId);
useEffect(() => {
if (isOpen && tasks.length === 0) {
loadTasks();
}
}, [isOpen]);
const loadTasks = async () => {
setLoading(true);
try {
const response = await tasksClient.getTasks({
status: ['todo', 'in_progress', 'backlog'],
limit: 100
});
setTasks(response.data);
} catch (error) {
console.error('Erreur lors du chargement des tâches:', error);
} finally {
setLoading(false);
}
};
const filteredTasks = tasks.filter(task =>
task.title.toLowerCase().includes(search.toLowerCase()) ||
task.description?.toLowerCase().includes(search.toLowerCase())
);
const handleTaskSelect = (taskId: string) => {
onTaskSelect(taskId);
setIsOpen(false);
setSearch('');
};
const handleClear = () => {
onTaskSelect(undefined);
setIsOpen(false);
setSearch('');
};
if (!isOpen) {
return (
<div className="flex gap-1">
<Button
type="button"
onClick={() => setIsOpen(true)}
disabled={disabled}
variant="ghost"
size="sm"
className="text-xs px-2 py-1 h-6"
title="Lier à une tâche"
>
{selectedTask ? `#${selectedTask.id.slice(-6)}` : '🔗'}
</Button>
{selectedTask && (
<Button
type="button"
onClick={handleClear}
disabled={disabled}
variant="ghost"
size="sm"
className="text-xs px-1 py-1 h-6 text-[var(--destructive)]"
title="Délier"
>
×
</Button>
)}
</div>
);
}
return (
<div className="relative">
<div className="absolute bottom-full mb-2 right-0 bg-[var(--card)] border border-[var(--border)] rounded-lg shadow-lg z-10 min-w-[300px] max-w-[400px]">
<div className="p-3">
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-medium text-[var(--foreground)]">Lier à une tâche</h3>
<Button
type="button"
onClick={() => setIsOpen(false)}
variant="ghost"
size="sm"
className="text-xs px-1 py-1 h-6"
>
×
</Button>
</div>
<input
type="text"
placeholder="Rechercher une tâche..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="w-full mb-2 px-2 py-1 text-xs border border-[var(--border)] rounded bg-[var(--background)] text-[var(--foreground)]"
autoFocus
/>
<div className="max-h-32 overflow-y-auto space-y-1">
{loading ? (
<div className="text-xs text-[var(--muted-foreground)] text-center py-2">
Chargement...
</div>
) : filteredTasks.length === 0 ? (
<div className="text-xs text-[var(--muted-foreground)] text-center py-2">
Aucune tâche trouvée
</div>
) : (
filteredTasks.map((task) => (
<button
key={task.id}
type="button"
onClick={() => handleTaskSelect(task.id)}
className="w-full text-left p-2 rounded text-xs hover:bg-[var(--muted)] transition-colors"
>
<div className="font-medium text-[var(--foreground)] truncate">
{task.title}
</div>
{task.description && (
<div className="text-[var(--muted-foreground)] truncate">
{task.description}
</div>
)}
<div className="flex items-center gap-2 mt-1">
<span className={`px-1 py-0.5 rounded text-xs ${
task.status === 'todo' ? 'bg-blue-100 text-blue-800' :
task.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{task.status}
</span>
<span className="text-[var(--muted-foreground)]">
#{task.id.slice(-6)}
</span>
</div>
</button>
))
)}
</div>
<div className="flex gap-2 mt-2 pt-2 border-t border-[var(--border)]">
<Button
type="button"
onClick={handleClear}
variant="ghost"
size="sm"
className="text-xs flex-1"
>
Aucune tâche
</Button>
<Button
type="button"
onClick={() => setIsOpen(false)}
variant="ghost"
size="sm"
className="text-xs flex-1"
>
Annuler
</Button>
</div>
</div>
</div>
</div>
);
}

View File

@@ -164,11 +164,14 @@ export class ValidationError extends Error {
}
// Types pour les dailies
export type DailyCheckboxType = 'task' | 'meeting';
export interface DailyCheckbox {
id: string;
date: Date;
text: string;
isChecked: boolean;
type: DailyCheckboxType;
order: number;
taskId?: string;
task?: Task; // Relation optionnelle vers une tâche
@@ -180,6 +183,7 @@ export interface DailyCheckbox {
export interface CreateDailyCheckboxData {
date: Date;
text: string;
type?: DailyCheckboxType;
taskId?: string;
order?: number;
isChecked?: boolean;
@@ -188,6 +192,7 @@ export interface CreateDailyCheckboxData {
export interface UpdateDailyCheckboxData {
text?: string;
isChecked?: boolean;
type?: DailyCheckboxType;
taskId?: string;
order?: number;
}

View File

@@ -0,0 +1,21 @@
-- 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,
"type" TEXT NOT NULL DEFAULT 'task',
"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", "date", "id", "isChecked", "order", "taskId", "text", "updatedAt") SELECT "createdAt", "date", "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

@@ -72,6 +72,7 @@ model DailyCheckbox {
date DateTime // Date de la checkbox (YYYY-MM-DD)
text String // Texte de la checkbox
isChecked Boolean @default(false)
type String @default("task") // "task" | "meeting"
order Int @default(0) // Ordre d'affichage pour cette date
taskId String? // Liaison optionnelle vers une tâche
createdAt DateTime @default(now())

View File

@@ -1,5 +1,5 @@
import { prisma } from './database';
import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError } from '@/lib/types';
import { DailyCheckbox, DailyView, CreateDailyCheckboxData, UpdateDailyCheckboxData, BusinessError, DailyCheckboxType } from '@/lib/types';
/**
* Service pour la gestion des checkboxes daily
@@ -67,6 +67,7 @@ export class DailyService {
data: {
date: normalizedDate,
text: data.text.trim(),
type: data.type ?? 'task',
taskId: data.taskId,
order,
isChecked: data.isChecked ?? false
@@ -85,6 +86,7 @@ export class DailyService {
if (data.text !== undefined) updateData.text = data.text.trim();
if (data.isChecked !== undefined) updateData.isChecked = data.isChecked;
if (data.type !== undefined) updateData.type = data.type;
if (data.taskId !== undefined) updateData.taskId = data.taskId;
if (data.order !== undefined) updateData.order = data.order;
@@ -214,6 +216,7 @@ export class DailyService {
date: checkbox.date,
text: checkbox.text,
isChecked: checkbox.isChecked,
type: checkbox.type as DailyCheckboxType,
order: checkbox.order,
taskId: checkbox.taskId,
task: checkbox.task ? {

View File

@@ -88,6 +88,7 @@ export async function POST(request: Request) {
const checkbox = await dailyService.addCheckbox({
date,
text: body.text,
type: body.type,
taskId: body.taskId,
order: body.order,
isChecked: body.isChecked

View File

@@ -1,216 +1,16 @@
'use client';
import { useState, useRef, useEffect } from 'react';
import { useState, useEffect } from 'react';
import React from 'react';
import { useDaily } from '@/hooks/useDaily';
import { DailyCheckbox, DailyView } from '@/lib/types';
import { DailyView, DailyCheckboxType } 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 { DailyCalendar } from '@/components/daily/DailyCalendar';
import { DailySection } from '@/components/daily/DailySection';
import { dailyClient } from '@/clients/daily-client';
import { SimpleHeader } from '@/components/ui/SimpleHeader';
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;
refreshing?: boolean;
}
function DailySectionComponent({
title,
date,
checkboxes,
onAddCheckbox,
onToggleCheckbox,
onUpdateCheckbox,
onDeleteCheckbox,
saving,
refreshing = false
}: 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 flex items-center gap-2">
{title} <span className="text-sm font-normal text-[var(--muted-foreground)]">({formatShortDate(date)})</span>
{refreshing && (
<div className="w-4 h-4 border-2 border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div>
)}
</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>
);
}
interface DailyPageClientProps {
initialDailyView?: DailyView;
initialDailyDates?: string[];
@@ -260,12 +60,14 @@ export function DailyPageClient({
}
}, [initialDailyDates.length]);
const handleAddTodayCheckbox = async (text: string) => {
const handleAddTodayCheckbox = async (text: string, type: DailyCheckboxType) => {
try {
const { dailyClient } = await import('@/clients/daily-client');
await dailyClient.addCheckbox({
date: currentDate,
text,
type,
// Pas de taskId lors de l'ajout - sera ajouté via l'édition
isChecked: false
});
// Recharger silencieusement la vue daily (sans clignotement)
@@ -277,7 +79,7 @@ export function DailyPageClient({
}
};
const handleAddYesterdayCheckbox = async (text: string) => {
const handleAddYesterdayCheckbox = async (text: string, type: DailyCheckboxType) => {
try {
const yesterday = new Date(currentDate);
yesterday.setDate(yesterday.getDate() - 1);
@@ -286,6 +88,8 @@ export function DailyPageClient({
await dailyClient.addCheckbox({
date: yesterday,
text,
type,
// Pas de taskId lors de l'ajout - sera ajouté via l'édition
isChecked: false
});
// Recharger silencieusement la vue daily (sans clignotement)
@@ -307,8 +111,12 @@ export function DailyPageClient({
await refreshDailyDates();
};
const handleUpdateCheckbox = async (checkboxId: string, text: string) => {
await updateCheckbox(checkboxId, { text });
const handleUpdateCheckbox = async (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => {
await updateCheckbox(checkboxId, {
text,
type,
taskId: type === 'task' ? taskId : undefined // Supprimer la liaison tâche si on passe en réunion
});
};
const getYesterdayDate = () => {
@@ -430,7 +238,7 @@ export function DailyPageClient({
{dailyView && (
<div className="xl:col-span-2 grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Section Hier */}
<DailySectionComponent
<DailySection
title="📋 Hier"
date={getYesterdayDate()}
checkboxes={dailyView.yesterday}
@@ -443,7 +251,7 @@ export function DailyPageClient({
/>
{/* Section Aujourd'hui */}
<DailySectionComponent
<DailySection
title="🎯 Aujourd'hui"
date={getTodayDate()}
checkboxes={dailyView.today}
@@ -458,7 +266,7 @@ export function DailyPageClient({
)}
</div>
{/* Footer avec stats */}
{/* Footer avec stats - dans le flux normal */}
{dailyView && (
<Card className="mt-8 p-4">
<div className="text-center text-sm text-[var(--muted-foreground)] font-mono">