Files
towercontrol/components/daily/DailySection.tsx
Julien Froidefond 618b2e9e5c feat: implement drag-and-drop reordering for daily checkboxes
- Added DnD functionality to `DailySection` for reordering checkboxes using `@dnd-kit/core` and `@dnd-kit/sortable`.
- Introduced `onReorderCheckboxes` prop to handle server updates after reordering.
- Updated `useDaily` hook to streamline error handling during reordering.
- Cleaned up `Header` component by removing unnecessary syncing text.
- Adjusted `DailyPageClient` to pass reorder function to `DailySection`.
2025-09-18 14:56:05 +02:00

184 lines
6.1 KiB
TypeScript

'use client';
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { DailyCheckboxSortable } from './DailyCheckboxSortable';
import { DailyCheckboxItem } from './DailyCheckboxItem';
import { DailyAddForm } from './DailyAddForm';
import { DndContext, closestCenter, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
import { useState } from 'react';
import React from 'react';
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>;
onReorderCheckboxes: (date: Date, checkboxIds: string[]) => Promise<void>;
onToggleAll?: () => Promise<void>;
saving: boolean;
refreshing?: boolean;
}
export function DailySection({
title,
date,
checkboxes,
onAddCheckbox,
onToggleCheckbox,
onUpdateCheckbox,
onDeleteCheckbox,
onReorderCheckboxes,
onToggleAll,
saving,
refreshing = false
}: DailySectionProps) {
const [activeId, setActiveId] = useState<string | null>(null);
const [items, setItems] = useState(checkboxes);
const formatShortDate = (date: Date) => {
return date.toLocaleDateString('fr-FR', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
// Mettre à jour les items quand les checkboxes changent
React.useEffect(() => {
setItems(checkboxes);
}, [checkboxes]);
const handleDragStart = (event: DragStartEvent) => {
setActiveId(event.active.id as string);
};
const handleDragEnd = async (event: DragEndEvent) => {
const { active, over } = event;
setActiveId(null);
if (!over || active.id === over.id) {
return;
}
const oldIndex = items.findIndex((item) => item.id === active.id);
const newIndex = items.findIndex((item) => item.id === over.id);
if (oldIndex !== -1 && newIndex !== -1) {
// Mise à jour optimiste
const newItems = arrayMove(items, oldIndex, newIndex);
setItems(newItems);
// Envoyer l'ordre au serveur
const checkboxIds = newItems.map(item => item.id);
try {
await onReorderCheckboxes(date, checkboxIds);
} catch (error) {
// Rollback en cas d'erreur
setItems(checkboxes);
console.error('Erreur lors du réordonnancement:', error);
}
}
};
const activeCheckbox = activeId ? items.find(item => item.id === activeId) : null;
return (
<DndContext
collisionDetection={closestCenter}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
id={`daily-dnd-${title.replace(/[^a-zA-Z0-9]/g, '-')}`}
>
<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>
<div className="flex items-center gap-2">
<span className="text-xs text-[var(--muted-foreground)] font-mono">
{checkboxes.filter(cb => cb.isChecked).length}/{checkboxes.length}
</span>
{onToggleAll && checkboxes.length > 0 && (
<Button
type="button"
onClick={onToggleAll}
variant="ghost"
size="sm"
disabled={saving}
className="text-xs px-2 py-1 h-6"
title="Tout cocher/décocher"
>
</Button>
)}
</div>
</div>
</div>
{/* Liste des checkboxes - zone scrollable avec drag & drop */}
<div className="flex-1 px-4 overflow-y-auto min-h-0">
<SortableContext items={items.map(item => item.id)} strategy={verticalListSortingStrategy}>
<div className="space-y-1.5 pb-4">
{items.map((checkbox) => (
<DailyCheckboxSortable
key={checkbox.id}
checkbox={checkbox}
onToggle={onToggleCheckbox}
onUpdate={onUpdateCheckbox}
onDelete={onDeleteCheckbox}
saving={saving}
/>
))}
{items.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>
</SortableContext>
</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>
<DragOverlay
dropAnimation={null}
style={{
transformOrigin: '0 0',
}}
>
{activeCheckbox ? (
<div className="bg-[var(--card)] border border-[var(--border)] rounded-md shadow-xl opacity-95 transform rotate-3 scale-105">
<div className="pl-4">
<DailyCheckboxItem
checkbox={activeCheckbox}
onToggle={() => Promise.resolve()}
onUpdate={() => Promise.resolve()}
onDelete={() => Promise.resolve()}
saving={false}
/>
</div>
</div>
) : null}
</DragOverlay>
</DndContext>
);
}