feat: enhance DailyCheckboxItem and EditCheckboxModal for task management

- Updated DailyCheckboxItem to display task title instead of ID, improving user clarity.
- Refactored EditCheckboxModal to load tasks dynamically, allowing for task selection with search functionality.
- Removed TaskSelector component to streamline task selection process within the modal.
- Added loading and filtering logic for tasks, enhancing user experience during task selection.
This commit is contained in:
Julien Froidefond
2025-09-15 22:39:58 +02:00
parent 4b27047e63
commit c2f949325a
3 changed files with 130 additions and 199 deletions

View File

@@ -129,10 +129,10 @@ export function DailyCheckboxItem({
{checkbox.task && (
<Link
href={`/?highlight=${checkbox.task.id}`}
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono"
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono truncate max-w-[120px]"
title={`Tâche: ${checkbox.task.title}`}
>
#{checkbox.task.id.slice(-6)}
{checkbox.task.title}
</Link>
)}

View File

@@ -1,11 +1,11 @@
'use client';
import { useState } from 'react';
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
import { useState, useEffect } from 'react';
import { DailyCheckbox, DailyCheckboxType, Task } 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';
import { tasksClient } from '@/clients/tasks-client';
interface EditCheckboxModalProps {
checkbox: DailyCheckbox;
@@ -25,6 +25,49 @@ export function EditCheckboxModal({
const [text, setText] = useState(checkbox.text);
const [type, setType] = useState<DailyCheckboxType>(checkbox.type);
const [taskId, setTaskId] = useState<string | undefined>(checkbox.taskId);
const [selectedTask, setSelectedTask] = useState<Task | undefined>(undefined);
const [allTasks, setAllTasks] = useState<Task[]>([]);
const [tasksLoading, setTasksLoading] = useState(false);
const [taskSearch, setTaskSearch] = useState('');
// Charger toutes les tâches au début
useEffect(() => {
if (isOpen) {
setTasksLoading(true);
tasksClient.getTasks()
.then(response => {
setAllTasks(response.data);
// Trouver la tâche sélectionnée si elle existe
if (taskId) {
const task = response.data.find((t: Task) => t.id === taskId);
setSelectedTask(task);
}
})
.catch(console.error)
.finally(() => setTasksLoading(false));
}
}, [isOpen, taskId]);
// Mettre à jour la tâche sélectionnée quand taskId change
useEffect(() => {
if (taskId && allTasks.length > 0) {
const task = allTasks.find((t: Task) => t.id === taskId);
setSelectedTask(task);
} else {
setSelectedTask(undefined);
}
}, [taskId, allTasks]);
// Filtrer les tâches selon la recherche
const filteredTasks = allTasks.filter(task =>
task.title.toLowerCase().includes(taskSearch.toLowerCase()) ||
(task.description && task.description.toLowerCase().includes(taskSearch.toLowerCase()))
);
const handleTaskSelect = (task: Task) => {
setTaskId(task.id);
setTaskSearch(''); // Fermer la recherche après sélection
};
const handleSave = async () => {
if (!text.trim()) return;
@@ -114,18 +157,89 @@ export function EditCheckboxModal({
<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)}
{selectedTask ? (
// Tâche déjà sélectionnée
<div className="border border-[var(--border)] rounded-lg p-3 bg-[var(--muted)]/30">
<div className="flex items-center justify-between">
<div className="flex-1">
<div className="font-medium text-sm">{selectedTask.title}</div>
{selectedTask.description && (
<div className="text-xs text-[var(--muted-foreground)] truncate">
{selectedTask.description}
</div>
)}
<span className={`inline-block px-1 py-0.5 rounded text-xs mt-1 ${
selectedTask.status === 'todo' ? 'bg-blue-100 text-blue-800' :
selectedTask.status === 'in_progress' ? 'bg-yellow-100 text-yellow-800' :
'bg-gray-100 text-gray-800'
}`}>
{selectedTask.status}
</span>
</div>
<Button
type="button"
onClick={() => setTaskId(undefined)}
variant="ghost"
size="sm"
className="text-[var(--destructive)] hover:bg-[var(--destructive)]/10"
disabled={saving}
>
×
</Button>
</div>
)}
</div>
</div>
) : (
// Interface de sélection simplifiée
<div className="space-y-2">
<Input
type="text"
placeholder="Rechercher une tâche..."
value={taskSearch}
onChange={(e) => setTaskSearch(e.target.value)}
disabled={saving || tasksLoading}
className="w-full"
/>
{taskSearch.trim() && (
<div className="border border-[var(--border)] rounded-lg max-h-40 overflow-y-auto">
{tasksLoading ? (
<div className="p-3 text-center text-sm text-[var(--muted-foreground)]">
Chargement...
</div>
) : filteredTasks.length === 0 ? (
<div className="p-3 text-center text-sm text-[var(--muted-foreground)]">
Aucune tâche trouvée
</div>
) : (
filteredTasks.slice(0, 5).map((task) => (
<button
key={task.id}
type="button"
onClick={() => handleTaskSelect(task)}
className="w-full text-left p-3 hover:bg-[var(--muted)]/50 transition-colors border-b border-[var(--border)]/30 last:border-b-0"
disabled={saving}
>
<div className="font-medium text-sm">{task.title}</div>
{task.description && (
<div className="text-xs text-[var(--muted-foreground)] truncate mt-1">
{task.description}
</div>
)}
<span className={`inline-block px-1 py-0.5 rounded text-xs mt-1 ${
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>
</button>
))
)}
</div>
)}
</div>
)}
</div>
)}

View File

@@ -1,183 +0,0 @@
'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>
);
}