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`.
This commit is contained in:
78
components/daily/DailyCheckboxSortable.tsx
Normal file
78
components/daily/DailyCheckboxSortable.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useSortable } from '@dnd-kit/sortable';
|
||||||
|
import { CSS } from '@dnd-kit/utilities';
|
||||||
|
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
||||||
|
import { DailyCheckboxItem } from './DailyCheckboxItem';
|
||||||
|
|
||||||
|
interface DailyCheckboxSortableProps {
|
||||||
|
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 DailyCheckboxSortable({
|
||||||
|
checkbox,
|
||||||
|
onToggle,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
saving = false
|
||||||
|
}: DailyCheckboxSortableProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
isDragging,
|
||||||
|
} = useSortable({
|
||||||
|
id: checkbox.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition,
|
||||||
|
opacity: isDragging ? 0.5 : 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
className={`
|
||||||
|
${isDragging ? 'z-50' : 'z-0'}
|
||||||
|
${isDragging ? 'shadow-lg' : ''}
|
||||||
|
transition-shadow duration-200
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="relative group">
|
||||||
|
{/* Handle de drag */}
|
||||||
|
<div
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
className="absolute left-0 top-0 bottom-0 w-3 cursor-grab active:cursor-grabbing flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity"
|
||||||
|
title="Glisser pour réorganiser"
|
||||||
|
>
|
||||||
|
<div className="w-1.5 h-6 flex flex-col justify-center gap-0.5">
|
||||||
|
<div className="w-full h-0.5 bg-[var(--muted-foreground)] rounded"></div>
|
||||||
|
<div className="w-full h-0.5 bg-[var(--muted-foreground)] rounded"></div>
|
||||||
|
<div className="w-full h-0.5 bg-[var(--muted-foreground)] rounded"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Checkbox item avec padding left pour le handle */}
|
||||||
|
<div className="pl-4">
|
||||||
|
<DailyCheckboxItem
|
||||||
|
checkbox={checkbox}
|
||||||
|
onToggle={onToggle}
|
||||||
|
onUpdate={onUpdate}
|
||||||
|
onDelete={onDelete}
|
||||||
|
saving={saving}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,8 +3,13 @@
|
|||||||
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { DailyCheckboxSortable } from './DailyCheckboxSortable';
|
||||||
import { DailyCheckboxItem } from './DailyCheckboxItem';
|
import { DailyCheckboxItem } from './DailyCheckboxItem';
|
||||||
import { DailyAddForm } from './DailyAddForm';
|
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 {
|
interface DailySectionProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -14,6 +19,7 @@ interface DailySectionProps {
|
|||||||
onToggleCheckbox: (checkboxId: string) => Promise<void>;
|
onToggleCheckbox: (checkboxId: string) => Promise<void>;
|
||||||
onUpdateCheckbox: (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => Promise<void>;
|
onUpdateCheckbox: (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => Promise<void>;
|
||||||
onDeleteCheckbox: (checkboxId: string) => Promise<void>;
|
onDeleteCheckbox: (checkboxId: string) => Promise<void>;
|
||||||
|
onReorderCheckboxes: (date: Date, checkboxIds: string[]) => Promise<void>;
|
||||||
onToggleAll?: () => Promise<void>;
|
onToggleAll?: () => Promise<void>;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
refreshing?: boolean;
|
refreshing?: boolean;
|
||||||
@@ -27,10 +33,13 @@ export function DailySection({
|
|||||||
onToggleCheckbox,
|
onToggleCheckbox,
|
||||||
onUpdateCheckbox,
|
onUpdateCheckbox,
|
||||||
onDeleteCheckbox,
|
onDeleteCheckbox,
|
||||||
|
onReorderCheckboxes,
|
||||||
onToggleAll,
|
onToggleAll,
|
||||||
saving,
|
saving,
|
||||||
refreshing = false
|
refreshing = false
|
||||||
}: DailySectionProps) {
|
}: DailySectionProps) {
|
||||||
|
const [activeId, setActiveId] = useState<string | null>(null);
|
||||||
|
const [items, setItems] = useState(checkboxes);
|
||||||
const formatShortDate = (date: Date) => {
|
const formatShortDate = (date: Date) => {
|
||||||
return date.toLocaleDateString('fr-FR', {
|
return date.toLocaleDateString('fr-FR', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@@ -39,67 +48,136 @@ export function DailySection({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<Card className="p-0 flex flex-col h-[600px]">
|
<DndContext
|
||||||
{/* Header */}
|
collisionDetection={closestCenter}
|
||||||
<div className="p-4 pb-0">
|
onDragStart={handleDragStart}
|
||||||
<div className="flex items-center justify-between mb-4">
|
onDragEnd={handleDragEnd}
|
||||||
<h2 className="text-lg font-bold text-[var(--foreground)] font-mono flex items-center gap-2">
|
id={`daily-dnd-${title.replace(/[^a-zA-Z0-9]/g, '-')}`}
|
||||||
{title} <span className="text-sm font-normal text-[var(--muted-foreground)]">({formatShortDate(date)})</span>
|
>
|
||||||
{refreshing && (
|
<Card className="p-0 flex flex-col h-[600px]">
|
||||||
<div className="w-4 h-4 border-2 border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div>
|
{/* Header */}
|
||||||
)}
|
<div className="p-4 pb-0">
|
||||||
</h2>
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center gap-2">
|
<h2 className="text-lg font-bold text-[var(--foreground)] font-mono flex items-center gap-2">
|
||||||
<span className="text-xs text-[var(--muted-foreground)] font-mono">
|
{title} <span className="text-sm font-normal text-[var(--muted-foreground)]">({formatShortDate(date)})</span>
|
||||||
{checkboxes.filter(cb => cb.isChecked).length}/{checkboxes.length}
|
{refreshing && (
|
||||||
</span>
|
<div className="w-4 h-4 border-2 border-[var(--primary)] border-t-transparent rounded-full animate-spin"></div>
|
||||||
{onToggleAll && checkboxes.length > 0 && (
|
)}
|
||||||
<Button
|
</h2>
|
||||||
type="button"
|
<div className="flex items-center gap-2">
|
||||||
onClick={onToggleAll}
|
<span className="text-xs text-[var(--muted-foreground)] font-mono">
|
||||||
variant="ghost"
|
{checkboxes.filter(cb => cb.isChecked).length}/{checkboxes.length}
|
||||||
size="sm"
|
</span>
|
||||||
disabled={saving}
|
{onToggleAll && checkboxes.length > 0 && (
|
||||||
className="text-xs px-2 py-1 h-6"
|
<Button
|
||||||
title="Tout cocher/décocher"
|
type="button"
|
||||||
>
|
onClick={onToggleAll}
|
||||||
✓
|
variant="ghost"
|
||||||
</Button>
|
size="sm"
|
||||||
)}
|
disabled={saving}
|
||||||
|
className="text-xs px-2 py-1 h-6"
|
||||||
|
title="Tout cocher/décocher"
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Liste des checkboxes - zone scrollable */}
|
{/* Liste des checkboxes - zone scrollable avec drag & drop */}
|
||||||
<div className="flex-1 px-4 overflow-y-auto min-h-0">
|
<div className="flex-1 px-4 overflow-y-auto min-h-0">
|
||||||
<div className="space-y-1.5 pb-4">
|
<SortableContext items={items.map(item => item.id)} strategy={verticalListSortingStrategy}>
|
||||||
{checkboxes.map((checkbox) => (
|
<div className="space-y-1.5 pb-4">
|
||||||
<DailyCheckboxItem
|
{items.map((checkbox) => (
|
||||||
key={checkbox.id}
|
<DailyCheckboxSortable
|
||||||
checkbox={checkbox}
|
key={checkbox.id}
|
||||||
onToggle={onToggleCheckbox}
|
checkbox={checkbox}
|
||||||
onUpdate={onUpdateCheckbox}
|
onToggle={onToggleCheckbox}
|
||||||
onDelete={onDeleteCheckbox}
|
onUpdate={onUpdateCheckbox}
|
||||||
saving={saving}
|
onDelete={onDeleteCheckbox}
|
||||||
/>
|
saving={saving}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
{checkboxes.length === 0 && (
|
{items.length === 0 && (
|
||||||
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm font-mono">
|
<div className="text-center py-8 text-[var(--muted-foreground)] text-sm font-mono">
|
||||||
Aucune tâche pour cette période
|
Aucune tâche pour cette période
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
</SortableContext>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Footer - Formulaire d'ajout toujours en bas */}
|
{/* Footer - Formulaire d'ajout toujours en bas */}
|
||||||
<div className="p-4 pt-2 border-t border-[var(--border)]/30 bg-[var(--card)]/50">
|
<div className="p-4 pt-2 border-t border-[var(--border)]/30 bg-[var(--card)]/50">
|
||||||
<DailyAddForm
|
<DailyAddForm
|
||||||
onAdd={onAddCheckbox}
|
onAdd={onAddCheckbox}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export function Header({ title = "TowerControl", subtitle = "Task Management", s
|
|||||||
{title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-sm">
|
<p className="text-[var(--muted-foreground)] mt-1 font-mono text-sm">
|
||||||
{subtitle} {syncing && '• Synchronisation...'}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -329,10 +329,7 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD
|
|||||||
const dailyId = data.date.toISOString().split('T')[0];
|
const dailyId = data.date.toISOString().split('T')[0];
|
||||||
const result = await reorderCheckboxesAction(dailyId, data.checkboxIds);
|
const result = await reorderCheckboxesAction(dailyId, data.checkboxIds);
|
||||||
|
|
||||||
if (result.success) {
|
if (!result.success) {
|
||||||
// Rafraîchir pour obtenir l'ordre correct
|
|
||||||
await refreshDaily();
|
|
||||||
} else {
|
|
||||||
setError(result.error || 'Erreur lors du réordonnancement');
|
setError(result.error || 'Erreur lors du réordonnancement');
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -341,7 +338,7 @@ export function useDaily(initialDate?: Date, initialDailyView?: DailyView): UseD
|
|||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [refreshDaily]);
|
}, []);
|
||||||
|
|
||||||
const goToPreviousDay = useCallback(async (): Promise<void> => {
|
const goToPreviousDay = useCallback(async (): Promise<void> => {
|
||||||
const previousDay = new Date(currentDate);
|
const previousDay = new Date(currentDate);
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export function DailyPageClient({
|
|||||||
toggleAllYesterday,
|
toggleAllYesterday,
|
||||||
updateCheckbox,
|
updateCheckbox,
|
||||||
deleteCheckbox,
|
deleteCheckbox,
|
||||||
|
reorderCheckboxes,
|
||||||
goToPreviousDay,
|
goToPreviousDay,
|
||||||
goToNextDay,
|
goToNextDay,
|
||||||
goToToday,
|
goToToday,
|
||||||
@@ -93,6 +94,10 @@ export function DailyPageClient({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleReorderCheckboxes = async (date: Date, checkboxIds: string[]) => {
|
||||||
|
await reorderCheckboxes({ date, checkboxIds });
|
||||||
|
};
|
||||||
|
|
||||||
const getYesterdayDate = () => {
|
const getYesterdayDate = () => {
|
||||||
const yesterday = new Date(currentDate);
|
const yesterday = new Date(currentDate);
|
||||||
yesterday.setDate(yesterday.getDate() - 1);
|
yesterday.setDate(yesterday.getDate() - 1);
|
||||||
@@ -220,6 +225,7 @@ export function DailyPageClient({
|
|||||||
onToggleCheckbox={handleToggleCheckbox}
|
onToggleCheckbox={handleToggleCheckbox}
|
||||||
onUpdateCheckbox={handleUpdateCheckbox}
|
onUpdateCheckbox={handleUpdateCheckbox}
|
||||||
onDeleteCheckbox={handleDeleteCheckbox}
|
onDeleteCheckbox={handleDeleteCheckbox}
|
||||||
|
onReorderCheckboxes={handleReorderCheckboxes}
|
||||||
onToggleAll={toggleAllYesterday}
|
onToggleAll={toggleAllYesterday}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
@@ -234,6 +240,7 @@ export function DailyPageClient({
|
|||||||
onToggleCheckbox={handleToggleCheckbox}
|
onToggleCheckbox={handleToggleCheckbox}
|
||||||
onUpdateCheckbox={handleUpdateCheckbox}
|
onUpdateCheckbox={handleUpdateCheckbox}
|
||||||
onDeleteCheckbox={handleDeleteCheckbox}
|
onDeleteCheckbox={handleDeleteCheckbox}
|
||||||
|
onReorderCheckboxes={handleReorderCheckboxes}
|
||||||
onToggleAll={toggleAllToday}
|
onToggleAll={toggleAllToday}
|
||||||
saving={saving}
|
saving={saving}
|
||||||
refreshing={refreshing}
|
refreshing={refreshing}
|
||||||
|
|||||||
Reference in New Issue
Block a user