diff --git a/src/app/daily/DailyPageClient.tsx b/src/app/daily/DailyPageClient.tsx index 7149612..8a9897e 100644 --- a/src/app/daily/DailyPageClient.tsx +++ b/src/app/daily/DailyPageClient.tsx @@ -7,12 +7,12 @@ import { DailyView, DailyCheckboxType, DailyCheckbox } from '@/lib/types'; import { DeadlineMetrics } from '@/services/analytics/deadline-analytics'; import { Button } from '@/components/ui/Button'; 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 { PendingTasksSection } from '@/components/daily/PendingTasksSection'; import { dailyClient } from '@/clients/daily-client'; import { Header } from '@/components/ui/Header'; -import { DeadlineReminder } from '@/components/daily/DeadlineReminder'; import { getPreviousWorkday, formatDateLong, isToday, generateDateTitle, formatDateShort, isYesterday } from '@/lib/date-utils'; interface DailyPageClientProps { @@ -53,7 +53,6 @@ export function DailyPageClient({ } = useDaily(initialDate, initialDailyView); const [dailyDates, setDailyDates] = useState(initialDailyDates); - const [refreshTrigger, setRefreshTrigger] = useState(0); // Fonction pour rafraîchir la liste des dates avec des dailies const refreshDailyDates = async () => { @@ -88,14 +87,12 @@ export function DailyPageClient({ const handleToggleCheckbox = async (checkboxId: string) => { await toggleCheckbox(checkboxId); - setRefreshTrigger(prev => prev + 1); // Trigger refresh pour les tâches en attente }; const handleDeleteCheckbox = async (checkboxId: string) => { await deleteCheckbox(checkboxId); // Refresh dates après suppression pour mettre à jour le calendrier 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) => { @@ -110,6 +107,7 @@ export function DailyPageClient({ await reorderCheckboxes({ date, checkboxIds }); }; + const getYesterdayDate = () => { return getPreviousWorkday(currentDate); }; @@ -142,6 +140,40 @@ export function DailyPageClient({ 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 = { '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) { return (
@@ -219,7 +251,16 @@ export function DailyPageClient({ {/* Rappel des échéances urgentes - Desktop uniquement */}
- + { + // Rediriger vers la page Kanban avec la tâche sélectionnée + window.location.href = `/kanban?taskId=${item.id}`; + }} + />
{/* Contenu principal */} @@ -244,10 +285,12 @@ export function DailyPageClient({ /> {/* Calendrier en bas sur mobile */} -
)} @@ -258,10 +301,12 @@ export function DailyPageClient({
{/* Calendrier - Desktop */}
-
@@ -307,7 +352,7 @@ export function DailyPageClient({ onToggleCheckbox={handleToggleCheckbox} onDeleteCheckbox={handleDeleteCheckbox} onRefreshDaily={refreshDailySilent} - refreshTrigger={refreshTrigger} + refreshTrigger={0} initialPendingTasks={initialPendingTasks} /> diff --git a/src/components/daily/DailyAddForm.tsx b/src/components/daily/DailyAddForm.tsx deleted file mode 100644 index 685f2b0..0000000 --- a/src/components/daily/DailyAddForm.tsx +++ /dev/null @@ -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; - disabled?: boolean; - placeholder?: string; -} - -export function DailyAddForm({ onAdd, disabled = false, placeholder = "Ajouter une tâche..." }: DailyAddFormProps) { - const [newCheckboxText, setNewCheckboxText] = useState(''); - const [selectedType, setSelectedType] = useState('meeting'); - const inputRef = useRef(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 ( -
- {/* Sélecteur de type */} -
- - -
- - {/* Champ de saisie et options */} -
- setNewCheckboxText(e.target.value)} - onKeyDown={handleKeyPress} - disabled={disabled} - className="flex-1 min-w-[300px]" - /> - -
-
- ); -} diff --git a/src/components/daily/DailySection.tsx b/src/components/daily/DailySection.tsx index 8f570ff..3470a97 100644 --- a/src/components/daily/DailySection.tsx +++ b/src/components/daily/DailySection.tsx @@ -4,8 +4,8 @@ import { DailyCheckbox, DailyCheckboxType } from '@/lib/types'; import { Card } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; import { DailyCheckboxSortable } from './DailyCheckboxSortable'; -import { DailyCheckboxItem } from './DailyCheckboxItem'; -import { DailyAddForm } from './DailyAddForm'; +import { CheckboxItem, CheckboxItemData } from '@/components/ui/CheckboxItem'; +import { DailyAddForm, AddFormOption } from '@/components/ui/DailyAddForm'; import { DndContext, closestCenter, DragEndEvent, DragOverlay, DragStartEvent } from '@dnd-kit/core'; import { SortableContext, verticalListSortingStrategy, arrayMove } from '@dnd-kit/sortable'; import { useState } from 'react'; @@ -80,6 +80,22 @@ export function DailySection({ 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 ( onAddCheckbox(text, option as DailyCheckboxType)} disabled={saving} + placeholder="Ajouter une tâche..." + options={addFormOptions} + defaultOption="task" />
@@ -160,12 +179,14 @@ export function DailySection({ {activeCheckbox ? (
- Promise.resolve()} onUpdate={() => Promise.resolve()} onDelete={() => Promise.resolve()} saving={false} + showEditButton={false} + showDeleteButton={false} />
diff --git a/src/components/daily/DeadlineReminder.tsx b/src/components/daily/DeadlineReminder.tsx deleted file mode 100644 index 41494f0..0000000 --- a/src/components/daily/DeadlineReminder.tsx +++ /dev/null @@ -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 = { '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 ( - -
-
-
⚠️
-
-

- Rappel - Tâches urgentes ({urgentTasks.length}) -

- -
- {urgentTasks.map((task, index) => ( -
- {getUrgencyIcon(task)} - - {task.title} - - - ({getUrgencyText(task)}) - - - {getSourceIcon(task.source)} - - {index < urgentTasks.length - 1 && ( - - )} -
- ))} -
- -
- Consultez la page d'accueil pour plus de détails sur les échéances -
-
-
-
-
- ); -} - -// 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 '📋'; - } -} diff --git a/src/components/dashboard/ManagerWeeklySummary.tsx b/src/components/dashboard/ManagerWeeklySummary.tsx index 7d72d4a..933fad2 100644 --- a/src/components/dashboard/ManagerWeeklySummary.tsx +++ b/src/components/dashboard/ManagerWeeklySummary.tsx @@ -4,12 +4,15 @@ import { useState } from 'react'; import { ManagerSummary } from '@/services/analytics/manager-summary'; import { Card, CardHeader, CardContent } from '@/components/ui/Card'; import { Button } from '@/components/ui/Button'; -import { TagDisplay } from '@/components/ui/TagDisplay'; -import { getPriorityConfig } from '@/lib/status-config'; +import { MetricCard } from '@/components/ui/MetricCard'; +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 { MetricsTab } from './MetricsTab'; import { format } from 'date-fns'; import { fr } from 'date-fns/locale'; +import { Tag } from '@/lib/types'; interface ManagerWeeklySummaryProps { initialSummary: ManagerSummary; @@ -20,6 +23,10 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu const [activeView, setActiveView] = useState<'narrative' | 'accomplishments' | 'challenges' | 'metrics'>('narrative'); const { tags: availableTags } = useTasksContext(); + const handleTabChange = (tabId: string) => { + setActiveView(tabId as 'narrative' | 'accomplishments' | 'challenges' | 'metrics'); + }; + const handleRefresh = () => { // SSR - refresh via page 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 })})`; }; - const getPriorityBadgeStyle = (priority: 'low' | 'medium' | 'high') => { - const config = getPriorityConfig(priority); - const baseClasses = 'text-xs px-2 py-0.5 rounded font-medium border'; - - switch (config.color) { - case 'blue': - 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`; - } - }; + // Configuration des onglets + const tabItems: TabItem[] = [ + { id: 'narrative', label: 'Vue Executive', icon: '📝' }, + { id: 'accomplishments', label: 'Accomplissements', icon: '✅', count: summary.keyAccomplishments.length }, + { id: 'challenges', label: 'Enjeux à venir', icon: '🎯', count: summary.upcomingChallenges.length }, + { id: 'metrics', label: 'Métriques', icon: '📊' } + ]; return ( @@ -67,50 +64,11 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu {/* Navigation des vues */} -
- -
+ {/* Vue Executive / Narrative */} {activeView === 'narrative' && ( @@ -147,45 +105,33 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
-
-
- {summary.metrics.totalTasksCompleted} -
-
Tâches complétées
-
- dont {summary.metrics.highPriorityTasksCompleted} priorité haute -
-
+ -
-
- {summary.metrics.totalCheckboxesCompleted} -
-
Todos complétés
-
- dont {summary.metrics.meetingCheckboxesCompleted} meetings -
-
+ -
-
- {summary.keyAccomplishments.filter(a => a.impact === 'high').length} -
-
Items à fort impact
-
- / {summary.keyAccomplishments.length} accomplissements -
-
+ a.impact === 'high').length} + subtitle={`/ ${summary.keyAccomplishments.length} accomplissements`} + color="warning" + /> -
-
- {summary.upcomingChallenges.filter(c => c.priority === 'high').length} -
-
Priorités critiques
-
- / {summary.upcomingChallenges.length} enjeux -
-
+ c.priority === 'high').length} + subtitle={`/ ${summary.upcomingChallenges.length} enjeux`} + color="destructive" + />
@@ -202,64 +148,18 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu

Aucun accomplissement significatif trouvé cette semaine.

Ajoutez des tâches avec priorité haute/medium ou des meetings.

- ) : ( - summary.keyAccomplishments.slice(0, 6).map((accomplishment, index) => ( -
- {/* Barre colorée gauche */} -
- - {/* Header compact */} -
-
- - #{index + 1} - - - {getPriorityConfig(accomplishment.impact).label} - -
- - {format(accomplishment.completedAt, 'dd/MM', { locale: fr })} - -
- - {/* Titre */} -

- {accomplishment.title} -

- - {/* Tags */} - {accomplishment.tags && accomplishment.tags.length > 0 && ( -
- -
- )} - - {/* Description si disponible */} - {accomplishment.description && ( -

- {accomplishment.description} -

- )} - - {/* Count de todos */} - {accomplishment.todosCount > 0 && ( -
- 📋 - {accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''} -
- )} -
- )) - )} + ) : ( + summary.keyAccomplishments.slice(0, 6).map((accomplishment, index) => ( + + )) + )} @@ -276,64 +176,16 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu

Aucun enjeu prioritaire trouvé.

Ajoutez des tâches non complétées avec priorité haute/medium.

- ) : ( - summary.upcomingChallenges.slice(0, 6).map((challenge, index) => ( -
- {/* Barre colorée gauche */} -
- - {/* Header compact */} -
-
- - #{index + 1} - - - {getPriorityConfig(challenge.priority).label} - -
- {challenge.deadline && ( - - {format(challenge.deadline, 'dd/MM', { locale: fr })} - - )} -
- - {/* Titre */} -

- {challenge.title} -

- - {/* Tags */} - {challenge.tags && challenge.tags.length > 0 && ( -
- -
- )} - - {/* Description si disponible */} - {challenge.description && ( -

- {challenge.description} -

- )} - - {/* Count de todos */} - {challenge.todosCount > 0 && ( -
- 📋 - {challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''} -
- )} -
+ ) : ( + summary.upcomingChallenges.slice(0, 6).map((challenge, index) => ( + )) )} @@ -354,60 +206,14 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
{summary.keyAccomplishments.map((accomplishment, index) => ( -
- {/* Barre colorée gauche */} -
- - {/* Header compact */} -
-
- - #{index + 1} - - - {getPriorityConfig(accomplishment.impact).label} - -
- - {format(accomplishment.completedAt, 'dd/MM', { locale: fr })} - -
- - {/* Titre */} -

- {accomplishment.title} -

- - {/* Tags */} - {accomplishment.tags && accomplishment.tags.length > 0 && ( -
- -
- )} - - {/* Description si disponible */} - {accomplishment.description && ( -

- {accomplishment.description} -

- )} - - {/* Count de todos */} - {accomplishment.todosCount > 0 && ( -
- 📋 - {accomplishment.todosCount} todo{accomplishment.todosCount > 1 ? 's' : ''} -
- )} -
+ ))}
@@ -426,62 +232,14 @@ export default function ManagerWeeklySummary({ initialSummary }: ManagerWeeklySu
{summary.upcomingChallenges.map((challenge, index) => ( -
- {/* Barre colorée gauche */} -
- - {/* Header compact */} -
-
- - #{index + 1} - - - {getPriorityConfig(challenge.priority).label} - -
- {challenge.deadline && ( - - {format(challenge.deadline, 'dd/MM', { locale: fr })} - - )} -
- - {/* Titre */} -

- {challenge.title} -

- - {/* Tags */} - {challenge.tags && challenge.tags.length > 0 && ( -
- -
- )} - - {/* Description si disponible */} - {challenge.description && ( -

- {challenge.description} -

- )} - - {/* Count de todos */} - {challenge.todosCount > 0 && ( -
- 📋 - {challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''} -
- )} -
+ ))}
diff --git a/src/components/ui-showcase/UIShowcaseClient.tsx b/src/components/ui-showcase/UIShowcaseClient.tsx index 2c9dcbb..5814dea 100644 --- a/src/components/ui-showcase/UIShowcaseClient.tsx +++ b/src/components/ui-showcase/UIShowcaseClient.tsx @@ -4,15 +4,111 @@ import { useState } from 'react'; import { Button } from '@/components/ui/Button'; 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 { StyledCard } from '@/components/ui/StyledCard'; 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 { TabItem } from '@/components/ui/Tabs'; +import { AchievementData } from '@/components/ui/AchievementCard'; +import { ChallengeData } from '@/components/ui/ChallengeCard'; export function UIShowcaseClient() { const [inputValue, setInputValue] = useState(''); + const [selectedDate, setSelectedDate] = useState(new Date()); + const [checkboxItems, setCheckboxItems] = useState([ + { 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([ + { 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([ + { + 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 (
@@ -93,40 +189,40 @@ export function UIShowcaseClient() {
- + Information Ceci est une alerte par défaut avec des informations importantes. - + - + Succès Opération terminée avec succès ! Toutes les données ont été sauvegardées. - + - + Erreur Une erreur s'est produite lors du traitement de votre demande. - + - + Attention Veuillez vérifier vos informations avant de continuer. - + - + Conseil Astuce : Vous pouvez utiliser les raccourcis clavier pour naviguer plus rapidement. - +
@@ -322,19 +418,19 @@ export function UIShowcaseClient() { Notifications - + Bienvenue ! Votre compte a été créé avec succès. - + - + Mise à jour disponible Une nouvelle version de l'application est disponible. - +
@@ -985,6 +1081,247 @@ export function UIShowcaseClient() {
+ {/* Daily Components Section */} +
+

+ Daily Components +

+ +
+ {/* CheckboxItem */} +
+

CheckboxItem

+
+
+
+ CheckboxItem - Élément de liste avec checkbox +
+
+ {checkboxItems.map((item) => ( + { + 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} + /> + ))} +
+
+
+
+ + {/* Calendar */} +
+

Calendar

+
+
+
+ Calendar - Calendrier avec dates marquées +
+
+ +
+
+
+
+ + {/* AddForm */} +
+

DailyAddForm

+
+
+
+ DailyAddForm - Formulaire d'ajout avec options +
+
+ { + 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" + /> +
+
+
+
+ + {/* Alert */} +
+

Alert

+
+
+
+ Alert - Alerte avec éléments urgents +
+
+ console.log('Clicked:', item)} + /> +
+
+
+
+ + {/* CollapsibleSection */} +
+

CollapsibleSection

+
+
+
+ CollapsibleSection - Section repliable avec éléments +
+
+ console.log('Refresh')} + onItemToggle={(id) => console.log('Toggle:', id)} + /> +
+
+
+
+
+
+ + {/* Weekly Manager Components Section */} +
+

+ Weekly Manager Components +

+ +
+ {/* Tabs */} +
+

Tabs

+
+
+
+ Tabs - Navigation par onglets +
+ +
+ Onglet actif: {activeTab} +
+
+
+
+ + {/* PriorityBadge */} +
+

PriorityBadge

+
+
+
+ PriorityBadge - Badge de priorité +
+
+ + + +
+
+
+
+
+ +
+ {/* AchievementCard */} +
+

AchievementCard

+
+
+
+ AchievementCard - Carte d'accomplissement +
+
+ {sampleAchievements.map((achievement, index) => ( + + ))} +
+
+
+
+ + {/* ChallengeCard */} +
+

ChallengeCard

+
+
+
+ ChallengeCard - Carte de défi +
+
+ {sampleChallenges.map((challenge, index) => ( + + ))} +
+
+
+
+
+
+ {/* Footer */}

diff --git a/src/components/ui/AchievementCard.tsx b/src/components/ui/AchievementCard.tsx new file mode 100644 index 0000000..6396b0b --- /dev/null +++ b/src/components/ui/AchievementCard.tsx @@ -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 ( +

+ {/* Barre colorée gauche */} +
+ + {/* Header compact */} +
+
+ + #{index + 1} + + +
+ + {format(achievement.completedAt, 'dd/MM', { locale: fr })} + +
+ + {/* Titre */} +

+ {achievement.title} +

+ + {/* Tags */} + {achievement.tags && achievement.tags.length > 0 && ( +
+ +
+ )} + + {/* Description si disponible */} + {showDescription && achievement.description && ( +

+ {achievement.description} +

+ )} + + {/* Count de todos */} + {achievement.todosCount && achievement.todosCount > 0 && ( +
+ 📋 + {achievement.todosCount} todo{achievement.todosCount > 1 ? 's' : ''} +
+ )} +
+ ); +} diff --git a/src/components/ui/Alert.tsx b/src/components/ui/Alert.tsx index a06b0c1..c90932d 100644 --- a/src/components/ui/Alert.tsx +++ b/src/components/ui/Alert.tsx @@ -1,58 +1,49 @@ -import { HTMLAttributes, forwardRef } from 'react'; -import { cn } from '@/lib/utils'; +'use client'; -interface AlertProps extends HTMLAttributes { - variant?: 'default' | 'success' | 'destructive' | 'warning' | 'info'; +import { Card } from '@/components/ui/Card'; + +interface AlertProps { + variant?: 'default' | 'destructive' | 'success' | 'warning' | 'info'; + className?: string; + children: React.ReactNode; } -const Alert = forwardRef( - ({ className, variant = 'default', ...props }, ref) => { - const variants = { - default: 'bg-[var(--card)] text-[var(--foreground)] border border-[var(--border)]', - success: 'bg-[color-mix(in_srgb,var(--success)_10%,transparent)] text-[var(--success)] border border-[color-mix(in_srgb,var(--success)_20%,var(--border))]', - destructive: 'bg-[color-mix(in_srgb,var(--destructive)_10%,transparent)] text-[var(--destructive)] border border-[color-mix(in_srgb,var(--destructive)_20%,var(--border))]', - warning: 'bg-[color-mix(in_srgb,var(--accent)_10%,transparent)] text-[var(--accent)] border border-[color-mix(in_srgb,var(--accent)_20%,var(--border))]', - info: 'bg-[color-mix(in_srgb,var(--primary)_10%,transparent)] text-[var(--primary)] border border-[color-mix(in_srgb,var(--primary)_20%,var(--border))]' - }; +export function Alert({ variant = 'default', className = '', children }: AlertProps) { + const getVariantClasses = () => { + switch (variant) { + case 'destructive': + return 'outline-card-red'; + case 'success': + return 'outline-card-green'; + case 'info': + return 'outline-card-blue'; + case 'warning': + return 'outline-card-yellow'; + case 'default': + default: + return 'outline-card-gray'; + } + }; - return ( -
- ); - } -); + return ( + + {children} + + ); +} -Alert.displayName = 'Alert'; +export function AlertTitle({ children, className = '' }: { children: React.ReactNode; className?: string }) { + return ( +

+ {children} +

+ ); +} -const AlertTitle = forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); - -AlertTitle.displayName = 'AlertTitle'; - -const AlertDescription = forwardRef>( - ({ className, ...props }, ref) => ( -
- ) -); - -AlertDescription.displayName = 'AlertDescription'; - -export { Alert, AlertTitle, AlertDescription }; +export function AlertDescription({ children, className = '' }: { children: React.ReactNode; className?: string }) { + return ( +
+ {children} +
+ ); +} \ No newline at end of file diff --git a/src/components/ui/AlertBanner.tsx b/src/components/ui/AlertBanner.tsx new file mode 100644 index 0000000..ca90e49 --- /dev/null +++ b/src/components/ui/AlertBanner.tsx @@ -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 ( + +
+
+
{icon}
+
+

+ {title} ({items.length}) +

+ +
+ {items.map((item, index) => ( +
onItemClick?.(item)} + title={item.title} + > + {item.icon || getSourceIcon(item.source)} + + {item.title} + + {item.metadata && ( + + ({item.metadata}) + + )} + {item.urgency && ( + + {item.urgency === 'critical' ? '🔴' : + item.urgency === 'high' ? '🟠' : + item.urgency === 'medium' ? '🟡' : '🟢'} + + )} + {index < items.length - 1 && ( + + )} +
+ ))} +
+ + {items.length > 0 && ( +
+ Cliquez sur un élément pour plus de détails +
+ )} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/daily/DailyCalendar.tsx b/src/components/ui/Calendar.tsx similarity index 78% rename from src/components/daily/DailyCalendar.tsx rename to src/components/ui/Calendar.tsx index 148873f..98988cb 100644 --- a/src/components/daily/DailyCalendar.tsx +++ b/src/components/ui/Calendar.tsx @@ -7,17 +7,23 @@ import { formatDateForAPI, createDate, getToday } from '@/lib/date-utils'; import { format } from 'date-fns'; import { fr } from 'date-fns/locale'; -interface DailyCalendarProps { +interface CalendarProps { currentDate: Date; 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, onDateSelect, - dailyDates, -}: DailyCalendarProps) { + markedDates = [], + showTodayButton = true, + showLegend = true, + className = '' +}: CalendarProps) { const [viewDate, setViewDate] = useState(createDate(currentDate)); // Formatage des dates pour comparaison (éviter le décalage timezone) @@ -90,8 +96,8 @@ export function DailyCalendar({ return date.getMonth() === viewDate.getMonth(); }; - const hasDaily = (date: Date) => { - return dailyDates.includes(formatDateKey(date)); + const hasMarkedDate = (date: Date) => { + return markedDates.includes(formatDateKey(date)); }; const isSelected = (date: Date) => { @@ -105,7 +111,7 @@ export function DailyCalendar({ const weekDays = ['Lun', 'Mar', 'Mer', 'Jeu', 'Ven', 'Sam', 'Dim']; return ( - + {/* Header avec navigation */}
-
+ {showTodayButton && ( +
+ +
+ )} {/* Jours de la semaine */}
@@ -155,7 +163,7 @@ export function DailyCalendar({ {days.map((date, index) => { const isCurrentMonthDay = isCurrentMonth(date); const isTodayDay = isTodayDate(date); - const hasCheckboxes = hasDaily(date); + const hasMarked = hasMarkedDate(date); const isSelectedDay = isSelected(date); return ( @@ -175,13 +183,13 @@ export function DailyCalendar({ : '' } ${isSelectedDay ? 'bg-[var(--primary)] text-white' : ''} - ${hasCheckboxes ? 'font-bold' : ''} + ${hasMarked ? 'font-bold' : ''} `} > {date.getDate()} - {/* Indicateur de daily existant */} - {hasCheckboxes && ( + {/* Indicateur de date marquée */} + {hasMarked && (
{/* Légende */} -
-
-
- Jour avec des tâches + {showLegend && ( +
+
+
+ Jour avec des éléments +
+
+
+ Aujourd'hui +
-
-
- Aujourd'hui -
-
+ )} ); } diff --git a/src/components/ui/ChallengeCard.tsx b/src/components/ui/ChallengeCard.tsx new file mode 100644 index 0000000..36ac006 --- /dev/null +++ b/src/components/ui/ChallengeCard.tsx @@ -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 ( +
+ {/* Barre colorée gauche */} +
+ + {/* Header compact */} +
+
+ + #{index + 1} + + +
+ {challenge.deadline && ( + + {format(challenge.deadline, 'dd/MM', { locale: fr })} + + )} +
+ + {/* Titre */} +

+ {challenge.title} +

+ + {/* Tags */} + {challenge.tags && challenge.tags.length > 0 && ( +
+ +
+ )} + + {/* Description si disponible */} + {showDescription && challenge.description && ( +

+ {challenge.description} +

+ )} + + {/* Count de todos */} + {challenge.todosCount && challenge.todosCount > 0 && ( +
+ 📋 + {challenge.todosCount} todo{challenge.todosCount > 1 ? 's' : ''} +
+ )} +
+ ); +} diff --git a/src/components/ui/CheckboxItem.tsx b/src/components/ui/CheckboxItem.tsx new file mode 100644 index 0000000..3c61323 --- /dev/null +++ b/src/components/ui/CheckboxItem.tsx @@ -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; + onUpdate: (itemId: string, text: string, type?: string, taskId?: string) => Promise; + onDelete: (itemId: string) => Promise; + 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(null); + const [inlineEditingText, setInlineEditingText] = useState(''); + const [optimisticChecked, setOptimisticChecked] = useState(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 ( +
+ {/* Checkbox */} + + + {/* Contenu principal */} + {inlineEditingId === item.id ? ( + setInlineEditingText(e.target.value)} + onKeyDown={handleInlineEditKeyPress} + onBlur={handleSaveInlineEdit} + autoFocus + className="flex-1 h-7 text-sm" + /> + ) : ( +
+ {/* Texte cliquable pour édition inline */} + + {item.text} + + + {/* Icône d'édition avancée */} + {showEditButton && ( + + )} +
+ )} + + {/* Lien vers la tâche si liée */} + {showTaskLink && item.task && ( + + {item.task.title} + + )} + + {/* Bouton de suppression */} + {showDeleteButton && ( + + )} +
+ ); +} diff --git a/src/components/ui/CollapsibleSection.tsx b/src/components/ui/CollapsibleSection.tsx new file mode 100644 index 0000000..51aa0e9 --- /dev/null +++ b/src/components/ui/CollapsibleSection.tsx @@ -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[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 ( + + +
+ + + {!isCollapsed && ( +
+ {/* Filtres */} + {filters.map((filter, index) => ( + + ))} + + {/* Bouton refresh */} + {onRefresh && ( + + )} +
+ )} +
+
+ + {!isCollapsed && ( + + {loading ? ( +
+ Chargement... +
+ ) : items.length === 0 ? ( +
+ 🎉 {emptyMessage} ! Excellent travail. +
+ ) : ( +
+ {items.map((item) => ( +
+ {/* Checkbox */} + {item.isChecked !== undefined && ( + + )} + + {/* Contenu */} +
+
+ {item.icon && {item.icon}} + + {item.title} + +
+ {(item.subtitle || item.metadata) && ( +
+ {item.subtitle && {item.subtitle}} + {item.metadata && {item.metadata}} +
+ )} +
+ + {/* Actions */} + {item.actions && ( +
+ {item.actions.map((action, index) => ( + + ))} +
+ )} +
+ ))} +
+ )} +
+ )} +
+ ); +} diff --git a/src/components/ui/DailyAddForm.tsx b/src/components/ui/DailyAddForm.tsx new file mode 100644 index 0000000..37b42aa --- /dev/null +++ b/src/components/ui/DailyAddForm.tsx @@ -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; + 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(defaultOption || (options.length > 0 ? options[0].value : '')); + const inputRef = useRef(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 ( +
+ {/* Sélecteur d'options */} + {options.length > 0 && ( +
+ {options.map((option) => ( + + ))} +
+ )} + + {/* Champ de saisie et bouton d'ajout */} +
+ setNewItemText(e.target.value)} + onKeyDown={handleKeyPress} + disabled={disabled} + className="flex-1 min-w-[300px]" + /> + +
+
+ ); +} diff --git a/src/components/ui/PriorityBadge.tsx b/src/components/ui/PriorityBadge.tsx new file mode 100644 index 0000000..b81ac13 --- /dev/null +++ b/src/components/ui/PriorityBadge.tsx @@ -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 ( + + {config.label} + + ); +} diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx new file mode 100644 index 0000000..5079f69 --- /dev/null +++ b/src/components/ui/Tabs.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/src/components/ui/index.ts b/src/components/ui/index.ts index 1d39294..12a3e8f 100644 --- a/src/components/ui/index.ts +++ b/src/components/ui/index.ts @@ -22,6 +22,19 @@ export { ColumnHeader } from './ColumnHeader'; export { EmptyState } from './EmptyState'; 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 export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './Card'; export { FontSizeToggle } from './FontSizeToggle'; diff --git a/src/lib/types.ts b/src/lib/types.ts index 9bc18fb..b1a9f8f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -1,6 +1,9 @@ import { TfsConfig } from '@/services/integrations/tfs'; import { Theme } from './theme-config'; +// Réexporter Theme pour les autres modules +export type { Theme }; + // 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 export type TaskStatus =