Files
towercontrol/src/components/daily/PendingTasksSection.tsx
Julien Froidefond 3cfed60f43 feat: refactor daily task management with new pending tasks section
- 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.
2025-09-21 19:55:04 +02:00

240 lines
8.9 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
);
}