- Updated `CreateTaskForm` and `EditTaskForm` to load priority options dynamically using `getAllPriorities`, improving maintainability. - Refactored `KanbanFilters` to utilize dynamic priority options, enhancing filter functionality. - Modified `QuickAddTask` and `TaskCard` to display priorities using centralized configuration, ensuring consistency across the application. - Introduced new utility functions in `status-config.ts` for managing priority configurations, streamlining the task management process.
198 lines
7.0 KiB
TypeScript
198 lines
7.0 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
import { Card } from '@/components/ui/Card';
|
|
import { TagInput } from '@/components/ui/TagInput';
|
|
import { TaskStatus, TaskPriority } from '@/lib/types';
|
|
import { CreateTaskData } from '@/clients/tasks-client';
|
|
import { getAllPriorities, getPriorityConfig } from '@/lib/status-config';
|
|
|
|
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 [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
|
|
});
|
|
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 gère ses propres événements Enter
|
|
} 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 handleTagsChange = (tags: string[]) => {
|
|
setFormData(prev => ({
|
|
...prev,
|
|
tags
|
|
}));
|
|
};
|
|
|
|
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"
|
|
>
|
|
{getAllPriorities().map(priorityConfig => (
|
|
<option key={priorityConfig.key} value={priorityConfig.key}>
|
|
{priorityConfig.icon} {priorityConfig.label}
|
|
</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">
|
|
<TagInput
|
|
tags={formData.tags || []}
|
|
onChange={handleTagsChange}
|
|
placeholder="Tags..."
|
|
maxTags={5}
|
|
className="text-xs"
|
|
/>
|
|
</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>
|
|
);
|
|
}
|