feat: enhance Kanban functionality and update TODO.md
- Completed the creation and validation forms for tasks in the Kanban board, improving task management capabilities. - Integrated new task creation and deletion functionalities in the `KanbanBoard` and `KanbanColumn` components. - Added quick task addition feature in `Column` component for better user experience. - Updated `TaskCard` to support task deletion with a new button. - Marked several tasks as completed in `TODO.md` to reflect the progress on Kanban features. - Updated TypeScript types to include 'manual' as a new task source.
This commit is contained in:
225
components/kanban/QuickAddTask.tsx
Normal file
225
components/kanban/QuickAddTask.tsx
Normal file
@@ -0,0 +1,225 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
import { TaskStatus, TaskPriority } from '@/lib/types';
|
||||
import { CreateTaskData } from '@/clients/tasks-client';
|
||||
|
||||
interface QuickAddTaskProps {
|
||||
status: TaskStatus;
|
||||
onSubmit: (data: CreateTaskData) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
export function QuickAddTask({ status, onSubmit, onCancel }: QuickAddTaskProps) {
|
||||
const [formData, setFormData] = useState<CreateTaskData>({
|
||||
title: '',
|
||||
description: '',
|
||||
status,
|
||||
priority: 'medium' as TaskPriority,
|
||||
tags: [],
|
||||
dueDate: undefined
|
||||
});
|
||||
const [tagInput, setTagInput] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [activeField, setActiveField] = useState<'title' | 'description' | 'tags' | 'date' | null>('title');
|
||||
const titleRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// Focus automatique sur le titre
|
||||
useEffect(() => {
|
||||
titleRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const trimmedTitle = formData.title.trim();
|
||||
console.log('handleSubmit called:', { trimmedTitle, isSubmitting });
|
||||
if (!trimmedTitle || isSubmitting) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
console.log('Submitting task:', { ...formData, title: trimmedTitle });
|
||||
await onSubmit({
|
||||
...formData,
|
||||
title: trimmedTitle
|
||||
});
|
||||
|
||||
// Réinitialiser pour la prochaine tâche
|
||||
setFormData({
|
||||
title: '',
|
||||
description: '',
|
||||
status,
|
||||
priority: 'medium',
|
||||
tags: [],
|
||||
dueDate: undefined
|
||||
});
|
||||
setTagInput('');
|
||||
setActiveField('title');
|
||||
setIsSubmitting(false);
|
||||
titleRef.current?.focus();
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la création:', error);
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent, field: string) => {
|
||||
console.log('Key pressed:', e.key, 'field:', field, 'title:', formData.title);
|
||||
|
||||
// Seulement intercepter les touches spécifiques qu'on veut gérer
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (field === 'title' && formData.title.trim()) {
|
||||
console.log('Calling handleSubmit from title');
|
||||
handleSubmit();
|
||||
} else if (field === 'tags' && tagInput.trim()) {
|
||||
console.log('Adding tag');
|
||||
addTag();
|
||||
} else if (formData.title.trim()) {
|
||||
// Permettre création depuis n'importe quel champ si titre rempli
|
||||
console.log('Calling handleSubmit from other field');
|
||||
handleSubmit();
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onCancel();
|
||||
} else if (e.key === 'Tab' && !e.metaKey && !e.ctrlKey) {
|
||||
// Navigation entre les champs seulement si pas de modificateur
|
||||
e.preventDefault();
|
||||
const fields = ['title', 'description', 'tags', 'date'];
|
||||
const currentIndex = fields.indexOf(activeField || 'title');
|
||||
const nextField = fields[(currentIndex + 1) % fields.length] as typeof activeField;
|
||||
setActiveField(nextField);
|
||||
}
|
||||
// Laisser passer tous les autres événements (y compris les raccourcis système)
|
||||
};
|
||||
|
||||
const addTag = () => {
|
||||
const tag = tagInput.trim();
|
||||
if (tag && !formData.tags?.includes(tag)) {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
tags: [...(prev.tags || []), tag]
|
||||
}));
|
||||
setTagInput('');
|
||||
}
|
||||
};
|
||||
|
||||
const removeTag = (tagToRemove: string) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
tags: prev.tags?.filter(tag => tag !== tagToRemove) || []
|
||||
}));
|
||||
};
|
||||
|
||||
const handleBlur = (e: React.FocusEvent) => {
|
||||
// Vérifier si le focus reste dans le composant
|
||||
setTimeout(() => {
|
||||
const currentTarget = e.currentTarget;
|
||||
const relatedTarget = e.relatedTarget as Node | null;
|
||||
|
||||
// Si le focus sort complètement du composant ET qu'il n'y a pas de titre
|
||||
if (currentTarget && (!relatedTarget || !currentTarget.contains(relatedTarget)) && !formData.title.trim()) {
|
||||
onCancel();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
return (
|
||||
<div onBlur={handleBlur}>
|
||||
<Card className="p-3 border-dashed border-cyan-500/30 bg-slate-800/50 hover:border-cyan-500/50 transition-all duration-300">
|
||||
{/* Header avec titre et priorité */}
|
||||
<div className="flex items-start gap-2 mb-2">
|
||||
<input
|
||||
ref={titleRef}
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, title: e.target.value }))}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'title')}
|
||||
onFocus={() => setActiveField('title')}
|
||||
placeholder="Titre de la tâche..."
|
||||
disabled={isSubmitting}
|
||||
className="flex-1 bg-transparent border-none outline-none text-slate-100 font-mono text-sm font-medium placeholder-slate-500 leading-tight"
|
||||
/>
|
||||
|
||||
{/* Indicateur de priorité */}
|
||||
<select
|
||||
value={formData.priority}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, priority: e.target.value as TaskPriority }))}
|
||||
disabled={isSubmitting}
|
||||
className="bg-transparent border-none outline-none text-xs font-mono text-slate-400"
|
||||
>
|
||||
<option value="low">L</option>
|
||||
<option value="medium">M</option>
|
||||
<option value="high">H</option>
|
||||
<option value="urgent">U</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => setFormData(prev => ({ ...prev, description: e.target.value }))}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'description')}
|
||||
onFocus={() => setActiveField('description')}
|
||||
placeholder="Description..."
|
||||
rows={2}
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-transparent border-none outline-none text-xs text-slate-400 font-mono placeholder-slate-500 resize-none mb-2"
|
||||
/>
|
||||
|
||||
{/* Tags */}
|
||||
<div className="mb-2">
|
||||
<div className="flex flex-wrap gap-1 mb-1">
|
||||
{formData.tags && formData.tags.map((tag: string, index: number) => (
|
||||
<Badge
|
||||
key={index}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="cursor-pointer hover:bg-red-950/50 hover:text-red-300 hover:border-red-500/30 transition-colors"
|
||||
onClick={() => removeTag(tag)}
|
||||
>
|
||||
{tag} ✕
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={tagInput}
|
||||
onChange={(e) => setTagInput(e.target.value)}
|
||||
onKeyDown={(e) => handleKeyDown(e, 'tags')}
|
||||
onFocus={() => setActiveField('tags')}
|
||||
placeholder="Tags..."
|
||||
disabled={isSubmitting}
|
||||
className="w-full bg-transparent border-none outline-none text-xs text-slate-400 font-mono placeholder-slate-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Footer avec date et actions */}
|
||||
<div className="pt-2 border-t border-slate-700/50">
|
||||
<div className="flex items-center justify-between text-xs min-w-0">
|
||||
<input
|
||||
type="datetime-local"
|
||||
value={formData.dueDate ? new Date(formData.dueDate.getTime() - formData.dueDate.getTimezoneOffset() * 60000).toISOString().slice(0, 16) : ''}
|
||||
onChange={(e) => setFormData(prev => ({
|
||||
...prev,
|
||||
dueDate: e.target.value ? new Date(e.target.value) : undefined
|
||||
}))}
|
||||
onFocus={() => setActiveField('date')}
|
||||
disabled={isSubmitting}
|
||||
className="bg-transparent border-none outline-none text-slate-400 font-mono text-xs flex-shrink min-w-0"
|
||||
/>
|
||||
|
||||
{isSubmitting && (
|
||||
<div className="flex items-center gap-1 text-cyan-400 font-mono text-xs flex-shrink-0">
|
||||
<div className="w-3 h-3 border border-cyan-400 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span>...</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user