- Added `PendingTasksSection` to `DailyPageClient` for displaying uncompleted tasks. - Implemented `getPendingCheckboxes` method in `DailyClient` and `DailyService` to fetch pending tasks. - Introduced `getDaysAgo` utility function for calculating elapsed days since a date. - Updated `TODO.md` to reflect the new task management features and adjustments. - Cleaned up and organized folder structure to align with Next.js 13+ best practices.
240 lines
8.9 KiB
TypeScript
240 lines
8.9 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useEffect, useCallback } from 'react';
|
||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||
import { Button } from '@/components/ui/Button';
|
||
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
||
import { dailyClient } from '@/clients/daily-client';
|
||
import { formatDateShort, getDaysAgo } from '@/lib/date-utils';
|
||
|
||
interface PendingTasksSectionProps {
|
||
onToggleCheckbox: (checkboxId: string) => Promise<void>;
|
||
onDeleteCheckbox: (checkboxId: string) => Promise<void>;
|
||
refreshTrigger?: number; // Pour forcer le refresh depuis le parent
|
||
}
|
||
|
||
export function PendingTasksSection({
|
||
onToggleCheckbox,
|
||
onDeleteCheckbox,
|
||
refreshTrigger
|
||
}: PendingTasksSectionProps) {
|
||
const [isCollapsed, setIsCollapsed] = useState(true);
|
||
const [pendingTasks, setPendingTasks] = useState<DailyCheckbox[]>([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [filters, setFilters] = useState({
|
||
maxDays: 7,
|
||
type: 'all' as 'all' | DailyCheckboxType,
|
||
limit: 50
|
||
});
|
||
|
||
// Charger les tâches en attente
|
||
const loadPendingTasks = useCallback(async () => {
|
||
setLoading(true);
|
||
try {
|
||
const tasks = await dailyClient.getPendingCheckboxes({
|
||
maxDays: filters.maxDays,
|
||
excludeToday: true,
|
||
type: filters.type === 'all' ? undefined : filters.type,
|
||
limit: filters.limit
|
||
});
|
||
setPendingTasks(tasks);
|
||
} catch (error) {
|
||
console.error('Erreur lors du chargement des tâches en attente:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, [filters]);
|
||
|
||
// Charger au montage et quand les filtres changent
|
||
useEffect(() => {
|
||
if (!isCollapsed) {
|
||
loadPendingTasks();
|
||
}
|
||
}, [isCollapsed, filters, refreshTrigger, loadPendingTasks]);
|
||
|
||
// Gérer l'archivage d'une tâche
|
||
const handleArchiveTask = async (checkboxId: string) => {
|
||
try {
|
||
await dailyClient.archiveCheckbox(checkboxId);
|
||
await loadPendingTasks(); // Recharger la liste
|
||
} catch (error) {
|
||
console.error('Erreur lors de l\'archivage:', error);
|
||
}
|
||
};
|
||
|
||
// Gérer le cochage d'une tâche
|
||
const handleToggleTask = async (checkboxId: string) => {
|
||
await onToggleCheckbox(checkboxId);
|
||
await loadPendingTasks(); // Recharger la liste
|
||
};
|
||
|
||
// Gérer la suppression d'une tâche
|
||
const handleDeleteTask = async (checkboxId: string) => {
|
||
await onDeleteCheckbox(checkboxId);
|
||
await loadPendingTasks(); // Recharger la liste
|
||
};
|
||
|
||
// Obtenir la couleur selon l'ancienneté
|
||
const getAgeColor = (date: Date) => {
|
||
const days = getDaysAgo(date);
|
||
if (days <= 1) return 'text-green-600';
|
||
if (days <= 3) return 'text-yellow-600';
|
||
if (days <= 7) return 'text-orange-600';
|
||
return 'text-red-600';
|
||
};
|
||
|
||
// Obtenir l'icône selon le type
|
||
const getTypeIcon = (type: DailyCheckboxType) => {
|
||
return type === 'meeting' ? '🤝' : '📋';
|
||
};
|
||
|
||
const pendingCount = pendingTasks.length;
|
||
|
||
return (
|
||
<Card className="mt-6">
|
||
<CardHeader>
|
||
<div className="flex items-center justify-between">
|
||
<button
|
||
onClick={() => setIsCollapsed(!isCollapsed)}
|
||
className="flex items-center gap-2 text-lg font-semibold hover:text-[var(--primary)] transition-colors"
|
||
>
|
||
<span className={`transform transition-transform ${isCollapsed ? 'rotate-0' : 'rotate-90'}`}>
|
||
▶️
|
||
</span>
|
||
📋 Tâches en attente
|
||
{pendingCount > 0 && (
|
||
<span className="bg-[var(--warning)] text-[var(--warning-foreground)] px-2 py-1 rounded-full text-xs font-medium">
|
||
{pendingCount}
|
||
</span>
|
||
)}
|
||
</button>
|
||
|
||
{!isCollapsed && (
|
||
<div className="flex items-center gap-2">
|
||
{/* Filtres rapides */}
|
||
<select
|
||
value={filters.maxDays}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, maxDays: parseInt(e.target.value) }))}
|
||
className="text-xs px-2 py-1 border border-[var(--border)] rounded bg-[var(--background)]"
|
||
>
|
||
<option value={7}>7 derniers jours</option>
|
||
<option value={14}>14 derniers jours</option>
|
||
<option value={30}>30 derniers jours</option>
|
||
</select>
|
||
|
||
<select
|
||
value={filters.type}
|
||
onChange={(e) => setFilters(prev => ({ ...prev, type: e.target.value as 'all' | DailyCheckboxType }))}
|
||
className="text-xs px-2 py-1 border border-[var(--border)] rounded bg-[var(--background)]"
|
||
>
|
||
<option value="all">Tous types</option>
|
||
<option value="task">Tâches</option>
|
||
<option value="meeting">Réunions</option>
|
||
</select>
|
||
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={loadPendingTasks}
|
||
disabled={loading}
|
||
>
|
||
{loading ? '🔄' : '↻'}
|
||
</Button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</CardHeader>
|
||
|
||
{!isCollapsed && (
|
||
<CardContent>
|
||
{loading ? (
|
||
<div className="text-center py-4 text-[var(--muted-foreground)]">
|
||
Chargement des tâches en attente...
|
||
</div>
|
||
) : pendingTasks.length === 0 ? (
|
||
<div className="text-center py-4 text-[var(--muted-foreground)]">
|
||
🎉 Aucune tâche en attente ! Excellent travail.
|
||
</div>
|
||
) : (
|
||
<div className="space-y-2">
|
||
{pendingTasks.map((task) => {
|
||
const daysAgo = getDaysAgo(task.date);
|
||
const isArchived = task.text.includes('[ARCHIVÉ]');
|
||
|
||
return (
|
||
<div
|
||
key={task.id}
|
||
className={`flex items-center gap-3 p-3 rounded-lg border border-[var(--border)] ${
|
||
isArchived ? 'opacity-60 bg-[var(--muted)]/20' : 'bg-[var(--card)]'
|
||
}`}
|
||
>
|
||
{/* Checkbox */}
|
||
<button
|
||
onClick={() => handleToggleTask(task.id)}
|
||
disabled={isArchived}
|
||
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
|
||
isArchived
|
||
? 'border-[var(--muted)] cursor-not-allowed'
|
||
: 'border-[var(--border)] hover:border-[var(--primary)]'
|
||
}`}
|
||
>
|
||
{task.isChecked && <span className="text-[var(--primary)]">✓</span>}
|
||
</button>
|
||
|
||
{/* Contenu */}
|
||
<div className="flex-1 min-w-0">
|
||
<div className="flex items-center gap-2 mb-1">
|
||
<span>{getTypeIcon(task.type)}</span>
|
||
<span className={`text-sm font-medium ${isArchived ? 'line-through' : ''}`}>
|
||
{task.text}
|
||
</span>
|
||
</div>
|
||
<div className="flex items-center gap-3 text-xs text-[var(--muted-foreground)]">
|
||
<span>{formatDateShort(task.date)}</span>
|
||
<span className={getAgeColor(task.date)}>
|
||
{daysAgo === 0 ? 'Aujourd\'hui' :
|
||
daysAgo === 1 ? 'Hier' :
|
||
`Il y a ${daysAgo} jours`}
|
||
</span>
|
||
{task.task && (
|
||
<span className="text-[var(--primary)]">
|
||
🔗 {task.task.title}
|
||
</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Actions */}
|
||
<div className="flex items-center gap-1">
|
||
{!isArchived && (
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleArchiveTask(task.id)}
|
||
title="Archiver cette tâche"
|
||
className="text-xs px-2 py-1"
|
||
>
|
||
📦
|
||
</Button>
|
||
)}
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
onClick={() => handleDeleteTask(task.id)}
|
||
title="Supprimer cette tâche"
|
||
className="text-xs px-2 py-1 text-[var(--destructive)] hover:text-[var(--destructive)]"
|
||
>
|
||
🗑️
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
)}
|
||
</Card>
|
||
);
|
||
}
|