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:
@@ -129,10 +129,10 @@ export function DailyCheckboxItem({
|
|||||||
{checkbox.task && (
|
{checkbox.task && (
|
||||||
<Link
|
<Link
|
||||||
href={`/?highlight=${checkbox.task.id}`}
|
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}`}
|
title={`Tâche: ${checkbox.task.title}`}
|
||||||
>
|
>
|
||||||
#{checkbox.task.id.slice(-6)}
|
{checkbox.task.title}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
import { DailyCheckbox, DailyCheckboxType, Task } from '@/lib/types';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { Modal } from '@/components/ui/Modal';
|
import { Modal } from '@/components/ui/Modal';
|
||||||
import { TaskSelector } from './TaskSelector';
|
import { tasksClient } from '@/clients/tasks-client';
|
||||||
|
|
||||||
interface EditCheckboxModalProps {
|
interface EditCheckboxModalProps {
|
||||||
checkbox: DailyCheckbox;
|
checkbox: DailyCheckbox;
|
||||||
@@ -25,6 +25,49 @@ export function EditCheckboxModal({
|
|||||||
const [text, setText] = useState(checkbox.text);
|
const [text, setText] = useState(checkbox.text);
|
||||||
const [type, setType] = useState<DailyCheckboxType>(checkbox.type);
|
const [type, setType] = useState<DailyCheckboxType>(checkbox.type);
|
||||||
const [taskId, setTaskId] = useState<string | undefined>(checkbox.taskId);
|
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 () => {
|
const handleSave = async () => {
|
||||||
if (!text.trim()) return;
|
if (!text.trim()) return;
|
||||||
@@ -114,18 +157,89 @@ export function EditCheckboxModal({
|
|||||||
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
<label className="block text-sm font-medium text-[var(--foreground)] mb-2">
|
||||||
Lier à une tâche (optionnel)
|
Lier à une tâche (optionnel)
|
||||||
</label>
|
</label>
|
||||||
<div className="border border-[var(--border)] rounded-lg p-3">
|
|
||||||
<TaskSelector
|
{selectedTask ? (
|
||||||
selectedTaskId={taskId}
|
// Tâche déjà sélectionnée
|
||||||
onTaskSelect={setTaskId}
|
<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}
|
disabled={saving}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</Button>
|
||||||
|
</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"
|
||||||
/>
|
/>
|
||||||
{taskId && (
|
|
||||||
<div className="mt-2 text-xs text-[var(--muted-foreground)]">
|
{taskSearch.trim() && (
|
||||||
Tâche liée : #{taskId.slice(-6)}
|
<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>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user