feat: refactor Daily components and enhance UI integration
- Replaced `DailyCalendar` with a new `Calendar` component for improved functionality and consistency. - Introduced `AlertBanner` to replace `DeadlineReminder`, providing a more flexible way to display urgent tasks. - Updated `DailyAddForm` to use new options for task types, enhancing user experience when adding tasks. - Removed unused state and components, streamlining the DailyPageClient for better performance and maintainability. - Enhanced `DailySection` to utilize new `CheckboxItem` format for better integration with the UI. - Cleaned up imports and improved overall structure for better readability.
This commit is contained in:
@@ -7,12 +7,12 @@ import { DailyView, DailyCheckboxType, DailyCheckbox } from '@/lib/types';
|
|||||||
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
|
import { DeadlineMetrics } from '@/services/analytics/deadline-analytics';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { DailyCalendar } from '@/components/daily/DailyCalendar';
|
import { Calendar } from '@/components/ui/Calendar';
|
||||||
|
import { AlertBanner, AlertItem } from '@/components/ui/AlertBanner';
|
||||||
import { DailySection } from '@/components/daily/DailySection';
|
import { DailySection } from '@/components/daily/DailySection';
|
||||||
import { PendingTasksSection } from '@/components/daily/PendingTasksSection';
|
import { PendingTasksSection } from '@/components/daily/PendingTasksSection';
|
||||||
import { dailyClient } from '@/clients/daily-client';
|
import { dailyClient } from '@/clients/daily-client';
|
||||||
import { Header } from '@/components/ui/Header';
|
import { Header } from '@/components/ui/Header';
|
||||||
import { DeadlineReminder } from '@/components/daily/DeadlineReminder';
|
|
||||||
import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils';
|
import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils';
|
||||||
|
|
||||||
interface DailyPageClientProps {
|
interface DailyPageClientProps {
|
||||||
@@ -53,7 +53,6 @@ export function DailyPageClient({
|
|||||||
} = useDaily(initialDate, initialDailyView);
|
} = useDaily(initialDate, initialDailyView);
|
||||||
|
|
||||||
const [dailyDates, setDailyDates] = useState<string[]>(initialDailyDates);
|
const [dailyDates, setDailyDates] = useState<string[]>(initialDailyDates);
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0);
|
|
||||||
|
|
||||||
// Fonction pour rafraîchir la liste des dates avec des dailies
|
// Fonction pour rafraîchir la liste des dates avec des dailies
|
||||||
const refreshDailyDates = async () => {
|
const refreshDailyDates = async () => {
|
||||||
@@ -88,14 +87,12 @@ export function DailyPageClient({
|
|||||||
|
|
||||||
const handleToggleCheckbox = async (checkboxId: string) => {
|
const handleToggleCheckbox = async (checkboxId: string) => {
|
||||||
await toggleCheckbox(checkboxId);
|
await toggleCheckbox(checkboxId);
|
||||||
setRefreshTrigger(prev => prev + 1); // Trigger refresh pour les tâches en attente
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteCheckbox = async (checkboxId: string) => {
|
const handleDeleteCheckbox = async (checkboxId: string) => {
|
||||||
await deleteCheckbox(checkboxId);
|
await deleteCheckbox(checkboxId);
|
||||||
// Refresh dates après suppression pour mettre à jour le calendrier
|
// Refresh dates après suppression pour mettre à jour le calendrier
|
||||||
await refreshDailyDates();
|
await refreshDailyDates();
|
||||||
setRefreshTrigger(prev => prev + 1); // Trigger refresh pour les tâches en attente
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateCheckbox = async (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => {
|
const handleUpdateCheckbox = async (checkboxId: string, text: string, type: DailyCheckboxType, taskId?: string) => {
|
||||||
@@ -110,6 +107,7 @@ export function DailyPageClient({
|
|||||||
await reorderCheckboxes({ date, checkboxIds });
|
await reorderCheckboxes({ date, checkboxIds });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const getYesterdayDate = () => {
|
const getYesterdayDate = () => {
|
||||||
return getPreviousWorkday(currentDate);
|
return getPreviousWorkday(currentDate);
|
||||||
};
|
};
|
||||||
@@ -142,6 +140,40 @@ export function DailyPageClient({
|
|||||||
return `📋 ${formatDateShort(yesterdayDate)}`;
|
return `📋 ${formatDateShort(yesterdayDate)}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Convertir les métriques de deadline en AlertItem
|
||||||
|
const convertDeadlineMetricsToAlertItems = (metrics: DeadlineMetrics | null): AlertItem[] => {
|
||||||
|
if (!metrics) return [];
|
||||||
|
|
||||||
|
const urgentTasks = [
|
||||||
|
...metrics.overdue,
|
||||||
|
...metrics.critical,
|
||||||
|
...metrics.warning
|
||||||
|
].sort((a, b) => {
|
||||||
|
const urgencyOrder: Record<string, number> = { 'overdue': 0, 'critical': 1, 'warning': 2 };
|
||||||
|
if (urgencyOrder[a.urgencyLevel] !== urgencyOrder[b.urgencyLevel]) {
|
||||||
|
return urgencyOrder[a.urgencyLevel] - urgencyOrder[b.urgencyLevel];
|
||||||
|
}
|
||||||
|
return a.daysRemaining - b.daysRemaining;
|
||||||
|
});
|
||||||
|
|
||||||
|
return urgentTasks.map(task => ({
|
||||||
|
id: task.id,
|
||||||
|
title: task.title,
|
||||||
|
icon: task.urgencyLevel === 'overdue' ? '🔴' :
|
||||||
|
task.urgencyLevel === 'critical' ? '🟠' : '🟡',
|
||||||
|
urgency: task.urgencyLevel as 'low' | 'medium' | 'high' | 'critical',
|
||||||
|
source: task.source,
|
||||||
|
metadata: task.urgencyLevel === 'overdue' ?
|
||||||
|
(task.daysRemaining === -1 ? 'En retard de 1 jour' : `En retard de ${Math.abs(task.daysRemaining)} jours`) :
|
||||||
|
task.urgencyLevel === 'critical' ?
|
||||||
|
(task.daysRemaining === 0 ? 'Échéance aujourd\'hui' :
|
||||||
|
task.daysRemaining === 1 ? 'Échéance demain' :
|
||||||
|
`Dans ${task.daysRemaining} jours`) :
|
||||||
|
`Dans ${task.daysRemaining} jours`
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
@@ -219,7 +251,16 @@ export function DailyPageClient({
|
|||||||
|
|
||||||
{/* Rappel des échéances urgentes - Desktop uniquement */}
|
{/* Rappel des échéances urgentes - Desktop uniquement */}
|
||||||
<div className="hidden sm:block container mx-auto px-4 pt-4 pb-2">
|
<div className="hidden sm:block container mx-auto px-4 pt-4 pb-2">
|
||||||
<DeadlineReminder deadlineMetrics={initialDeadlineMetrics} />
|
<AlertBanner
|
||||||
|
title="Rappel - Tâches urgentes"
|
||||||
|
items={convertDeadlineMetricsToAlertItems(initialDeadlineMetrics || null)}
|
||||||
|
icon="⚠️"
|
||||||
|
variant="warning"
|
||||||
|
onItemClick={(item) => {
|
||||||
|
// Rediriger vers la page Kanban avec la tâche sélectionnée
|
||||||
|
window.location.href = `/kanban?taskId=${item.id}`;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contenu principal */}
|
{/* Contenu principal */}
|
||||||
@@ -244,10 +285,12 @@ export function DailyPageClient({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Calendrier en bas sur mobile */}
|
{/* Calendrier en bas sur mobile */}
|
||||||
<DailyCalendar
|
<Calendar
|
||||||
currentDate={currentDate}
|
currentDate={currentDate}
|
||||||
onDateSelect={handleDateSelect}
|
onDateSelect={handleDateSelect}
|
||||||
dailyDates={dailyDates}
|
markedDates={dailyDates}
|
||||||
|
showTodayButton={true}
|
||||||
|
showLegend={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -258,10 +301,12 @@ export function DailyPageClient({
|
|||||||
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
|
||||||
{/* Calendrier - Desktop */}
|
{/* Calendrier - Desktop */}
|
||||||
<div className="xl:col-span-1">
|
<div className="xl:col-span-1">
|
||||||
<DailyCalendar
|
<Calendar
|
||||||
currentDate={currentDate}
|
currentDate={currentDate}
|
||||||
onDateSelect={handleDateSelect}
|
onDateSelect={handleDateSelect}
|
||||||
dailyDates={dailyDates}
|
markedDates={dailyDates}
|
||||||
|
showTodayButton={true}
|
||||||
|
showLegend={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -307,7 +352,7 @@ export function DailyPageClient({
|
|||||||
onToggleCheckbox={handleToggleCheckbox}
|
onToggleCheckbox={handleToggleCheckbox}
|
||||||
onDeleteCheckbox={handleDeleteCheckbox}
|
onDeleteCheckbox={handleDeleteCheckbox}
|
||||||
onRefreshDaily={refreshDailySilent}
|
onRefreshDaily={refreshDailySilent}
|
||||||
refreshTrigger={refreshTrigger}
|
refreshTrigger={0}
|
||||||
initialPendingTasks={initialPendingTasks}
|
initialPendingTasks={initialPendingTasks}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useState, useRef } from 'react';
|
|
||||||
import { DailyCheckboxType } from '@/lib/types';
|
|
||||||
import { Button } from '@/components/ui/Button';
|
|
||||||
import { Input } from '@/components/ui/Input';
|
|
||||||
|
|
||||||
interface DailyAddFormProps {
|
|
||||||
onAdd: (text: string, type: DailyCheckboxType) => Promise<void>;
|
|
||||||
disabled?: boolean;
|
|
||||||
placeholder?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DailyAddForm({ onAdd, disabled = false, placeholder = "Ajouter une tâche..." }: DailyAddFormProps) {
|
|
||||||
const [newCheckboxText, setNewCheckboxText] = useState('');
|
|
||||||
const [selectedType, setSelectedType] = useState<DailyCheckboxType>('meeting');
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const handleAddCheckbox = () => {
|
|
||||||
if (!newCheckboxText.trim()) return;
|
|
||||||
|
|
||||||
const text = newCheckboxText.trim();
|
|
||||||
|
|
||||||
// Vider et refocus IMMÉDIATEMENT pour l'UX optimiste
|
|
||||||
setNewCheckboxText('');
|
|
||||||
inputRef.current?.focus();
|
|
||||||
|
|
||||||
// Lancer l'ajout en arrière-plan (fire and forget)
|
|
||||||
onAdd(text, selectedType).catch(error => {
|
|
||||||
console.error('Erreur lors de l\'ajout:', error);
|
|
||||||
// En cas d'erreur, on pourrait restaurer le texte
|
|
||||||
// setNewCheckboxText(text);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyPress = (e: React.KeyboardEvent) => {
|
|
||||||
if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
handleAddCheckbox();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getPlaceholder = () => {
|
|
||||||
if (placeholder !== "Ajouter une tâche...") return placeholder;
|
|
||||||
return selectedType === 'meeting' ? 'Ajouter une réunion...' : 'Ajouter une tâche...';
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{/* Sélecteur de type */}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedType('task')}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={`flex items-center gap-1 text-xs border-l-4 ${
|
|
||||||
selectedType === 'task'
|
|
||||||
? 'border-l-green-500 bg-green-500/30 text-white font-medium'
|
|
||||||
: 'border-l-green-300 hover:border-l-green-400 opacity-70 hover:opacity-90'
|
|
||||||
}`}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
✅ Tâche
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
onClick={() => setSelectedType('meeting')}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
className={`flex items-center gap-1 text-xs border-l-4 ${
|
|
||||||
selectedType === 'meeting'
|
|
||||||
? 'border-l-blue-500 bg-blue-500/30 text-white font-medium'
|
|
||||||
: 'border-l-blue-300 hover:border-l-blue-400 opacity-70 hover:opacity-90'
|
|
||||||
}`}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
🗓️ Réunion
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Champ de saisie et options */}
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
ref={inputRef}
|
|
||||||
type="text"
|
|
||||||
placeholder={getPlaceholder()}
|
|
||||||
value={newCheckboxText}
|
|
||||||
onChange={(e) => setNewCheckboxText(e.target.value)}
|
|
||||||
onKeyDown={handleKeyPress}
|
|
||||||
disabled={disabled}
|
|
||||||
className="flex-1 min-w-[300px]"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={handleAddCheckbox}
|
|
||||||
disabled={!newCheckboxText.trim() || disabled}
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
className="min-w-[40px]"
|
|
||||||
>
|
|
||||||
+
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,8 @@ import { DailyCheckbox, DailyCheckboxType } from '@/lib/types';
|
|||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { DailyCheckboxSortable } from './DailyCheckboxSortable';
|
import { DailyCheckboxSortable } from './DailyCheckboxSortable';
|
||||||
import { DailyCheckboxItem } from './DailyCheckboxItem';
|
import { CheckboxItem, CheckboxItemData } from '@/components/ui/CheckboxItem';
|
||||||
import { DailyAddForm } from './DailyAddForm';
|
import { DailyAddForm, AddFormOption } from '@/components/ui/DailyAddForm';
|
||||||
import { DndContext, closestCenter, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core';
|
import { DndContext, closestCenter, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core';
|
||||||
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
|
import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
@@ -80,6 +80,22 @@ export function DailySection({
|
|||||||
|
|
||||||
const activeCheckbox = activeId ? items.find(item => item.id === activeId) : null;
|
const activeCheckbox = activeId ? items.find(item => item.id === activeId) : null;
|
||||||
|
|
||||||
|
// Options pour le formulaire d'ajout
|
||||||
|
const addFormOptions: AddFormOption[] = [
|
||||||
|
{ value: 'task', label: 'Tâche', icon: '✅', color: 'green' },
|
||||||
|
{ value: 'meeting', label: 'Réunion', icon: '🗓️', color: 'blue' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Convertir les checkboxes en format CheckboxItemData
|
||||||
|
const convertToCheckboxItemData = (checkbox: DailyCheckbox): CheckboxItemData => ({
|
||||||
|
id: checkbox.id,
|
||||||
|
text: checkbox.text,
|
||||||
|
isChecked: checkbox.isChecked,
|
||||||
|
type: checkbox.type,
|
||||||
|
taskId: checkbox.taskId,
|
||||||
|
task: checkbox.task
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DndContext
|
<DndContext
|
||||||
collisionDetection={closestCenter}
|
collisionDetection={closestCenter}
|
||||||
@@ -145,8 +161,11 @@ export function DailySection({
|
|||||||
{/* Footer - Formulaire d'ajout toujours en bas */}
|
{/* Footer - Formulaire d'ajout toujours en bas */}
|
||||||
<div className="p-4 pt-2 border-t border-[var(--border)]/30 bg-[var(--card)]/50">
|
<div className="p-4 pt-2 border-t border-[var(--border)]/30 bg-[var(--card)]/50">
|
||||||
<DailyAddForm
|
<DailyAddForm
|
||||||
onAdd={onAddCheckbox}
|
onAdd={(text, option) => onAddCheckbox(text, option as DailyCheckboxType)}
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
|
placeholder="Ajouter une tâche..."
|
||||||
|
options={addFormOptions}
|
||||||
|
defaultOption="task"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -160,12 +179,14 @@ export function DailySection({
|
|||||||
{activeCheckbox ? (
|
{activeCheckbox ? (
|
||||||
<div className="bg-[var(--card)] border border-[var(--border)] rounded-md shadow-xl opacity-95 transform rotate-3 scale-105">
|
<div className="bg-[var(--card)] border border-[var(--border)] rounded-md shadow-xl opacity-95 transform rotate-3 scale-105">
|
||||||
<div className="pl-4">
|
<div className="pl-4">
|
||||||
<DailyCheckboxItem
|
<CheckboxItem
|
||||||
checkbox={activeCheckbox}
|
item={convertToCheckboxItemData(activeCheckbox)}
|
||||||
onToggle={() => Promise.resolve()}
|
onToggle={() => Promise.resolve()}
|
||||||
onUpdate={() => Promise.resolve()}
|
onUpdate={() => Promise.resolve()}
|
||||||
onDelete={() => Promise.resolve()}
|
onDelete={() => Promise.resolve()}
|
||||||
saving={false}
|
saving={false}
|
||||||
|
showEditButton={false}
|
||||||
|
showDeleteButton={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { DeadlineTask, DeadlineMetrics } from '@/services/analytics/deadline-analytics';
|
|
||||||
import { Card } from '@/components/ui/Card';
|
|
||||||
|
|
||||||
// Fonction utilitaire pour combiner et trier les tâches urgentes
|
|
||||||
function combineAndSortUrgentTasks(metrics: DeadlineMetrics): DeadlineTask[] {
|
|
||||||
return [
|
|
||||||
...metrics.overdue,
|
|
||||||
...metrics.critical,
|
|
||||||
...metrics.warning
|
|
||||||
].sort((a, b) => {
|
|
||||||
// En retard d'abord, puis critique, puis attention
|
|
||||||
const urgencyOrder: Record<string, number> = { 'overdue': 0, 'critical': 1, 'warning': 2 };
|
|
||||||
if (urgencyOrder[a.urgencyLevel] !== urgencyOrder[b.urgencyLevel]) {
|
|
||||||
return urgencyOrder[a.urgencyLevel] - urgencyOrder[b.urgencyLevel];
|
|
||||||
}
|
|
||||||
// Si même urgence, trier par jours restants
|
|
||||||
return a.daysRemaining - b.daysRemaining;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeadlineReminderProps {
|
|
||||||
deadlineMetrics?: DeadlineMetrics | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function DeadlineReminder({ deadlineMetrics }: DeadlineReminderProps) {
|
|
||||||
// Ne rien afficher si pas de données ou pas de tâches urgentes
|
|
||||||
if (!deadlineMetrics) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const urgentTasks = combineAndSortUrgentTasks(deadlineMetrics);
|
|
||||||
|
|
||||||
if (urgentTasks.length === 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card className="mb-2">
|
|
||||||
<div className="outline-card-yellow p-4">
|
|
||||||
<div className="flex items-start gap-3">
|
|
||||||
<div className="text-2xl">⚠️</div>
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="text-sm font-semibold mb-2">
|
|
||||||
Rappel - Tâches urgentes ({urgentTasks.length})
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{urgentTasks.map((task, index) => (
|
|
||||||
<div
|
|
||||||
key={task.id}
|
|
||||||
className="inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium"
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'color-mix(in srgb, #eab308 15%, var(--card))',
|
|
||||||
borderColor: 'color-mix(in srgb, #eab308 35%, var(--border))',
|
|
||||||
border: '1px solid',
|
|
||||||
color: 'color-mix(in srgb, #eab308 85%, var(--foreground))'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>{getUrgencyIcon(task)}</span>
|
|
||||||
<span className="truncate max-w-[200px]" title={task.title}>
|
|
||||||
{task.title}
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] opacity-75">
|
|
||||||
({getUrgencyText(task)})
|
|
||||||
</span>
|
|
||||||
<span className="text-[10px] opacity-60">
|
|
||||||
{getSourceIcon(task.source)}
|
|
||||||
</span>
|
|
||||||
{index < urgentTasks.length - 1 && (
|
|
||||||
<span className="opacity-50">•</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-2 text-xs opacity-75">
|
|
||||||
Consultez la page d'accueil pour plus de détails sur les échéances
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fonctions utilitaires déplacées en dehors du composant
|
|
||||||
function getUrgencyIcon(task: DeadlineTask): string {
|
|
||||||
if (task.urgencyLevel === 'overdue') return '🔴';
|
|
||||||
if (task.urgencyLevel === 'critical') return '🟠';
|
|
||||||
return '🟡';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getUrgencyText(task: DeadlineTask): string {
|
|
||||||
if (task.urgencyLevel === 'overdue') {
|
|
||||||
return task.daysRemaining === -1 ? 'En retard de 1 jour' : `En retard de ${Math.abs(task.daysRemaining)} jours`;
|
|
||||||
} else if (task.urgencyLevel === 'critical') {
|
|
||||||
return task.daysRemaining === 0 ? 'Échéance aujourd\'hui' :
|
|
||||||
task.daysRemaining === 1 ? 'Échéance demain' :
|
|
||||||
`Dans ${task.daysRemaining} jours`;
|
|
||||||
} else {
|
|
||||||
return `Dans ${task.daysRemaining} jours`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSourceIcon(source: string): string {
|
|
||||||
switch (source) {
|
|
||||||
case 'jira': return '🔗';
|
|
||||||
case 'reminder': return '📱';
|
|
||||||
default: return '📋';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,12 +4,15 @@ import { useState } from 'react';
|
|||||||
import { ManagerSummary } from '@/services/analytics/manager-summary';
|
import { ManagerSummary } from '@/services/analytics/manager-summary';
|
||||||
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { TagDisplay } from '@/components/ui/TagDisplay';
|
import { MetricCard } from '@/components/ui/MetricCard';
|
||||||
import { getPriorityConfig } from '@/lib/status-config';
|
import { Tabs, TabItem } from '@/components/ui/Tabs';
|
||||||
|
import { AchievementCard } from '@/components/ui/AchievementCard';
|
||||||
|
import { ChallengeCard } from '@/components/ui/ChallengeCard';
|
||||||
import { useTasksContext } from '@/contexts/TasksContext';
|
import { useTasksContext } from '@/contexts/TasksContext';
|
||||||
import { MetricsTab } from './MetricsTab';
|
import { MetricsTab } from './MetricsTab';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { fr } from 'date-fns/locale';
|
import { fr } from 'date-fns/locale';
|
||||||
|
import { Tag } from '@/lib/types';
|
||||||
|
|
||||||
interface ManagerWeeklySummaryProps {
|
interface ManagerWeeklySummaryProps {
|
||||||
initialSummary: ManagerSummary;
|
initialSummary: ManagerSummary;
|
||||||
@@ -20,6 +23,10 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
|
|||||||
const [activeView, setActiveView] = useState<'narrative' | 'accomplishments' | 'challenges' | 'metrics'>('narrative');
|
const [activeView, setActiveView] = useState<'narrative' | 'accomplishments' | 'challenges' | 'metrics'>('narrative');
|
||||||
const { tags: availableTags } = useTasksContext();
|
const { tags: availableTags } = useTasksContext();
|
||||||
|
|
||||||
|
const handleTabChange = (tabId: string) => {
|
||||||
|
setActiveView(tabId as 'narrative' | 'accomplishments' | 'challenges' | 'metrics');
|
||||||
|
};
|
||||||
|
|
||||||
const handleRefresh = () => {
|
const handleRefresh = () => {
|
||||||
// SSR - refresh via page reload
|
// SSR - refresh via page reload
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
@@ -30,23 +37,13 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
|
|||||||
return `7 derniers jours (${format(summary.period.start, 'dd MMM', { locale: fr })} - ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })})`;
|
return `7 derniers jours (${format(summary.period.start, 'dd MMM', { locale: fr })} - ${format(summary.period.end, 'dd MMM yyyy', { locale: fr })})`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPriorityBadgeStyle = (priority: 'low' | 'medium' | 'high') => {
|
// Configuration des onglets
|
||||||
const config = getPriorityConfig(priority);
|
const tabItems: TabItem[] = [
|
||||||
const baseClasses = 'text-xs px-2 py-0.5 rounded font-medium border';
|
{ id: 'narrative', label: 'Vue Executive', icon: '📝' },
|
||||||
|
{ id: 'accomplishments', label: 'Accomplissements', icon: '✅', count: summary.keyAccomplishments.length },
|
||||||
switch (config.color) {
|
{ id: 'challenges', label: 'Enjeux à venir', icon: '🎯', count: summary.upcomingChallenges.length },
|
||||||
case 'blue':
|
{ id: 'metrics', label: 'Métriques', icon: '📊' }
|
||||||
return `${baseClasses} text-[var(--primary)] bg-[var(--primary)]/10 border-[var(--primary)]/20`;
|
];
|
||||||
case 'yellow':
|
|
||||||
return `${baseClasses} text-[var(--accent)] bg-[var(--accent)]/10 border-[var(--accent)]/20`;
|
|
||||||
case 'purple':
|
|
||||||
return `${baseClasses} text-[#8b5cf6] bg-[#8b5cf6]/10 border-[#8b5cf6]/20`;
|
|
||||||
case 'red':
|
|
||||||
return `${baseClasses} text-[var(--destructive)] bg-[var(--destructive)]/10 border-[var(--destructive)]/20`;
|
|
||||||
default:
|
|
||||||
return `${baseClasses} text-[var(--muted-foreground)] bg-[var(--muted)]/10 border-[var(--muted)]/20`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -67,50 +64,11 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Navigation des vues */}
|
{/* Navigation des vues */}
|
||||||
<div className="border-b border-[var(--border)]">
|
<Tabs
|
||||||
<nav className="flex space-x-8">
|
items={tabItems}
|
||||||
<button
|
activeTab={activeView}
|
||||||
onClick={() => setActiveView('narrative')}
|
onTabChange={handleTabChange}
|
||||||
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
/>
|
||||||
activeView === 'narrative'
|
|
||||||
? 'border-[var(--primary)] text-[var(--primary)]'
|
|
||||||
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
📝 Vue Executive
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveView('accomplishments')}
|
|
||||||
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
|
||||||
activeView === 'accomplishments'
|
|
||||||
? 'border-[var(--primary)] text-[var(--primary)]'
|
|
||||||
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
✅ Accomplissements ({summary.keyAccomplishments.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveView('challenges')}
|
|
||||||
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
|
||||||
activeView === 'challenges'
|
|
||||||
? 'border-[var(--primary)] text-[var(--primary)]'
|
|
||||||
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
🎯 Enjeux à venir ({summary.upcomingChallenges.length})
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveView('metrics')}
|
|
||||||
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
|
||||||
activeView === 'metrics'
|
|
||||||
? 'border-[var(--primary)] text-[var(--primary)]'
|
|
||||||
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
📊 Métriques
|
|
||||||
</button>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Vue Executive / Narrative */}
|
{/* Vue Executive / Narrative */}
|
||||||
{activeView === 'narrative' && (
|
{activeView === 'narrative' && (
|
||||||
@@ -147,45 +105,33 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
<div className="outline-metric-blue">
|
<MetricCard
|
||||||
<div className="text-2xl font-bold">
|
title="Tâches complétées"
|
||||||
{summary.metrics.totalTasksCompleted}
|
value={summary.metrics.totalTasksCompleted}
|
||||||
</div>
|
subtitle={`dont ${summary.metrics.highPriorityTasksCompleted} priorité haute`}
|
||||||
<div className="text-sm">Tâches complétées</div>
|
color="primary"
|
||||||
<div className="text-xs opacity-75">
|
/>
|
||||||
dont {summary.metrics.highPriorityTasksCompleted} priorité haute
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="outline-metric-green">
|
<MetricCard
|
||||||
<div className="text-2xl font-bold">
|
title="Todos complétés"
|
||||||
{summary.metrics.totalCheckboxesCompleted}
|
value={summary.metrics.totalCheckboxesCompleted}
|
||||||
</div>
|
subtitle={`dont ${summary.metrics.meetingCheckboxesCompleted} meetings`}
|
||||||
<div className="text-sm">Todos complétés</div>
|
color="success"
|
||||||
<div className="text-xs opacity-75">
|
/>
|
||||||
dont {summary.metrics.meetingCheckboxesCompleted} meetings
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="outline-metric-purple">
|
<MetricCard
|
||||||
<div className="text-2xl font-bold">
|
title="Items à fort impact"
|
||||||
{summary.keyAccomplishments.filter(a => a.impact === 'high').length}
|
value={summary.keyAccomplishments.filter(a => a.impact === 'high').length}
|
||||||
</div>
|
subtitle={`/ ${summary.keyAccomplishments.length} accomplissements`}
|
||||||
<div className="text-sm">Items à fort impact</div>
|
color="warning"
|
||||||
<div className="text-xs opacity-75">
|
/>
|
||||||
/ {summary.keyAccomplishments.length} accomplissements
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="outline-metric-orange">
|
<MetricCard
|
||||||
<div className="text-2xl font-bold">
|
title="Priorités critiques"
|
||||||
{summary.upcomingChallenges.filter(c => c.priority === 'high').length}
|
value={summary.upcomingChallenges.filter(c => c.priority === 'high').length}
|
||||||
</div>
|
subtitle={`/ ${summary.upcomingChallenges.length} enjeux`}
|
||||||
<div className="text-sm">Priorités critiques</div>
|
color="destructive"
|
||||||
<div className="text-xs opacity-75">
|
/>
|
||||||
/ {summary.upcomingChallenges.length} enjeux
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -204,60 +150,14 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
summary.keyAccomplishments.slice(0, 6).map((accomplishment, index) => (
|
summary.keyAccomplishments.slice(0, 6).map((accomplishment, index) => (
|
||||||
<div
|
<AchievementCard
|
||||||
key={accomplishment.id}
|
key={accomplishment.id}
|
||||||
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
|
achievement={accomplishment}
|
||||||
>
|
availableTags={availableTags as (Tag & { usage: number })[]}
|
||||||
{/* Barre colorée gauche */}
|
index={index}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
|
showDescription={true}
|
||||||
|
|
||||||
{/* Header compact */}
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--success)] bg-[var(--success)]/15 border border-[var(--success)]/25">
|
|
||||||
#{index + 1}
|
|
||||||
</span>
|
|
||||||
<span className={getPriorityBadgeStyle(accomplishment.impact)}>
|
|
||||||
{getPriorityConfig(accomplishment.impact).label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-[var(--muted-foreground)]">
|
|
||||||
{format(accomplishment.completedAt, 'dd/MM', { locale: fr })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Titre */}
|
|
||||||
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
|
||||||
{accomplishment.title}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
{accomplishment.tags && accomplishment.tags.length > 0 && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<TagDisplay
|
|
||||||
tags={accomplishment.tags}
|
|
||||||
availableTags={availableTags}
|
|
||||||
size="sm"
|
|
||||||
maxTags={2}
|
maxTags={2}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description si disponible */}
|
|
||||||
{accomplishment.description && (
|
|
||||||
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
|
|
||||||
{accomplishment.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Count de todos */}
|
|
||||||
{accomplishment.todosCount > 0 && (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
|
||||||
<span>📋</span>
|
|
||||||
<span>{accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -278,62 +178,14 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
summary.upcomingChallenges.slice(0, 6).map((challenge, index) => (
|
summary.upcomingChallenges.slice(0, 6).map((challenge, index) => (
|
||||||
<div
|
<ChallengeCard
|
||||||
key={challenge.id}
|
key={challenge.id}
|
||||||
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
|
challenge={challenge}
|
||||||
>
|
availableTags={availableTags as (Tag & { usage: number })[]}
|
||||||
{/* Barre colorée gauche */}
|
index={index}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
|
showDescription={true}
|
||||||
|
|
||||||
{/* Header compact */}
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--accent)] bg-[var(--accent)]/15 border border-[var(--accent)]/25">
|
|
||||||
#{index + 1}
|
|
||||||
</span>
|
|
||||||
<span className={getPriorityBadgeStyle(challenge.priority)}>
|
|
||||||
{getPriorityConfig(challenge.priority).label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{challenge.deadline && (
|
|
||||||
<span className="text-xs text-[var(--muted-foreground)]">
|
|
||||||
{format(challenge.deadline, 'dd/MM', { locale: fr })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Titre */}
|
|
||||||
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
|
||||||
{challenge.title}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
{challenge.tags && challenge.tags.length > 0 && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<TagDisplay
|
|
||||||
tags={challenge.tags}
|
|
||||||
availableTags={availableTags}
|
|
||||||
size="sm"
|
|
||||||
maxTags={2}
|
maxTags={2}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description si disponible */}
|
|
||||||
{challenge.description && (
|
|
||||||
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
|
|
||||||
{challenge.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Count de todos */}
|
|
||||||
{challenge.todosCount > 0 && (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
|
||||||
<span>📋</span>
|
|
||||||
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -354,60 +206,14 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{summary.keyAccomplishments.map((accomplishment, index) => (
|
{summary.keyAccomplishments.map((accomplishment, index) => (
|
||||||
<div
|
<AchievementCard
|
||||||
key={accomplishment.id}
|
key={accomplishment.id}
|
||||||
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
|
achievement={accomplishment}
|
||||||
>
|
availableTags={availableTags as (Tag & { usage: number })[]}
|
||||||
{/* Barre colorée gauche */}
|
index={index}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
|
showDescription={true}
|
||||||
|
|
||||||
{/* Header compact */}
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--success)] bg-[var(--success)]/15 border border-[var(--success)]/25">
|
|
||||||
#{index + 1}
|
|
||||||
</span>
|
|
||||||
<span className={getPriorityBadgeStyle(accomplishment.impact)}>
|
|
||||||
{getPriorityConfig(accomplishment.impact).label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-[var(--muted-foreground)]">
|
|
||||||
{format(accomplishment.completedAt, 'dd/MM', { locale: fr })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Titre */}
|
|
||||||
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
|
||||||
{accomplishment.title}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
{accomplishment.tags && accomplishment.tags.length > 0 && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<TagDisplay
|
|
||||||
tags={accomplishment.tags}
|
|
||||||
availableTags={availableTags}
|
|
||||||
size="sm"
|
|
||||||
maxTags={3}
|
maxTags={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description si disponible */}
|
|
||||||
{accomplishment.description && (
|
|
||||||
<p className="text-xs text-[var(--muted-foreground)] line-clamp-3 leading-relaxed mb-2">
|
|
||||||
{accomplishment.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Count de todos */}
|
|
||||||
{accomplishment.todosCount > 0 && (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
|
||||||
<span>📋</span>
|
|
||||||
<span>{accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -426,62 +232,14 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{summary.upcomingChallenges.map((challenge, index) => (
|
{summary.upcomingChallenges.map((challenge, index) => (
|
||||||
<div
|
<ChallengeCard
|
||||||
key={challenge.id}
|
key={challenge.id}
|
||||||
className="relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group"
|
challenge={challenge}
|
||||||
>
|
availableTags={availableTags as (Tag & { usage: number })[]}
|
||||||
{/* Barre colorée gauche */}
|
index={index}
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
|
showDescription={true}
|
||||||
|
|
||||||
{/* Header compact */}
|
|
||||||
<div className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--accent)] bg-[var(--accent)]/15 border border-[var(--accent)]/25">
|
|
||||||
#{index + 1}
|
|
||||||
</span>
|
|
||||||
<span className={getPriorityBadgeStyle(challenge.priority)}>
|
|
||||||
{getPriorityConfig(challenge.priority).label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{challenge.deadline && (
|
|
||||||
<span className="text-xs text-[var(--muted-foreground)]">
|
|
||||||
{format(challenge.deadline, 'dd/MM', { locale: fr })}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Titre */}
|
|
||||||
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
|
||||||
{challenge.title}
|
|
||||||
</h4>
|
|
||||||
|
|
||||||
{/* Tags */}
|
|
||||||
{challenge.tags && challenge.tags.length > 0 && (
|
|
||||||
<div className="mb-2">
|
|
||||||
<TagDisplay
|
|
||||||
tags={challenge.tags}
|
|
||||||
availableTags={availableTags}
|
|
||||||
size="sm"
|
|
||||||
maxTags={3}
|
maxTags={3}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Description si disponible */}
|
|
||||||
{challenge.description && (
|
|
||||||
<p className="text-xs text-[var(--muted-foreground)] line-clamp-3 leading-relaxed mb-2">
|
|
||||||
{challenge.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Count de todos */}
|
|
||||||
{challenge.todosCount > 0 && (
|
|
||||||
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
|
||||||
<span>📋</span>
|
|
||||||
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -4,15 +4,111 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Alert, AlertTitle, AlertDescription } from '@/components/ui/Alert';
|
import { Alert as ShadcnAlert, AlertTitle, AlertDescription } from '@/components/ui/Alert';
|
||||||
import { Input } from '@/components/ui/Input';
|
import { Input } from '@/components/ui/Input';
|
||||||
import { StyledCard } from '@/components/ui/StyledCard';
|
import { StyledCard } from '@/components/ui/StyledCard';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||||
import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone } from '@/components/ui';
|
import { StatCard, ProgressBar, ActionCard, TaskCard, MetricCard, ToggleButton, SearchInput, ControlPanel, ControlSection, ControlGroup, FilterSummary, FilterChip, ColumnHeader, EmptyState, DropZone, Tabs, PriorityBadge, AchievementCard, ChallengeCard } from '@/components/ui';
|
||||||
|
import { CheckboxItem, CheckboxItemData } from '@/components/ui/CheckboxItem';
|
||||||
|
import { Calendar } from '@/components/ui/Calendar';
|
||||||
|
import { DailyAddForm } from '@/components/ui/DailyAddForm';
|
||||||
|
import { AlertBanner, AlertItem } from '@/components/ui/AlertBanner';
|
||||||
|
import { CollapsibleSection, CollapsibleItem } from '@/components/ui/CollapsibleSection';
|
||||||
import { Header } from '@/components/ui/Header';
|
import { Header } from '@/components/ui/Header';
|
||||||
|
import { TabItem } from '@/components/ui/Tabs';
|
||||||
|
import { AchievementData } from '@/components/ui/AchievementCard';
|
||||||
|
import { ChallengeData } from '@/components/ui/ChallengeCard';
|
||||||
|
|
||||||
export function UIShowcaseClient() {
|
export function UIShowcaseClient() {
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
|
const [selectedDate, setSelectedDate] = useState(new Date());
|
||||||
|
const [checkboxItems, setCheckboxItems] = useState<CheckboxItemData[]>([
|
||||||
|
{ id: '1', text: 'Tâche complétée', isChecked: true, type: 'task' },
|
||||||
|
{ id: '2', text: 'Réunion importante', isChecked: false, type: 'meeting' },
|
||||||
|
{ id: '3', text: 'Tâche en cours', isChecked: false, type: 'task' }
|
||||||
|
]);
|
||||||
|
const [alertItems] = useState<AlertItem[]>([
|
||||||
|
{ id: '1', title: 'Tâche critique', icon: '🔴', urgency: 'critical', metadata: 'Dans 1 jour' },
|
||||||
|
{ id: '2', title: 'Réunion urgente', icon: '🟠', urgency: 'high', metadata: 'Dans 2 jours' },
|
||||||
|
{ id: '3', title: 'Rappel', icon: '🟡', urgency: 'medium', metadata: 'Dans 5 jours' }
|
||||||
|
]);
|
||||||
|
const [collapsibleItems] = useState<CollapsibleItem[]>([
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Tâche en attente',
|
||||||
|
subtitle: '15 Jan 2024',
|
||||||
|
metadata: 'Il y a 2 jours',
|
||||||
|
isChecked: false,
|
||||||
|
icon: '📋',
|
||||||
|
actions: [
|
||||||
|
{ label: 'Déplacer', icon: '📅', onClick: () => console.log('Move'), variant: 'primary' },
|
||||||
|
{ label: 'Supprimer', icon: '🗑️', onClick: () => console.log('Delete'), variant: 'destructive' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Données pour les composants Weekly Manager
|
||||||
|
const [activeTab, setActiveTab] = useState('tab1');
|
||||||
|
const tabItems: TabItem[] = [
|
||||||
|
{ id: 'tab1', label: 'Vue Executive', icon: '📝' },
|
||||||
|
{ id: 'tab2', label: 'Accomplissements', icon: '✅', count: 5 },
|
||||||
|
{ id: 'tab3', label: 'Enjeux', icon: '🎯', count: 3 },
|
||||||
|
{ id: 'tab4', label: 'Métriques', icon: '📊' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const sampleAchievements: AchievementData[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Refactoring de la page Daily',
|
||||||
|
description: 'Migration vers les composants UI génériques',
|
||||||
|
impact: 'high',
|
||||||
|
completedAt: new Date(),
|
||||||
|
tags: ['refactoring', 'ui'],
|
||||||
|
todosCount: 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Implémentation du système de thèmes',
|
||||||
|
description: 'Ajout de 10 nouveaux thèmes avec CSS variables',
|
||||||
|
impact: 'medium',
|
||||||
|
completedAt: new Date(Date.now() - 86400000),
|
||||||
|
tags: ['themes', 'css'],
|
||||||
|
todosCount: 3
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const sampleChallenges: ChallengeData[] = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: 'Migration vers Next.js 15',
|
||||||
|
description: 'Mise à jour majeure avec nouvelles fonctionnalités',
|
||||||
|
priority: 'high',
|
||||||
|
deadline: new Date(Date.now() + 7 * 86400000),
|
||||||
|
tags: ['migration', 'nextjs'],
|
||||||
|
todosCount: 12,
|
||||||
|
blockers: ['Tests à mettre à jour']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: 'Optimisation des performances',
|
||||||
|
description: 'Réduction du temps de chargement',
|
||||||
|
priority: 'medium',
|
||||||
|
deadline: new Date(Date.now() + 14 * 86400000),
|
||||||
|
tags: ['performance', 'optimization'],
|
||||||
|
todosCount: 5
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const sampleTags = [
|
||||||
|
{ id: '1', name: 'refactoring', color: '#3b82f6', usage: 5 },
|
||||||
|
{ id: '2', name: 'ui', color: '#10b981', usage: 8 },
|
||||||
|
{ id: '3', name: 'themes', color: '#8b5cf6', usage: 3 },
|
||||||
|
{ id: '4', name: 'css', color: '#f59e0b', usage: 4 },
|
||||||
|
{ id: '5', name: 'migration', color: '#ef4444', usage: 2 },
|
||||||
|
{ id: '6', name: 'nextjs', color: '#06b6d4', usage: 3 },
|
||||||
|
{ id: '7', name: 'performance', color: '#84cc16', usage: 6 },
|
||||||
|
{ id: '8', name: 'optimization', color: '#f97316', usage: 2 }
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[var(--background)]">
|
<div className="min-h-screen bg-[var(--background)]">
|
||||||
@@ -93,40 +189,40 @@ export function UIShowcaseClient() {
|
|||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Alert variant="default">
|
<ShadcnAlert variant="default">
|
||||||
<AlertTitle>Information</AlertTitle>
|
<AlertTitle>Information</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Ceci est une alerte par défaut avec des informations importantes.
|
Ceci est une alerte par défaut avec des informations importantes.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</ShadcnAlert>
|
||||||
|
|
||||||
<Alert variant="success">
|
<ShadcnAlert variant="success">
|
||||||
<AlertTitle>Succès</AlertTitle>
|
<AlertTitle>Succès</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Opération terminée avec succès ! Toutes les données ont été sauvegardées.
|
Opération terminée avec succès ! Toutes les données ont été sauvegardées.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</ShadcnAlert>
|
||||||
|
|
||||||
<Alert variant="destructive">
|
<ShadcnAlert variant="destructive">
|
||||||
<AlertTitle>Erreur</AlertTitle>
|
<AlertTitle>Erreur</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Une erreur s'est produite lors du traitement de votre demande.
|
Une erreur s'est produite lors du traitement de votre demande.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</ShadcnAlert>
|
||||||
|
|
||||||
<Alert variant="warning">
|
<ShadcnAlert variant="warning">
|
||||||
<AlertTitle>Attention</AlertTitle>
|
<AlertTitle>Attention</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Veuillez vérifier vos informations avant de continuer.
|
Veuillez vérifier vos informations avant de continuer.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</ShadcnAlert>
|
||||||
|
|
||||||
<Alert variant="info">
|
<ShadcnAlert variant="info">
|
||||||
<AlertTitle>Conseil</AlertTitle>
|
<AlertTitle>Conseil</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Astuce : Vous pouvez utiliser les raccourcis clavier pour naviguer plus rapidement.
|
Astuce : Vous pouvez utiliser les raccourcis clavier pour naviguer plus rapidement.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</ShadcnAlert>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -322,19 +418,19 @@ export function UIShowcaseClient() {
|
|||||||
<CardTitle>Notifications</CardTitle>
|
<CardTitle>Notifications</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<Alert variant="success">
|
<ShadcnAlert variant="success">
|
||||||
<AlertTitle>Bienvenue !</AlertTitle>
|
<AlertTitle>Bienvenue !</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Votre compte a été créé avec succès.
|
Votre compte a été créé avec succès.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</ShadcnAlert>
|
||||||
|
|
||||||
<Alert variant="warning">
|
<ShadcnAlert variant="warning">
|
||||||
<AlertTitle>Mise à jour disponible</AlertTitle>
|
<AlertTitle>Mise à jour disponible</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
Une nouvelle version de l'application est disponible.
|
Une nouvelle version de l'application est disponible.
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</ShadcnAlert>
|
||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button size="sm">Voir les détails</Button>
|
<Button size="sm">Voir les détails</Button>
|
||||||
@@ -985,6 +1081,247 @@ export function UIShowcaseClient() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Daily Components Section */}
|
||||||
|
<section className="space-y-8">
|
||||||
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
|
Daily Components
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* CheckboxItem */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">CheckboxItem</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
CheckboxItem - Élément de liste avec checkbox
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{checkboxItems.map((item) => (
|
||||||
|
<CheckboxItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onToggle={async (id) => {
|
||||||
|
setCheckboxItems(prev =>
|
||||||
|
prev.map(item =>
|
||||||
|
item.id === id ? { ...item, isChecked: !item.isChecked } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onUpdate={async (id, text) => {
|
||||||
|
setCheckboxItems(prev =>
|
||||||
|
prev.map(item =>
|
||||||
|
item.id === id ? { ...item, text } : item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onDelete={async (id) => {
|
||||||
|
setCheckboxItems(prev => prev.filter(item => item.id !== id));
|
||||||
|
}}
|
||||||
|
saving={false}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Calendar */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">Calendar</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
Calendar - Calendrier avec dates marquées
|
||||||
|
</div>
|
||||||
|
<div className="max-w-sm">
|
||||||
|
<Calendar
|
||||||
|
currentDate={selectedDate}
|
||||||
|
onDateSelect={setSelectedDate}
|
||||||
|
markedDates={['2024-01-15', '2024-01-20', '2024-01-25']}
|
||||||
|
showTodayButton={true}
|
||||||
|
showLegend={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* AddForm */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">DailyAddForm</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
DailyAddForm - Formulaire d'ajout avec options
|
||||||
|
</div>
|
||||||
|
<div className="max-w-md">
|
||||||
|
<DailyAddForm
|
||||||
|
onAdd={async (text, option) => {
|
||||||
|
console.log('Adding:', text, option);
|
||||||
|
const newItem: CheckboxItemData = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
text,
|
||||||
|
isChecked: false,
|
||||||
|
type: option as 'task' | 'meeting'
|
||||||
|
};
|
||||||
|
setCheckboxItems(prev => [...prev, newItem]);
|
||||||
|
}}
|
||||||
|
placeholder="Ajouter une tâche..."
|
||||||
|
options={[
|
||||||
|
{ value: 'task', label: 'Tâche', icon: '✅', color: 'green' },
|
||||||
|
{ value: 'meeting', label: 'Réunion', icon: '🗓️', color: 'blue' }
|
||||||
|
]}
|
||||||
|
defaultOption="task"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Alert */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">Alert</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
Alert - Alerte avec éléments urgents
|
||||||
|
</div>
|
||||||
|
<div className="max-w-md">
|
||||||
|
<AlertBanner
|
||||||
|
title="Tâches urgentes"
|
||||||
|
items={alertItems}
|
||||||
|
icon="⚠️"
|
||||||
|
variant="warning"
|
||||||
|
onItemClick={(item) => console.log('Clicked:', item)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* CollapsibleSection */}
|
||||||
|
<div className="space-y-6 lg:col-span-2">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">CollapsibleSection</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
CollapsibleSection - Section repliable avec éléments
|
||||||
|
</div>
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<CollapsibleSection
|
||||||
|
title="Tâches en attente"
|
||||||
|
items={collapsibleItems}
|
||||||
|
icon="📋"
|
||||||
|
defaultCollapsed={false}
|
||||||
|
loading={false}
|
||||||
|
emptyMessage="Aucune tâche en attente"
|
||||||
|
onRefresh={() => console.log('Refresh')}
|
||||||
|
onItemToggle={(id) => console.log('Toggle:', id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Weekly Manager Components Section */}
|
||||||
|
<section className="space-y-8">
|
||||||
|
<h2 className="text-2xl font-mono font-semibold text-[var(--foreground)] border-b border-[var(--border)] pb-3">
|
||||||
|
Weekly Manager Components
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* Tabs */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">Tabs</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
Tabs - Navigation par onglets
|
||||||
|
</div>
|
||||||
|
<Tabs
|
||||||
|
items={tabItems}
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
/>
|
||||||
|
<div className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
Onglet actif: {activeTab}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* PriorityBadge */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">PriorityBadge</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
PriorityBadge - Badge de priorité
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<PriorityBadge priority="high" />
|
||||||
|
<PriorityBadge priority="medium" />
|
||||||
|
<PriorityBadge priority="low" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
|
{/* AchievementCard */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">AchievementCard</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
AchievementCard - Carte d'accomplissement
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{sampleAchievements.map((achievement, index) => (
|
||||||
|
<AchievementCard
|
||||||
|
key={achievement.id}
|
||||||
|
achievement={achievement}
|
||||||
|
availableTags={sampleTags}
|
||||||
|
index={index}
|
||||||
|
showDescription={true}
|
||||||
|
maxTags={2}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ChallengeCard */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h3 className="text-lg font-medium text-[var(--foreground)]">ChallengeCard</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="text-xs font-mono text-[var(--muted-foreground)] bg-[var(--card)] px-2 py-1 rounded">
|
||||||
|
ChallengeCard - Carte de défi
|
||||||
|
</div>
|
||||||
|
<div className="space-y-3">
|
||||||
|
{sampleChallenges.map((challenge, index) => (
|
||||||
|
<ChallengeCard
|
||||||
|
key={challenge.id}
|
||||||
|
challenge={challenge}
|
||||||
|
availableTags={sampleTags}
|
||||||
|
index={index}
|
||||||
|
showDescription={true}
|
||||||
|
maxTags={2}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="text-center pt-8 border-t border-[var(--border)]">
|
<div className="text-center pt-8 border-t border-[var(--border)]">
|
||||||
<p className="text-[var(--muted-foreground)]">
|
<p className="text-[var(--muted-foreground)]">
|
||||||
|
|||||||
87
src/components/ui/AchievementCard.tsx
Normal file
87
src/components/ui/AchievementCard.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { fr } from 'date-fns/locale';
|
||||||
|
import { TagDisplay } from '@/components/ui/TagDisplay';
|
||||||
|
import { PriorityBadge } from '@/components/ui/PriorityBadge';
|
||||||
|
import { Tag } from '@/lib/types';
|
||||||
|
|
||||||
|
export interface AchievementData {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
impact: 'low' | 'medium' | 'high';
|
||||||
|
completedAt: Date;
|
||||||
|
tags?: string[];
|
||||||
|
todosCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AchievementCardProps {
|
||||||
|
achievement: AchievementData;
|
||||||
|
availableTags: (Tag & { usage: number })[];
|
||||||
|
index: number;
|
||||||
|
showDescription?: boolean;
|
||||||
|
maxTags?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AchievementCard({
|
||||||
|
achievement,
|
||||||
|
availableTags,
|
||||||
|
index,
|
||||||
|
showDescription = true,
|
||||||
|
maxTags = 2,
|
||||||
|
className = ''
|
||||||
|
}: AchievementCardProps) {
|
||||||
|
return (
|
||||||
|
<div className={`relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group ${className}`}>
|
||||||
|
{/* Barre colorée gauche */}
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-1 bg-green-500 rounded-l-lg"></div>
|
||||||
|
|
||||||
|
{/* Header compact */}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--success)] bg-[var(--success)]/15 border border-[var(--success)]/25">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
<PriorityBadge priority={achievement.impact} />
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
{format(achievement.completedAt, 'dd/MM', { locale: fr })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Titre */}
|
||||||
|
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
||||||
|
{achievement.title}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
{achievement.tags && achievement.tags.length > 0 && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<TagDisplay
|
||||||
|
tags={achievement.tags}
|
||||||
|
availableTags={availableTags as Tag[]}
|
||||||
|
size="sm"
|
||||||
|
maxTags={maxTags}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description si disponible */}
|
||||||
|
{showDescription && achievement.description && (
|
||||||
|
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
|
||||||
|
{achievement.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Count de todos */}
|
||||||
|
{achievement.todosCount && achievement.todosCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
||||||
|
<span>📋</span>
|
||||||
|
<span>{achievement.todosCount} todo{achievement.todosCount > 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,58 +1,49 @@
|
|||||||
import { HTMLAttributes, forwardRef } from 'react';
|
'use client';
|
||||||
import { cn } from '@/lib/utils';
|
|
||||||
|
|
||||||
interface AlertProps extends HTMLAttributes<HTMLDivElement> {
|
import { Card } from '@/components/ui/Card';
|
||||||
variant?: 'default' | 'success' | 'destructive' | 'warning' | 'info';
|
|
||||||
|
interface AlertProps {
|
||||||
|
variant?: 'default' | 'destructive' | 'success' | 'warning' | 'info';
|
||||||
|
className?: string;
|
||||||
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
export function Alert({ variant = 'default', className = '', children }: AlertProps) {
|
||||||
({ className, variant = 'default', ...props }, ref) => {
|
const getVariantClasses = () => {
|
||||||
const variants = {
|
switch (variant) {
|
||||||
default: 'bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)]',
|
case 'destructive':
|
||||||
success: 'bg-[color-mix(in_srgb,var(--success)_10%,transparent)] text-[var(--success)] border border-[color-mix(in_srgb,var(--success)_20%,var(--border))]',
|
return 'outline-card-red';
|
||||||
destructive: 'bg-[color-mix(in_srgb,var(--destructive)_10%,transparent)] text-[var(--destructive)] border border-[color-mix(in_srgb,var(--destructive)_20%,var(--border))]',
|
case 'success':
|
||||||
warning: 'bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] text-[var(--accent)] border border-[color-mix(in_srgb,var(--accent)_20%,var(--border))]',
|
return 'outline-card-green';
|
||||||
info: 'bg-[color-mix(in_srgb,var(--primary)_10%,transparent)] text-[var(--primary)] border border-[color-mix(in_srgb,var(--primary)_20%,var(--border))]'
|
case 'info':
|
||||||
|
return 'outline-card-blue';
|
||||||
|
case 'warning':
|
||||||
|
return 'outline-card-yellow';
|
||||||
|
case 'default':
|
||||||
|
default:
|
||||||
|
return 'outline-card-gray';
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Card className={`${getVariantClasses()} ${className}`}>
|
||||||
ref={ref}
|
{children}
|
||||||
className={cn(
|
</Card>
|
||||||
'relative w-full rounded-lg border p-4',
|
|
||||||
variants[variant],
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
Alert.displayName = 'Alert';
|
export function AlertTitle({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
||||||
|
return (
|
||||||
|
<h3 className={`text-sm font-semibold mb-2 ${className}`}>
|
||||||
|
{children}
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const AlertTitle = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLHeadingElement>>(
|
export function AlertDescription({ children, className = '' }: { children: React.ReactNode; className?: string }) {
|
||||||
({ className, ...props }, ref) => (
|
return (
|
||||||
<h5
|
<div className={`text-sm ${className}`}>
|
||||||
ref={ref}
|
{children}
|
||||||
className={cn('mb-1 font-medium leading-none tracking-tight', className)}
|
</div>
|
||||||
{...props}
|
);
|
||||||
/>
|
}
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
AlertTitle.displayName = 'AlertTitle';
|
|
||||||
|
|
||||||
const AlertDescription = forwardRef<HTMLParagraphElement, HTMLAttributes<HTMLParagraphElement>>(
|
|
||||||
({ className, ...props }, ref) => (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn('text-sm [&_p]:leading-relaxed', className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
AlertDescription.displayName = 'AlertDescription';
|
|
||||||
|
|
||||||
export { Alert, AlertTitle, AlertDescription };
|
|
||||||
137
src/components/ui/AlertBanner.tsx
Normal file
137
src/components/ui/AlertBanner.tsx
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Card } from '@/components/ui/Card';
|
||||||
|
|
||||||
|
export interface AlertItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
urgency?: 'low' | 'medium' | 'high' | 'critical';
|
||||||
|
source?: string;
|
||||||
|
metadata?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AlertProps {
|
||||||
|
title: string;
|
||||||
|
items: AlertItem[];
|
||||||
|
icon?: string;
|
||||||
|
variant?: 'info' | 'warning' | 'error' | 'success';
|
||||||
|
className?: string;
|
||||||
|
onItemClick?: (item: AlertItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AlertBanner({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
icon = '⚠️',
|
||||||
|
variant = 'warning',
|
||||||
|
className = '',
|
||||||
|
onItemClick
|
||||||
|
}: AlertProps) {
|
||||||
|
// Ne rien afficher si pas d'éléments
|
||||||
|
if (!items || items.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getVariantClasses = () => {
|
||||||
|
switch (variant) {
|
||||||
|
case 'error':
|
||||||
|
return 'outline-card-red';
|
||||||
|
case 'success':
|
||||||
|
return 'outline-card-green';
|
||||||
|
case 'info':
|
||||||
|
return 'outline-card-blue';
|
||||||
|
case 'warning':
|
||||||
|
default:
|
||||||
|
return 'outline-card-yellow';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getUrgencyColor = (urgency?: string) => {
|
||||||
|
switch (urgency) {
|
||||||
|
case 'critical':
|
||||||
|
return 'text-red-600';
|
||||||
|
case 'high':
|
||||||
|
return 'text-orange-600';
|
||||||
|
case 'medium':
|
||||||
|
return 'text-yellow-600';
|
||||||
|
case 'low':
|
||||||
|
return 'text-green-600';
|
||||||
|
default:
|
||||||
|
return 'text-gray-600';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSourceIcon = (source?: string) => {
|
||||||
|
switch (source) {
|
||||||
|
case 'jira':
|
||||||
|
return '🔗';
|
||||||
|
case 'reminder':
|
||||||
|
return '📱';
|
||||||
|
case 'tfs':
|
||||||
|
return '🔧';
|
||||||
|
default:
|
||||||
|
return '📋';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={`mb-2 ${className}`}>
|
||||||
|
<div className={`${getVariantClasses()} p-4`}>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="text-2xl">{icon}</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-sm font-semibold mb-2">
|
||||||
|
{title} ({items.length})
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{items.map((item, index) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={`inline-flex items-center gap-1 px-2 py-1 rounded-md text-xs font-medium cursor-pointer hover:opacity-80 transition-opacity ${
|
||||||
|
onItemClick ? 'hover:bg-[var(--card)]/50' : ''
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'color-mix(in srgb, var(--primary) 15%, var(--card))',
|
||||||
|
borderColor: 'color-mix(in srgb, var(--primary) 35%, var(--border))',
|
||||||
|
border: '1px solid',
|
||||||
|
color: 'color-mix(in srgb, var(--primary) 85%, var(--foreground))'
|
||||||
|
}}
|
||||||
|
onClick={() => onItemClick?.(item)}
|
||||||
|
title={item.title}
|
||||||
|
>
|
||||||
|
<span>{item.icon || getSourceIcon(item.source)}</span>
|
||||||
|
<span className="truncate max-w-[200px]">
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
{item.metadata && (
|
||||||
|
<span className="text-[10px] opacity-75">
|
||||||
|
({item.metadata})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{item.urgency && (
|
||||||
|
<span className={`text-[10px] ${getUrgencyColor(item.urgency)}`}>
|
||||||
|
{item.urgency === 'critical' ? '🔴' :
|
||||||
|
item.urgency === 'high' ? '🟠' :
|
||||||
|
item.urgency === 'medium' ? '🟡' : '🟢'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{index < items.length - 1 && (
|
||||||
|
<span className="opacity-50">•</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{items.length > 0 && (
|
||||||
|
<div className="mt-2 text-xs opacity-75">
|
||||||
|
Cliquez sur un élément pour plus de détails
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,17 +7,23 @@ import { formatDateForAPI, createDate, getToday } from '@/lib/date-utils';
|
|||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { fr } from 'date-fns/locale';
|
import { fr } from 'date-fns/locale';
|
||||||
|
|
||||||
interface DailyCalendarProps {
|
interface CalendarProps {
|
||||||
currentDate: Date;
|
currentDate: Date;
|
||||||
onDateSelect: (date: Date) => void;
|
onDateSelect: (date: Date) => void;
|
||||||
dailyDates: string[]; // Liste des dates qui ont des dailies (format YYYY-MM-DD)
|
markedDates?: string[]; // Liste des dates marquées (format YYYY-MM-DD)
|
||||||
|
showTodayButton?: boolean;
|
||||||
|
showLegend?: boolean;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DailyCalendar({
|
export function Calendar({
|
||||||
currentDate,
|
currentDate,
|
||||||
onDateSelect,
|
onDateSelect,
|
||||||
dailyDates,
|
markedDates = [],
|
||||||
}: DailyCalendarProps) {
|
showTodayButton = true,
|
||||||
|
showLegend = true,
|
||||||
|
className = ''
|
||||||
|
}: CalendarProps) {
|
||||||
const [viewDate, setViewDate] = useState(createDate(currentDate));
|
const [viewDate, setViewDate] = useState(createDate(currentDate));
|
||||||
|
|
||||||
// Formatage des dates pour comparaison (éviter le décalage timezone)
|
// Formatage des dates pour comparaison (éviter le décalage timezone)
|
||||||
@@ -90,8 +96,8 @@ export function DailyCalendar({
|
|||||||
return date.getMonth() === viewDate.getMonth();
|
return date.getMonth() === viewDate.getMonth();
|
||||||
};
|
};
|
||||||
|
|
||||||
const hasDaily = (date: Date) => {
|
const hasMarkedDate = (date: Date) => {
|
||||||
return dailyDates.includes(formatDateKey(date));
|
return markedDates.includes(formatDateKey(date));
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSelected = (date: Date) => {
|
const isSelected = (date: Date) => {
|
||||||
@@ -105,7 +111,7 @@ export function DailyCalendar({
|
|||||||
const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
|
const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim'];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="p-4">
|
<Card className={`p-4 ${className}`}>
|
||||||
{/* Header avec navigation */}
|
{/* Header avec navigation */}
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<Button
|
<Button
|
||||||
@@ -132,11 +138,13 @@ export function DailyCalendar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bouton Aujourd'hui */}
|
{/* Bouton Aujourd'hui */}
|
||||||
|
{showTodayButton && (
|
||||||
<div className="mb-4 text-center">
|
<div className="mb-4 text-center">
|
||||||
<Button onClick={goToToday} variant="primary" size="sm">
|
<Button onClick={goToToday} variant="primary" size="sm">
|
||||||
Aujourd'hui
|
Aujourd'hui
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Jours de la semaine */}
|
{/* Jours de la semaine */}
|
||||||
<div className="grid grid-cols-7 gap-1 mb-2">
|
<div className="grid grid-cols-7 gap-1 mb-2">
|
||||||
@@ -155,7 +163,7 @@ export function DailyCalendar({
|
|||||||
{days.map((date, index) => {
|
{days.map((date, index) => {
|
||||||
const isCurrentMonthDay = isCurrentMonth(date);
|
const isCurrentMonthDay = isCurrentMonth(date);
|
||||||
const isTodayDay = isTodayDate(date);
|
const isTodayDay = isTodayDate(date);
|
||||||
const hasCheckboxes = hasDaily(date);
|
const hasMarked = hasMarkedDate(date);
|
||||||
const isSelectedDay = isSelected(date);
|
const isSelectedDay = isSelected(date);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -175,13 +183,13 @@ export function DailyCalendar({
|
|||||||
: ''
|
: ''
|
||||||
}
|
}
|
||||||
${isSelectedDay ? 'bg-[var(--primary)] text-white' : ''}
|
${isSelectedDay ? 'bg-[var(--primary)] text-white' : ''}
|
||||||
${hasCheckboxes ? 'font-bold' : ''}
|
${hasMarked ? 'font-bold' : ''}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{date.getDate()}
|
{date.getDate()}
|
||||||
|
|
||||||
{/* Indicateur de daily existant */}
|
{/* Indicateur de date marquée */}
|
||||||
{hasCheckboxes && (
|
{hasMarked && (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
absolute bottom-1 right-1 w-2 h-2 rounded-full
|
absolute bottom-1 right-1 w-2 h-2 rounded-full
|
||||||
@@ -195,16 +203,18 @@ export function DailyCalendar({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Légende */}
|
{/* Légende */}
|
||||||
|
{showLegend && (
|
||||||
<div className="mt-4 text-xs text-[var(--muted-foreground)] space-y-1">
|
<div className="mt-4 text-xs text-[var(--muted-foreground)] space-y-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-2 h-2 rounded-full bg-[var(--primary)]"></div>
|
<div className="w-2 h-2 rounded-full bg-[var(--primary)]"></div>
|
||||||
<span>Jour avec des tâches</span>
|
<span>Jour avec des éléments</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 rounded border border-[var(--primary)] bg-[var(--primary)]/20"></div>
|
<div className="w-4 h-4 rounded border border-[var(--primary)] bg-[var(--primary)]/20"></div>
|
||||||
<span>Aujourd'hui</span>
|
<span>Aujourd'hui</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
90
src/components/ui/ChallengeCard.tsx
Normal file
90
src/components/ui/ChallengeCard.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { format } from 'date-fns';
|
||||||
|
import { fr } from 'date-fns/locale';
|
||||||
|
import { TagDisplay } from '@/components/ui/TagDisplay';
|
||||||
|
import { PriorityBadge } from '@/components/ui/PriorityBadge';
|
||||||
|
import { Tag } from '@/lib/types';
|
||||||
|
|
||||||
|
export interface ChallengeData {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
priority: 'low' | 'medium' | 'high';
|
||||||
|
deadline?: Date;
|
||||||
|
tags?: string[];
|
||||||
|
todosCount?: number;
|
||||||
|
blockers?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ChallengeCardProps {
|
||||||
|
challenge: ChallengeData;
|
||||||
|
availableTags: (Tag & { usage: number })[];
|
||||||
|
index: number;
|
||||||
|
showDescription?: boolean;
|
||||||
|
maxTags?: number;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ChallengeCard({
|
||||||
|
challenge,
|
||||||
|
availableTags,
|
||||||
|
index,
|
||||||
|
showDescription = true,
|
||||||
|
maxTags = 2,
|
||||||
|
className = ''
|
||||||
|
}: ChallengeCardProps) {
|
||||||
|
return (
|
||||||
|
<div className={`relative bg-[var(--card)] border border-[var(--border)] rounded-lg p-3 transition-all duration-200 group ${className}`}>
|
||||||
|
{/* Barre colorée gauche */}
|
||||||
|
<div className="absolute left-0 top-0 bottom-0 w-1 bg-orange-500 rounded-l-lg"></div>
|
||||||
|
|
||||||
|
{/* Header compact */}
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="w-5 h-5 rounded-full text-xs font-bold flex items-center justify-center text-[var(--accent)] bg-[var(--accent)]/15 border border-[var(--accent)]/25">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
<PriorityBadge priority={challenge.priority} />
|
||||||
|
</div>
|
||||||
|
{challenge.deadline && (
|
||||||
|
<span className="text-xs text-[var(--muted-foreground)]">
|
||||||
|
{format(challenge.deadline, 'dd/MM', { locale: fr })}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Titre */}
|
||||||
|
<h4 className="font-semibold text-sm text-[var(--foreground)] mb-2 line-clamp-2">
|
||||||
|
{challenge.title}
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
{/* Tags */}
|
||||||
|
{challenge.tags && challenge.tags.length > 0 && (
|
||||||
|
<div className="mb-2">
|
||||||
|
<TagDisplay
|
||||||
|
tags={challenge.tags}
|
||||||
|
availableTags={availableTags as Tag[]}
|
||||||
|
size="sm"
|
||||||
|
maxTags={maxTags}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Description si disponible */}
|
||||||
|
{showDescription && challenge.description && (
|
||||||
|
<p className="text-xs text-[var(--muted-foreground)] line-clamp-2 leading-relaxed mb-2">
|
||||||
|
{challenge.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Count de todos */}
|
||||||
|
{challenge.todosCount && challenge.todosCount > 0 && (
|
||||||
|
<div className="flex items-center gap-1 text-xs text-[var(--muted-foreground)]">
|
||||||
|
<span>📋</span>
|
||||||
|
<span>{challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
193
src/components/ui/CheckboxItem.tsx
Normal file
193
src/components/ui/CheckboxItem.tsx
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
|
||||||
|
export interface CheckboxItemData {
|
||||||
|
id: string;
|
||||||
|
text: string;
|
||||||
|
isChecked: boolean;
|
||||||
|
type?: 'task' | 'meeting' | string;
|
||||||
|
taskId?: string;
|
||||||
|
task?: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CheckboxItemProps {
|
||||||
|
item: CheckboxItemData;
|
||||||
|
onToggle: (itemId: string) => Promise<void>;
|
||||||
|
onUpdate: (itemId: string, text: string, type?: string, taskId?: string) => Promise<void>;
|
||||||
|
onDelete: (itemId: string) => Promise<void>;
|
||||||
|
saving?: boolean;
|
||||||
|
showTypeIndicator?: boolean;
|
||||||
|
showTaskLink?: boolean;
|
||||||
|
showEditButton?: boolean;
|
||||||
|
showDeleteButton?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CheckboxItem({
|
||||||
|
item,
|
||||||
|
onToggle,
|
||||||
|
onUpdate,
|
||||||
|
onDelete,
|
||||||
|
saving = false,
|
||||||
|
showTaskLink = true,
|
||||||
|
showEditButton = true,
|
||||||
|
showDeleteButton = true,
|
||||||
|
className = ''
|
||||||
|
}: CheckboxItemProps) {
|
||||||
|
const [inlineEditingId, setInlineEditingId] = useState<string | null>(null);
|
||||||
|
const [inlineEditingText, setInlineEditingText] = useState('');
|
||||||
|
const [optimisticChecked, setOptimisticChecked] = useState<boolean | null>(null);
|
||||||
|
|
||||||
|
// État optimiste local pour une réponse immédiate
|
||||||
|
const isChecked = optimisticChecked !== null ? optimisticChecked : item.isChecked;
|
||||||
|
|
||||||
|
// Synchroniser l'état optimiste avec les changements externes
|
||||||
|
useEffect(() => {
|
||||||
|
if (optimisticChecked !== null && optimisticChecked === item.isChecked) {
|
||||||
|
// L'état serveur a été mis à jour, on peut reset l'optimiste
|
||||||
|
setOptimisticChecked(null);
|
||||||
|
}
|
||||||
|
}, [item.isChecked, optimisticChecked]);
|
||||||
|
|
||||||
|
// Handler optimiste pour le toggle
|
||||||
|
const handleOptimisticToggle = async () => {
|
||||||
|
const newCheckedState = !isChecked;
|
||||||
|
|
||||||
|
// Mise à jour optimiste immédiate
|
||||||
|
setOptimisticChecked(newCheckedState);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onToggle(item.id);
|
||||||
|
// Reset l'état optimiste après succès
|
||||||
|
setOptimisticChecked(null);
|
||||||
|
} catch (error) {
|
||||||
|
// Rollback en cas d'erreur
|
||||||
|
setOptimisticChecked(null);
|
||||||
|
console.error('Erreur lors du toggle:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Édition inline simple
|
||||||
|
const handleStartInlineEdit = () => {
|
||||||
|
setInlineEditingId(item.id);
|
||||||
|
setInlineEditingText(item.text);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveInlineEdit = async () => {
|
||||||
|
if (!inlineEditingText.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await onUpdate(item.id, inlineEditingText.trim(), item.type, item.taskId);
|
||||||
|
setInlineEditingId(null);
|
||||||
|
setInlineEditingText('');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la modification:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancelInlineEdit = () => {
|
||||||
|
setInlineEditingId(null);
|
||||||
|
setInlineEditingText('');
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInlineEditKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSaveInlineEdit();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCancelInlineEdit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Obtenir la couleur de bordure selon le type
|
||||||
|
const getTypeBorderColor = () => {
|
||||||
|
if (item.type === 'meeting') return 'border-l-blue-500';
|
||||||
|
return 'border-l-green-500';
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`flex items-center gap-3 px-3 py-2 sm:py-1.5 sm:gap-2 rounded border transition-colors group border-l-4 ${getTypeBorderColor()} border-t-[var(--border)]/30 border-r-[var(--border)]/30 border-b-[var(--border)]/30 hover:border-t-[var(--border)] hover:border-r-[var(--border)] hover:border-b-[var(--border)] ${className}`}>
|
||||||
|
{/* Checkbox */}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isChecked}
|
||||||
|
onChange={handleOptimisticToggle}
|
||||||
|
disabled={saving}
|
||||||
|
className="w-4 h-4 md:w-3.5 md:h-3.5 rounded border border-[var(--border)] text-[var(--primary)] focus:ring-[var(--primary)]/20 focus:ring-1"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Contenu principal */}
|
||||||
|
{inlineEditingId === item.id ? (
|
||||||
|
<Input
|
||||||
|
value={inlineEditingText}
|
||||||
|
onChange={(e) => setInlineEditingText(e.target.value)}
|
||||||
|
onKeyDown={handleInlineEditKeyPress}
|
||||||
|
onBlur={handleSaveInlineEdit}
|
||||||
|
autoFocus
|
||||||
|
className="flex-1 h-7 text-sm"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex items-center gap-2">
|
||||||
|
{/* Texte cliquable pour édition inline */}
|
||||||
|
<span
|
||||||
|
className={`flex-1 text-sm sm:text-xs font-mono transition-all cursor-pointer hover:bg-[var(--muted)]/50 py-0.5 px-1 rounded ${
|
||||||
|
item.isChecked
|
||||||
|
? 'line-through text-[var(--muted-foreground)]'
|
||||||
|
: 'text-[var(--foreground)]'
|
||||||
|
}`}
|
||||||
|
onClick={handleStartInlineEdit}
|
||||||
|
title="Cliquer pour éditer le texte"
|
||||||
|
>
|
||||||
|
{item.text}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Icône d'édition avancée */}
|
||||||
|
{showEditButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
// Pour l'instant, on utilise l'édition inline
|
||||||
|
// Plus tard, on pourra ajouter une modal d'édition avancée
|
||||||
|
handleStartInlineEdit();
|
||||||
|
}}
|
||||||
|
disabled={saving}
|
||||||
|
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded-full bg-[var(--muted)]/50 hover:bg-[var(--muted)] border border-[var(--border)]/30 hover:border-[var(--border)] flex items-center justify-center transition-all duration-200 text-[var(--foreground)] text-xs"
|
||||||
|
title="Éditer le texte"
|
||||||
|
>
|
||||||
|
✏️
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lien vers la tâche si liée */}
|
||||||
|
{showTaskLink && item.task && (
|
||||||
|
<Link
|
||||||
|
href={`/kanban?taskId=${item.task.id}`}
|
||||||
|
className="text-xs text-[var(--primary)] hover:text-[var(--primary)]/80 font-mono truncate max-w-[100px]"
|
||||||
|
title={`Tâche: ${item.task.title}`}
|
||||||
|
>
|
||||||
|
{item.task.title}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Bouton de suppression */}
|
||||||
|
{showDeleteButton && (
|
||||||
|
<button
|
||||||
|
onClick={() => onDelete(item.id)}
|
||||||
|
disabled={saving}
|
||||||
|
className="opacity-0 group-hover:opacity-100 w-5 h-5 rounded-full bg-[var(--destructive)]/20 hover:bg-[var(--destructive)]/30 border border-[var(--destructive)]/30 hover:border-[var(--destructive)]/50 flex items-center justify-center transition-all duration-200 text-[var(--destructive)] text-xs"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
224
src/components/ui/CollapsibleSection.tsx
Normal file
224
src/components/ui/CollapsibleSection.tsx
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
|
||||||
|
export interface CollapsibleItem {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
metadata?: string;
|
||||||
|
isChecked?: boolean;
|
||||||
|
isArchived?: boolean;
|
||||||
|
icon?: string;
|
||||||
|
actions?: Array<{
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
onClick: () => void;
|
||||||
|
variant?: 'primary' | 'secondary' | 'destructive';
|
||||||
|
disabled?: boolean;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CollapsibleSectionProps {
|
||||||
|
title: string;
|
||||||
|
items: CollapsibleItem[];
|
||||||
|
icon?: string;
|
||||||
|
defaultCollapsed?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
emptyMessage?: string;
|
||||||
|
filters?: Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
options: Array<{ value: string; label: string }>;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}>;
|
||||||
|
onRefresh?: () => void;
|
||||||
|
onItemToggle?: (itemId: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CollapsibleSection({
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
icon = '📋',
|
||||||
|
defaultCollapsed = false,
|
||||||
|
loading = false,
|
||||||
|
emptyMessage = 'Aucun élément',
|
||||||
|
filters = [],
|
||||||
|
onRefresh,
|
||||||
|
onItemToggle,
|
||||||
|
className = ''
|
||||||
|
}: CollapsibleSectionProps) {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState(defaultCollapsed);
|
||||||
|
|
||||||
|
const handleItemToggle = (itemId: string) => {
|
||||||
|
onItemToggle?.(itemId);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getItemClasses = (item: CollapsibleItem) => {
|
||||||
|
let classes = 'flex items-center gap-3 p-3 rounded-lg border border-[var(--border)]';
|
||||||
|
|
||||||
|
if (item.isArchived) {
|
||||||
|
classes += ' opacity-60 bg-[var(--muted)]/20';
|
||||||
|
} else {
|
||||||
|
classes += ' bg-[var(--card)]';
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCheckboxClasses = (item: CollapsibleItem) => {
|
||||||
|
let classes = 'w-5 h-5 rounded border-2 flex items-center justify-center transition-colors';
|
||||||
|
|
||||||
|
if (item.isArchived) {
|
||||||
|
classes += ' border-[var(--muted)] cursor-not-allowed';
|
||||||
|
} else {
|
||||||
|
classes += ' border-[var(--border)] hover:border-[var(--primary)]';
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getActionClasses = (action: NonNullable<CollapsibleItem['actions']>[0]) => {
|
||||||
|
let classes = 'text-xs px-2 py-1';
|
||||||
|
|
||||||
|
switch (action.variant) {
|
||||||
|
case 'destructive':
|
||||||
|
classes += ' text-[var(--destructive)] hover:text-[var(--destructive)]';
|
||||||
|
break;
|
||||||
|
case 'primary':
|
||||||
|
classes += ' text-[var(--primary)] hover:text-[var(--primary)]';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
classes += ' text-[var(--foreground)]';
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className={`mt-6 ${className}`}>
|
||||||
|
<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>
|
||||||
|
{icon} {title}
|
||||||
|
{items.length > 0 && (
|
||||||
|
<span className="bg-[var(--warning)] text-[var(--warning-foreground)] px-2 py-1 rounded-full text-xs font-medium">
|
||||||
|
{items.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{/* Filtres */}
|
||||||
|
{filters.map((filter, index) => (
|
||||||
|
<select
|
||||||
|
key={index}
|
||||||
|
value={filter.value}
|
||||||
|
onChange={(e) => filter.onChange(e.target.value)}
|
||||||
|
className="text-xs px-2 py-1 border border-[var(--border)] rounded bg-[var(--background)]"
|
||||||
|
>
|
||||||
|
{filter.options.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{/* Bouton refresh */}
|
||||||
|
{onRefresh && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onRefresh}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? '🔄' : '↻'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
{!isCollapsed && (
|
||||||
|
<CardContent>
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center py-4 text-[var(--muted-foreground)]">
|
||||||
|
Chargement...
|
||||||
|
</div>
|
||||||
|
) : items.length === 0 ? (
|
||||||
|
<div className="text-center py-4 text-[var(--muted-foreground)]">
|
||||||
|
🎉 {emptyMessage} ! Excellent travail.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2">
|
||||||
|
{items.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={getItemClasses(item)}
|
||||||
|
>
|
||||||
|
{/* Checkbox */}
|
||||||
|
{item.isChecked !== undefined && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleItemToggle(item.id)}
|
||||||
|
disabled={item.isArchived}
|
||||||
|
className={getCheckboxClasses(item)}
|
||||||
|
>
|
||||||
|
{item.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">
|
||||||
|
{item.icon && <span>{item.icon}</span>}
|
||||||
|
<span className={`text-sm font-medium ${item.isArchived ? 'line-through' : ''}`}>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{(item.subtitle || item.metadata) && (
|
||||||
|
<div className="flex items-center gap-3 text-xs text-[var(--muted-foreground)]">
|
||||||
|
{item.subtitle && <span>{item.subtitle}</span>}
|
||||||
|
{item.metadata && <span>{item.metadata}</span>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
{item.actions && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
{item.actions.map((action, index) => (
|
||||||
|
<Button
|
||||||
|
key={index}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={action.onClick}
|
||||||
|
disabled={action.disabled}
|
||||||
|
title={action.label}
|
||||||
|
className={getActionClasses(action)}
|
||||||
|
>
|
||||||
|
{action.icon}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
157
src/components/ui/DailyAddForm.tsx
Normal file
157
src/components/ui/DailyAddForm.tsx
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useRef } from 'react';
|
||||||
|
import { Button } from '@/components/ui/Button';
|
||||||
|
import { Input } from '@/components/ui/Input';
|
||||||
|
|
||||||
|
export interface AddFormOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddFormProps {
|
||||||
|
onAdd: (text: string, option?: string) => Promise<void>;
|
||||||
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
|
options?: AddFormOption[];
|
||||||
|
defaultOption?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DailyAddForm({
|
||||||
|
onAdd,
|
||||||
|
disabled = false,
|
||||||
|
placeholder = "Ajouter un élément...",
|
||||||
|
options = [],
|
||||||
|
defaultOption,
|
||||||
|
className = ''
|
||||||
|
}: AddFormProps) {
|
||||||
|
const [newItemText, setNewItemText] = useState('');
|
||||||
|
const [selectedOption, setSelectedOption] = useState<string>(defaultOption || (options.length > 0 ? options[0].value : ''));
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleAddItem = () => {
|
||||||
|
if (!newItemText.trim()) return;
|
||||||
|
|
||||||
|
const text = newItemText.trim();
|
||||||
|
|
||||||
|
// Vider et refocus IMMÉDIATEMENT pour l'UX optimiste
|
||||||
|
setNewItemText('');
|
||||||
|
inputRef.current?.focus();
|
||||||
|
|
||||||
|
// Lancer l'ajout en arrière-plan (fire and forget)
|
||||||
|
onAdd(text, selectedOption).catch(error => {
|
||||||
|
console.error('Erreur lors de l\'ajout:', error);
|
||||||
|
// En cas d'erreur, on pourrait restaurer le texte
|
||||||
|
// setNewItemText(text);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyPress = (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleAddItem();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlaceholder = () => {
|
||||||
|
if (placeholder !== "Ajouter un élément...") return placeholder;
|
||||||
|
|
||||||
|
if (options.length > 0) {
|
||||||
|
const selectedOptionData = options.find(opt => opt.value === selectedOption);
|
||||||
|
if (selectedOptionData) {
|
||||||
|
return `Ajouter ${selectedOptionData.label.toLowerCase()}...`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return placeholder;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOptionColor = (option: AddFormOption) => {
|
||||||
|
if (option.color) return option.color;
|
||||||
|
|
||||||
|
// Couleurs par défaut selon le type
|
||||||
|
switch (option.value) {
|
||||||
|
case 'task':
|
||||||
|
return 'green';
|
||||||
|
case 'meeting':
|
||||||
|
return 'blue';
|
||||||
|
default:
|
||||||
|
return 'gray';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOptionClasses = (option: AddFormOption) => {
|
||||||
|
const color = getOptionColor(option);
|
||||||
|
const isSelected = selectedOption === option.value;
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
switch (color) {
|
||||||
|
case 'green':
|
||||||
|
return 'border-l-green-500 bg-green-500/30 text-white font-medium';
|
||||||
|
case 'blue':
|
||||||
|
return 'border-l-blue-500 bg-blue-500/30 text-white font-medium';
|
||||||
|
default:
|
||||||
|
return 'border-l-gray-500 bg-gray-500/30 text-white font-medium';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch (color) {
|
||||||
|
case 'green':
|
||||||
|
return 'border-l-green-300 hover:border-l-green-400 opacity-70 hover:opacity-90';
|
||||||
|
case 'blue':
|
||||||
|
return 'border-l-blue-300 hover:border-l-blue-400 opacity-70 hover:opacity-90';
|
||||||
|
default:
|
||||||
|
return 'border-l-gray-300 hover:border-l-gray-400 opacity-70 hover:opacity-90';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`space-y-2 ${className}`}>
|
||||||
|
{/* Sélecteur d'options */}
|
||||||
|
{options.length > 0 && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{options.map((option) => (
|
||||||
|
<Button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedOption(option.value)}
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className={`flex items-center gap-1 text-xs border-l-4 ${getOptionClasses(option)}`}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{option.icon && <span>{option.icon}</span>}
|
||||||
|
{option.label}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Champ de saisie et bouton d'ajout */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Input
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
placeholder={getPlaceholder()}
|
||||||
|
value={newItemText}
|
||||||
|
onChange={(e) => setNewItemText(e.target.value)}
|
||||||
|
onKeyDown={handleKeyPress}
|
||||||
|
disabled={disabled}
|
||||||
|
className="flex-1 min-w-[300px]"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={handleAddItem}
|
||||||
|
disabled={!newItemText.trim() || disabled}
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="min-w-[40px]"
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
37
src/components/ui/PriorityBadge.tsx
Normal file
37
src/components/ui/PriorityBadge.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { getPriorityConfig } from '@/lib/status-config';
|
||||||
|
|
||||||
|
interface PriorityBadgeProps {
|
||||||
|
priority: 'low' | 'medium' | 'high';
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PriorityBadge({ priority, className = '' }: PriorityBadgeProps) {
|
||||||
|
const config = getPriorityConfig(priority);
|
||||||
|
const baseClasses = 'text-xs px-2 py-0.5 rounded font-medium border';
|
||||||
|
|
||||||
|
let colorClasses = '';
|
||||||
|
switch (config.color) {
|
||||||
|
case 'blue':
|
||||||
|
colorClasses = 'text-[var(--primary)] bg-[var(--primary)]/10 border-[var(--primary)]/20';
|
||||||
|
break;
|
||||||
|
case 'yellow':
|
||||||
|
colorClasses = 'text-[var(--accent)] bg-[var(--accent)]/10 border-[var(--accent)]/20';
|
||||||
|
break;
|
||||||
|
case 'purple':
|
||||||
|
colorClasses = 'text-[#8b5cf6] bg-[#8b5cf6]/10 border-[#8b5cf6]/20';
|
||||||
|
break;
|
||||||
|
case 'red':
|
||||||
|
colorClasses = 'text-[var(--destructive)] bg-[var(--destructive)]/10 border-[var(--destructive)]/20';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
colorClasses = 'text-[var(--muted-foreground)] bg-[var(--muted)]/10 border-[var(--muted)]/20';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={`${baseClasses} ${colorClasses} ${className}`}>
|
||||||
|
{config.label}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
src/components/ui/Tabs.tsx
Normal file
45
src/components/ui/Tabs.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
export interface TabItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
icon?: string;
|
||||||
|
count?: number;
|
||||||
|
disabled?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TabsProps {
|
||||||
|
items: TabItem[];
|
||||||
|
activeTab: string;
|
||||||
|
onTabChange: (tabId: string) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Tabs({ items, activeTab, onTabChange, className = '' }: TabsProps) {
|
||||||
|
return (
|
||||||
|
<div className={`border-b border-[var(--border)] ${className}`}>
|
||||||
|
<nav className="flex space-x-8">
|
||||||
|
{items.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => !item.disabled && onTabChange(item.id)}
|
||||||
|
disabled={item.disabled}
|
||||||
|
className={`py-3 px-1 border-b-2 font-medium text-sm transition-colors ${
|
||||||
|
activeTab === item.id
|
||||||
|
? 'border-[var(--primary)] text-[var(--primary)]'
|
||||||
|
: 'border-transparent text-[var(--muted-foreground)] hover:text-[var(--foreground)]'
|
||||||
|
} ${item.disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer'}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{item.icon && <span>{item.icon}</span>}
|
||||||
|
<span>{item.label}</span>
|
||||||
|
{item.count !== undefined && (
|
||||||
|
<span className="text-xs opacity-75">({item.count})</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -22,6 +22,19 @@ export { ColumnHeader } from './ColumnHeader';
|
|||||||
export { EmptyState } from './EmptyState';
|
export { EmptyState } from './EmptyState';
|
||||||
export { DropZone } from './DropZone';
|
export { DropZone } from './DropZone';
|
||||||
|
|
||||||
|
// Composants Weekly Manager
|
||||||
|
export { Tabs } from './Tabs';
|
||||||
|
export { PriorityBadge } from './PriorityBadge';
|
||||||
|
export { AchievementCard } from './AchievementCard';
|
||||||
|
export { ChallengeCard } from './ChallengeCard';
|
||||||
|
|
||||||
|
// Composants Daily
|
||||||
|
export { CheckboxItem } from './CheckboxItem';
|
||||||
|
export { Calendar } from './Calendar';
|
||||||
|
export { DailyAddForm } from './DailyAddForm';
|
||||||
|
export { AlertBanner } from './AlertBanner';
|
||||||
|
export { CollapsibleSection } from './CollapsibleSection';
|
||||||
|
|
||||||
// Composants existants
|
// Composants existants
|
||||||
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card';
|
||||||
export { FontSizeToggle } from './FontSizeToggle';
|
export { FontSizeToggle } from './FontSizeToggle';
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { TfsConfig } from '@/services/integrations/tfs';
|
import { TfsConfig } from '@/services/integrations/tfs';
|
||||||
import { Theme } from './theme-config';
|
import { Theme } from './theme-config';
|
||||||
|
|
||||||
|
// Réexporter Theme pour les autres modules
|
||||||
|
export type { Theme };
|
||||||
|
|
||||||
// Types de base pour les tâches
|
// Types de base pour les tâches
|
||||||
// Note: TaskStatus et TaskPriority sont maintenant gérés par la configuration centralisée dans lib/status-config.ts
|
// Note: TaskStatus et TaskPriority sont maintenant gérés par la configuration centralisée dans lib/status-config.ts
|
||||||
export type TaskStatus =
|
export type TaskStatus =
|
||||||
|
|||||||
Reference in New Issue
Block a user