diff --git a/src/app/sessions/NewWorkshopDropdown.tsx b/src/app/sessions/NewWorkshopDropdown.tsx index 2db5c31..a766694 100644 --- a/src/app/sessions/NewWorkshopDropdown.tsx +++ b/src/app/sessions/NewWorkshopDropdown.tsx @@ -15,14 +15,17 @@ export function NewWorkshopDropdown() {
{open && ( -
+
{WORKSHOPS.map((w) => ( setOpen(false)} > - {w.icon} + + {w.icon} +
{w.label}
{w.description}
diff --git a/src/app/sessions/SessionCard.tsx b/src/app/sessions/SessionCard.tsx new file mode 100644 index 0000000..c656fe0 --- /dev/null +++ b/src/app/sessions/SessionCard.tsx @@ -0,0 +1,282 @@ +'use client'; + +import { useState, useTransition } from 'react'; +import Link from 'next/link'; +import { Button, Modal, ModalFooter, Input, CollaboratorDisplay } from '@/components/ui'; +import { deleteSwotSession, updateSwotSession } from '@/actions/session'; +import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators'; +import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review'; +import { deleteWeeklyCheckInSession, updateWeeklyCheckInSession } from '@/actions/weekly-checkin'; +import { deleteWeatherSession, updateWeatherSession } from '@/actions/weather'; +import { deleteGifMoodSession, updateGifMoodSession } from '@/actions/gif-mood'; +import { type WorkshopTypeId, getWorkshop, getSessionPath } from '@/lib/workshops'; +import type { Share } from '@/lib/share-utils'; +import type { + AnySession, CardView, + SwotSession, MotivatorSession, YearReviewSession, + WeeklyCheckInSession, WeatherSession, GifMoodSession, +} from './workshop-session-types'; +import { TABLE_COLS } from './workshop-session-types'; +import { getResolvedCollaborator, formatDate, getStatsText } from './workshop-session-helpers'; + +// ─── RoleBadge ──────────────────────────────────────────────────────────────── + +export function RoleBadge({ role }: { role: 'OWNER' | 'VIEWER' | 'EDITOR' }) { + return ( + + {role === 'EDITOR' ? 'Éditeur' : 'Lecteur'} + + ); +} + +// ─── SharesList ─────────────────────────────────────────────────────────────── + +export function SharesList({ shares }: { shares: Share[] }) { + if (!shares.length) return null; + return ( +
+ Partagé avec + {shares.slice(0, 3).map((s) => ( + + {s.user.name?.split(' ')[0] || s.user.email.split('@')[0]} + + ))} + {shares.length > 3 && +{shares.length - 3}} +
+ ); +} + +// ─── SessionCard ────────────────────────────────────────────────────────────── + +export function SessionCard({ + session, isTeamCollab = false, view = 'grid', +}: { + session: AnySession; isTeamCollab?: boolean; view?: CardView; +}) { + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); + const [isPending, startTransition] = useTransition(); + + const isSwot = session.workshopType === 'swot'; + const isYearReview = session.workshopType === 'year-review'; + const isWeeklyCheckIn = session.workshopType === 'weekly-checkin'; + const isWeather = session.workshopType === 'weather'; + const isGifMood = session.workshopType === 'gif-mood'; + + const participant = isSwot ? (session as SwotSession).collaborator + : isYearReview ? (session as YearReviewSession).participant + : isWeeklyCheckIn ? (session as WeeklyCheckInSession).participant + : isWeather ? (session as WeatherSession).user.name || (session as WeatherSession).user.email + : isGifMood ? (session as GifMoodSession).user.name || (session as GifMoodSession).user.email + : (session as MotivatorSession).participant; + + const [editTitle, setEditTitle] = useState(session.title); + const [editParticipant, setEditParticipant] = useState( + isSwot ? (session as SwotSession).collaborator + : isYearReview ? (session as YearReviewSession).participant + : isWeather || isGifMood ? '' + : (session as MotivatorSession).participant + ); + + const workshop = getWorkshop(session.workshopType as WorkshopTypeId); + const href = getSessionPath(session.workshopType as WorkshopTypeId, session.id); + const accentColor = workshop.accentColor; + const resolved = getResolvedCollaborator(session); + const participantName = resolved.matchedUser?.name || resolved.matchedUser?.email?.split('@')[0] || resolved.raw; + const statsText = getStatsText(session); + + const handleDelete = () => { + startTransition(async () => { + const result = isSwot ? await deleteSwotSession(session.id) + : isYearReview ? await deleteYearReviewSession(session.id) + : isWeeklyCheckIn ? await deleteWeeklyCheckInSession(session.id) + : isWeather ? await deleteWeatherSession(session.id) + : isGifMood ? await deleteGifMoodSession(session.id) + : await deleteMotivatorSession(session.id); + if (result.success) setShowDeleteModal(false); + else console.error('Error deleting:', result.error); + }); + }; + + const handleEdit = () => { + startTransition(async () => { + const result = isSwot ? await updateSwotSession(session.id, { title: editTitle, collaborator: editParticipant }) + : isYearReview ? await updateYearReviewSession(session.id, { title: editTitle, participant: editParticipant }) + : isWeeklyCheckIn ? await updateWeeklyCheckInSession(session.id, { title: editTitle, participant: editParticipant }) + : isWeather ? await updateWeatherSession(session.id, { title: editTitle }) + : isGifMood ? await updateGifMoodSession(session.id, { title: editTitle }) + : await updateMotivatorSession(session.id, { title: editTitle, participant: editParticipant }); + if (result.success) setShowEditModal(false); + else console.error('Error updating:', result.error); + }); + }; + + const openEditModal = () => { setEditTitle(session.title); setEditParticipant(participant); setShowEditModal(true); }; + const hoverCard = !isTeamCollab ? 'hover:-translate-y-0.5 hover:shadow-md' : ''; + const opacity = isTeamCollab ? 'opacity-60' : ''; + + // ── Vue Grille ─────────────────────────────────────────────────────────── + const gridCard = ( +
+
+
+
+

{session.title}

+ {!session.isOwner && } +
+ + {!session.isOwner &&

par {session.user.name || session.user.email}

} + {session.isOwner && session.shares.length > 0 &&
} +
+
+
+ {workshop.icon} + {workshop.labelShort} + · + {statsText} +
+ {formatDate(session.updatedAt)} +
+
+
+ ); + + // ── Vue Liste ──────────────────────────────────────────────────────────── + const listCard = ( +
+
+ {workshop.icon} +
+
+ {session.title} + {!session.isOwner && } +
+
+ {participantName} + · + {workshop.labelShort} + · + {statsText} +
+
+ {formatDate(session.updatedAt)} + + + +
+ ); + + // ── Vue Tableau ────────────────────────────────────────────────────────── + const tableRow = ( +
+
+
+ {workshop.icon} + {workshop.labelShort} +
+
+ {session.title} + {!session.isOwner && } +
+
+ +
+
+ +
+
+ {statsText} +
+
+ {formatDate(session.updatedAt)} +
+
+ ); + + const cardContent = view === 'list' ? listCard : view === 'table' ? tableRow : gridCard; + + const actionButtons = ( + <> + {(session.isOwner || session.role === 'EDITOR' || session.isTeamCollab) && ( +
+ + {(session.isOwner || session.isTeamCollab) && ( + + )} +
+ )} + + ); + + return ( + <> +
+ + {cardContent} + + {actionButtons} +
+ + setShowEditModal(false)} title="Modifier l'atelier" size="sm"> +
{ e.preventDefault(); handleEdit(); }} className="space-y-4"> +
+ + setEditTitle(e.target.value)} placeholder="Titre de l'atelier" required /> +
+
+ + {!isWeather && !isGifMood && ( + setEditParticipant(e.target.value)} + placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'} required /> + )} +
+ + + + +
+
+ + setShowDeleteModal(false)} title="Supprimer l'atelier" size="sm"> +
+

Êtes-vous sûr de vouloir supprimer "{session.title}" ?

+

Cette action est irréversible. Toutes les données seront perdues.

+ + + + +
+
+ + ); +} diff --git a/src/app/sessions/WorkshopTabs.tsx b/src/app/sessions/WorkshopTabs.tsx index a9d574c..9065f17 100644 --- a/src/app/sessions/WorkshopTabs.tsx +++ b/src/app/sessions/WorkshopTabs.tsx @@ -1,464 +1,144 @@ 'use client'; -import { useState, useTransition, useRef } from 'react'; -import Link from 'next/link'; +import { useState, useRef } from 'react'; import { useSearchParams, useRouter } from 'next/navigation'; -import { - Card, - Badge, - Button, - Modal, - ModalFooter, - Input, - CollaboratorDisplay, -} from '@/components/ui'; -import { deleteSwotSession, updateSwotSession } from '@/actions/session'; -import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators'; -import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review'; -import { deleteWeeklyCheckInSession, updateWeeklyCheckInSession } from '@/actions/weekly-checkin'; -import { deleteWeatherSession, updateWeatherSession } from '@/actions/weather'; -import { deleteGifMoodSession, updateGifMoodSession } from '@/actions/gif-mood'; -import { - type WorkshopTabType, - type WorkshopTypeId, - WORKSHOPS, - VALID_TAB_PARAMS, - getWorkshop, - getSessionPath, -} from '@/lib/workshops'; +import { CollaboratorDisplay } from '@/components/ui'; +import { type WorkshopTabType, WORKSHOPS, VALID_TAB_PARAMS } from '@/lib/workshops'; import { useClickOutside } from '@/hooks/useClickOutside'; +import { + type CardView, type SortCol, type WorkshopTabsProps, type AnySession, + TABLE_COLS, SORT_COLUMNS, TYPE_TABS, +} from './workshop-session-types'; +import { + getResolvedCollaborator, groupByPerson, getMonthGroup, sortSessions, +} from './workshop-session-helpers'; +import { SessionCard } from './SessionCard'; -import type { Share } from '@/lib/share-utils'; +// ─── SectionHeader ──────────────────────────────────────────────────────────── -const TYPE_TABS = [ - { value: 'all' as const, icon: '📋', label: 'Tous' }, - { value: 'team' as const, icon: '🏢', label: 'Équipe' }, - ...WORKSHOPS.map((w) => ({ value: w.id, icon: w.icon, label: w.labelShort })), -]; - -interface ResolvedCollaborator { - raw: string; - matchedUser: { - id: string; - email: string; - name: string | null; - } | null; -} - -interface SwotSession { - id: string; - title: string; - collaborator: string; - resolvedCollaborator: ResolvedCollaborator; - updatedAt: Date; - isOwner: boolean; - role: 'OWNER' | 'VIEWER' | 'EDITOR'; - user: { id: string; name: string | null; email: string }; - shares: Share[]; - _count: { items: number; actions: number }; - workshopType: 'swot'; - isTeamCollab?: true; - canEdit?: boolean; -} - -interface MotivatorSession { - id: string; - title: string; - participant: string; - resolvedParticipant: ResolvedCollaborator; - updatedAt: Date; - isOwner: boolean; - role: 'OWNER' | 'VIEWER' | 'EDITOR'; - user: { id: string; name: string | null; email: string }; - shares: Share[]; - _count: { cards: number }; - workshopType: 'motivators'; - isTeamCollab?: true; - canEdit?: boolean; -} - -interface YearReviewSession { - id: string; - title: string; - participant: string; - resolvedParticipant: ResolvedCollaborator; - year: number; - updatedAt: Date; - isOwner: boolean; - role: 'OWNER' | 'VIEWER' | 'EDITOR'; - user: { id: string; name: string | null; email: string }; - shares: Share[]; - _count: { items: number }; - workshopType: 'year-review'; - isTeamCollab?: true; - canEdit?: boolean; -} - -interface WeeklyCheckInSession { - id: string; - title: string; - participant: string; - resolvedParticipant: ResolvedCollaborator; - date: Date; - updatedAt: Date; - isOwner: boolean; - role: 'OWNER' | 'VIEWER' | 'EDITOR'; - user: { id: string; name: string | null; email: string }; - shares: Share[]; - _count: { items: number }; - workshopType: 'weekly-checkin'; - isTeamCollab?: true; - canEdit?: boolean; -} - -interface WeatherSession { - id: string; - title: string; - date: Date; - updatedAt: Date; - isOwner: boolean; - role: 'OWNER' | 'VIEWER' | 'EDITOR'; - user: { id: string; name: string | null; email: string }; - shares: Share[]; - _count: { entries: number }; - workshopType: 'weather'; - isTeamCollab?: true; - canEdit?: boolean; -} - -interface GifMoodSession { - id: string; - title: string; - date: Date; - updatedAt: Date; - isOwner: boolean; - role: 'OWNER' | 'VIEWER' | 'EDITOR'; - user: { id: string; name: string | null; email: string }; - shares: Share[]; - _count: { items: number }; - workshopType: 'gif-mood'; - isTeamCollab?: true; - canEdit?: boolean; -} - -type AnySession = - | SwotSession - | MotivatorSession - | YearReviewSession - | WeeklyCheckInSession - | WeatherSession - | GifMoodSession; - -interface WorkshopTabsProps { - swotSessions: SwotSession[]; - motivatorSessions: MotivatorSession[]; - yearReviewSessions: YearReviewSession[]; - weeklyCheckInSessions: WeeklyCheckInSession[]; - weatherSessions: WeatherSession[]; - gifMoodSessions: GifMoodSession[]; - teamCollabSessions?: (AnySession & { isTeamCollab?: true })[]; -} - -// Helper to get resolved collaborator from any session -function getResolvedCollaborator(session: AnySession): ResolvedCollaborator { - if (session.workshopType === 'swot') { - return (session as SwotSession).resolvedCollaborator; - } else if (session.workshopType === 'year-review') { - return (session as YearReviewSession).resolvedParticipant; - } else if (session.workshopType === 'weekly-checkin') { - return (session as WeeklyCheckInSession).resolvedParticipant; - } else if (session.workshopType === 'weather') { - const weatherSession = session as WeatherSession; - return { - raw: weatherSession.user.name || weatherSession.user.email, - matchedUser: { - id: weatherSession.user.id, - email: weatherSession.user.email, - name: weatherSession.user.name, - }, - }; - } else if (session.workshopType === 'gif-mood') { - const gifMoodSession = session as GifMoodSession; - return { - raw: gifMoodSession.user.name || gifMoodSession.user.email, - matchedUser: { - id: gifMoodSession.user.id, - email: gifMoodSession.user.email, - name: gifMoodSession.user.name, - }, - }; - } else { - return (session as MotivatorSession).resolvedParticipant; - } -} - -// Get grouping key - use matched user ID if available, otherwise normalized raw string -function getGroupKey(session: AnySession): string { - const resolved = getResolvedCollaborator(session); - // If we have a matched user, use their ID as key (ensures same person = same group) - if (resolved.matchedUser) { - return `user:${resolved.matchedUser.id}`; - } - // Otherwise, normalize the raw string - return `raw:${resolved.raw.trim().toLowerCase()}`; -} - -// Group sessions by participant (using matched user ID when available) -function groupByPerson(sessions: AnySession[]): Map { - const grouped = new Map(); - - sessions.forEach((session) => { - const key = getGroupKey(session); - - const existing = grouped.get(key); - if (existing) { - existing.push(session); - } else { - grouped.set(key, [session]); - } - }); - - // Sort sessions within each group by date - grouped.forEach((sessions) => { - sessions.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); - }); - - return grouped; -} - -export function WorkshopTabs({ - swotSessions, - motivatorSessions, - yearReviewSessions, - weeklyCheckInSessions, - weatherSessions, - gifMoodSessions, - teamCollabSessions = [], -}: WorkshopTabsProps) { - const searchParams = useSearchParams(); - const router = useRouter(); - const [typeDropdownOpen, setTypeDropdownOpen] = useState(false); - - // Get tab from URL or default to 'all' - const tabParam = searchParams.get('tab'); - const activeTab: WorkshopTabType = - tabParam && VALID_TAB_PARAMS.includes(tabParam as WorkshopTabType) - ? (tabParam as WorkshopTabType) - : 'all'; - - const setActiveTab = (tab: WorkshopTabType) => { - const params = new URLSearchParams(searchParams.toString()); - if (tab === 'all') { - params.delete('tab'); - } else { - params.set('tab', tab); - } - router.push(`/sessions${params.toString() ? `?${params.toString()}` : ''}`); - }; - - // Combine and sort all sessions (exclude team collab from main list - they're shown separately) - const allSessions: AnySession[] = [ - ...swotSessions, - ...motivatorSessions, - ...yearReviewSessions, - ...weeklyCheckInSessions, - ...weatherSessions, - ...gifMoodSessions, - ].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); - - // Filter based on active tab (for non-byPerson tabs) - const filteredSessions = - activeTab === 'all' || activeTab === 'byPerson' - ? allSessions - : activeTab === 'team' - ? teamCollabSessions - : activeTab === 'swot' - ? swotSessions - : activeTab === 'motivators' - ? motivatorSessions - : activeTab === 'year-review' - ? yearReviewSessions - : activeTab === 'weekly-checkin' - ? weeklyCheckInSessions - : activeTab === 'gif-mood' - ? gifMoodSessions - : weatherSessions; - - // Separate by ownership (for non-team tab: owned, shared, teamCollab) - const ownedSessions = filteredSessions.filter((s) => s.isOwner); - const sharedSessions = filteredSessions.filter( - (s) => !s.isOwner && !(s as AnySession & { isTeamCollab?: boolean }).isTeamCollab +function SectionHeader({ label, count }: { label: string; count: number }) { + return ( +
+
+

{label}

+ + {count} + +
); - const teamCollabFiltered = - activeTab === 'all' ? teamCollabSessions : activeTab === 'team' ? teamCollabSessions : []; +} - // Group by person (all sessions - owned and shared) - const sessionsByPerson = groupByPerson(allSessions); - const sortedPersons = Array.from(sessionsByPerson.entries()).sort((a, b) => - a[0].localeCompare(b[0], 'fr') +// ─── SortIcon ───────────────────────────────────────────────────────────────── + +function SortIcon({ active, dir }: { active: boolean; dir: 'asc' | 'desc' }) { + if (!active) { + return ( + + + + + ); + } + if (dir === 'asc') { + return ( + + + + ); + } + return ( + + + + ); +} + +// ─── ViewToggle ─────────────────────────────────────────────────────────────── + +function ViewToggle({ view, setView }: { view: CardView; setView: (v: CardView) => void }) { + const btn = (v: CardView, label: string, icon: React.ReactNode) => ( + ); return ( -
- {/* Tabs */} -
- setActiveTab('all')} - icon="📋" - label="Tous" - count={allSessions.length} - /> - setActiveTab('byPerson')} - icon="👥" - label="Par personne" - count={sessionsByPerson.size} - /> - {teamCollabSessions.length > 0 && ( - setActiveTab('team')} - icon="🏢" - label="Équipe" - count={teamCollabSessions.length} - /> - )} - -
- - {/* Sessions */} - {activeTab === 'byPerson' ? ( - // By Person View - sortedPersons.length === 0 ? ( -
Aucun atelier pour le moment
- ) : ( -
- {sortedPersons.map(([personKey, sessions]) => { - const resolved = getResolvedCollaborator(sessions[0]); - return ( -
-

- - - {sessions.length} atelier{sessions.length > 1 ? 's' : ''} - -

-
- {sessions.map((s) => ( - - ))} -
-
- ); - })} -
- ) - ) : activeTab === 'team' ? ( - teamCollabSessions.length === 0 ? ( -
- Aucun atelier de vos collaborateurs d'équipe (non partagés avec vous) -
- ) : ( -
-
-

- 🏢 Ateliers de l'équipe – non partagés ({teamCollabSessions.length}) -

-

- En tant qu'admin d'équipe, vous voyez les ateliers de vos collaborateurs - qui ne vous sont pas encore partagés. -

-
- {teamCollabSessions.map((s) => ( - - ))} -
-
-
- ) - ) : filteredSessions.length === 0 ? ( -
Aucun atelier de ce type pour le moment
- ) : ( -
- {/* My Sessions */} - {ownedSessions.length > 0 && ( -
-

- 📁 Mes ateliers ({ownedSessions.length}) -

-
- {ownedSessions.map((s) => ( - - ))} -
-
- )} - - {/* Shared Sessions */} - {sharedSessions.length > 0 && ( -
-

- 🤝 Partagés avec moi ({sharedSessions.length}) -

-
- {sharedSessions.map((s) => ( - - ))} -
-
- )} - - {/* Team collab sessions (non-shared) - grayed out, admin view only */} - {activeTab === 'all' && teamCollabFiltered.length > 0 && ( -
-

- 🏢 Équipe – non partagés ({teamCollabFiltered.length}) -

-
- {teamCollabFiltered.map((s) => ( - - ))} -
-
- )} -
+
+ {btn('grid', 'Grille', + + + + + + )} + {btn('list', 'Liste', + + + + )} + {btn('table', 'Tableau', + + + + + + )} + {btn('timeline', 'Chronologique', + + + + + + + + + )}
); } -function TypeFilterDropdown({ - activeTab, - setActiveTab, - open, - onOpenChange, - counts, -}: { - activeTab: WorkshopTabType; - setActiveTab: (t: WorkshopTabType) => void; - open: boolean; - onOpenChange: (v: boolean) => void; - counts: Record; +// ─── TabButton ──────────────────────────────────────────────────────────────── + +function TabButton({ active, onClick, icon, label, count }: { + active: boolean; onClick: () => void; icon: string; label: string; count: number; }) { - const typeTabs = TYPE_TABS.filter((t) => t.value !== 'all'); + return ( + + ); +} + +// ─── TypeFilterDropdown ─────────────────────────────────────────────────────── + +function TypeFilterDropdown({ + activeTab, setActiveTab, open, onOpenChange, counts, +}: { + activeTab: WorkshopTabType; setActiveTab: (t: WorkshopTabType) => void; + open: boolean; onOpenChange: (v: boolean) => void; counts: Record; +}) { + const typeTabs = TYPE_TABS.filter((t) => t.value !== 'all' && t.value !== 'team'); const current = TYPE_TABS.find((t) => t.value === activeTab) ?? TYPE_TABS[0]; - const isTypeSelected = activeTab !== 'all' && activeTab !== 'byPerson'; + const isTypeSelected = activeTab !== 'all' && activeTab !== 'byPerson' && activeTab !== 'team'; const totalCount = typeTabs.reduce((s, t) => s + (counts[t.value] ?? 0), 0); const containerRef = useRef(null); useClickOutside(containerRef, () => onOpenChange(false), open); @@ -468,60 +148,33 @@ function TypeFilterDropdown({ {open && ( -
- {typeTabs.map((t) => ( - ))}
@@ -530,414 +183,269 @@ function TypeFilterDropdown({ ); } -function TabButton({ - active, - onClick, - icon, - label, - count, +// ─── SessionsGrid ───────────────────────────────────────────────────────────── + +function SessionsGrid({ + sessions, view, isTeamCollab = false, }: { - active: boolean; - onClick: () => void; - icon: string; - label: string; - count: number; + sessions: AnySession[]; view: CardView; isTeamCollab?: boolean; }) { + if (view === 'table') { + return ( +
+
+ {SORT_COLUMNS.map((col) => ( +
{col.label}
+ ))} +
+ {sessions.map((s) => ( + + ))} +
+ ); + } return ( - +
+ {sessions.map((s) => ( + + ))} +
); } -function SessionCard({ - session, - isTeamCollab = false, +// ─── SortableTableView ──────────────────────────────────────────────────────── + +function SortableTableView({ + sessions, sortCol, sortDir, onSort, }: { - session: AnySession; - isTeamCollab?: boolean; + sessions: AnySession[]; + sortCol: SortCol; + sortDir: 'asc' | 'desc'; + onSort: (col: SortCol) => void; }) { - const [showDeleteModal, setShowDeleteModal] = useState(false); - const [showEditModal, setShowEditModal] = useState(false); - const [isPending, startTransition] = useTransition(); - - // Edit form state - const [editTitle, setEditTitle] = useState(session.title); - const [editParticipant, setEditParticipant] = useState( - session.workshopType === 'swot' - ? (session as SwotSession).collaborator - : session.workshopType === 'year-review' - ? (session as YearReviewSession).participant - : session.workshopType === 'weather' || session.workshopType === 'gif-mood' - ? '' - : (session as MotivatorSession).participant - ); - - const workshop = getWorkshop(session.workshopType as WorkshopTypeId); - const isSwot = session.workshopType === 'swot'; - const isYearReview = session.workshopType === 'year-review'; - const isWeeklyCheckIn = session.workshopType === 'weekly-checkin'; - const isWeather = session.workshopType === 'weather'; - const isGifMood = session.workshopType === 'gif-mood'; - const href = getSessionPath(session.workshopType as WorkshopTypeId, session.id); - const participant = isSwot - ? (session as SwotSession).collaborator - : isYearReview - ? (session as YearReviewSession).participant - : isWeeklyCheckIn - ? (session as WeeklyCheckInSession).participant - : isWeather - ? (session as WeatherSession).user.name || (session as WeatherSession).user.email - : isGifMood - ? (session as GifMoodSession).user.name || (session as GifMoodSession).user.email - : (session as MotivatorSession).participant; - const accentColor = workshop.accentColor; - - const handleDelete = () => { - startTransition(async () => { - const result = isSwot - ? await deleteSwotSession(session.id) - : isYearReview - ? await deleteYearReviewSession(session.id) - : isWeeklyCheckIn - ? await deleteWeeklyCheckInSession(session.id) - : isWeather - ? await deleteWeatherSession(session.id) - : isGifMood - ? await deleteGifMoodSession(session.id) - : await deleteMotivatorSession(session.id); - - if (result.success) { - setShowDeleteModal(false); - } else { - console.error('Error deleting session:', result.error); - } - }); - }; - - const handleEdit = () => { - startTransition(async () => { - const result = isSwot - ? await updateSwotSession(session.id, { title: editTitle, collaborator: editParticipant }) - : isYearReview - ? await updateYearReviewSession(session.id, { - title: editTitle, - participant: editParticipant, - }) - : isWeeklyCheckIn - ? await updateWeeklyCheckInSession(session.id, { - title: editTitle, - participant: editParticipant, - }) - : isWeather - ? await updateWeatherSession(session.id, { title: editTitle }) - : isGifMood - ? await updateGifMoodSession(session.id, { title: editTitle }) - : await updateMotivatorSession(session.id, { - title: editTitle, - participant: editParticipant, - }); - - if (result.success) { - setShowEditModal(false); - } else { - console.error('Error updating session:', result.error); - } - }); - }; - - const openEditModal = () => { - // Reset form values when opening - setEditTitle(session.title); - setEditParticipant(participant); - setShowEditModal(true); - }; - - const editParticipantLabel = workshop.participantLabel; - - const cardContent = ( - - {/* Accent bar */} -
- - {/* Header: Icon + Title + Role badge */} -
- {workshop.icon} -

{session.title}

- {!session.isOwner && ( - Aucun atelier pour le moment
; + } + return ( +
+
+ {SORT_COLUMNS.map((col) => ( + + ))} +
+ {sessions.map((s) => ( + + ))} +
+ ); +} + +// ─── WorkshopTabs ───────────────────────────────────────────────────────────── + +export function WorkshopTabs({ + swotSessions, motivatorSessions, yearReviewSessions, + weeklyCheckInSessions, weatherSessions, gifMoodSessions, + teamCollabSessions = [], +}: WorkshopTabsProps) { + const searchParams = useSearchParams(); + const router = useRouter(); + const [typeDropdownOpen, setTypeDropdownOpen] = useState(false); + const [cardView, setCardView] = useState('grid'); + const [sortCol, setSortCol] = useState('date'); + const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc'); + + const handleSort = (col: SortCol) => { + if (sortCol === col) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc')); + else { setSortCol(col); setSortDir('asc'); } + }; + + const tabParam = searchParams.get('tab'); + const activeTab: WorkshopTabType = + tabParam && VALID_TAB_PARAMS.includes(tabParam as WorkshopTabType) + ? (tabParam as WorkshopTabType) + : 'all'; + + const setActiveTab = (tab: WorkshopTabType) => { + const params = new URLSearchParams(searchParams.toString()); + if (tab === 'all') params.delete('tab'); + else params.set('tab', tab); + router.push(`/sessions${params.toString() ? `?${params.toString()}` : ''}`); + }; + + const allSessions: AnySession[] = [ + ...swotSessions, ...motivatorSessions, ...yearReviewSessions, + ...weeklyCheckInSessions, ...weatherSessions, ...gifMoodSessions, + ].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); + + const filteredSessions: AnySession[] = + activeTab === 'all' || activeTab === 'byPerson' ? allSessions + : activeTab === 'team' ? teamCollabSessions + : activeTab === 'swot' ? swotSessions + : activeTab === 'motivators' ? motivatorSessions + : activeTab === 'year-review' ? yearReviewSessions + : activeTab === 'weekly-checkin' ? weeklyCheckInSessions + : activeTab === 'gif-mood' ? gifMoodSessions + : weatherSessions; + + const ownedSessions = filteredSessions.filter((s) => s.isOwner); + const sharedSessions = filteredSessions.filter( + (s) => !s.isOwner && !(s as AnySession & { isTeamCollab?: boolean }).isTeamCollab + ); + const teamCollabFiltered = activeTab === 'all' ? teamCollabSessions : []; + + const sessionsByPerson = groupByPerson(allSessions); + const sortedPersons = Array.from(sessionsByPerson.entries()).sort((a, b) => a[0].localeCompare(b[0], 'fr')); + + // Timeline grouping + const timelineSessions = [...(activeTab === 'all' ? [...filteredSessions, ...teamCollabSessions] : filteredSessions)] + .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); + const byMonth = new Map(); + timelineSessions.forEach((s) => { + const key = getMonthGroup(s.updatedAt); + if (!byMonth.has(key)) byMonth.set(key, []); + byMonth.get(key)!.push(s); + }); + + // Flat sorted sessions for the sortable table view + const flatTableSessions = + cardView === 'table' && activeTab !== 'byPerson' + ? sortSessions( + activeTab === 'all' ? [...filteredSessions, ...teamCollabSessions] : filteredSessions, + sortCol, sortDir, + ) + : []; + + return ( +
+ {/* Tabs + vue toggle */} +
+ setActiveTab('all')} icon="📋" label="Tous" count={allSessions.length} /> + setActiveTab('byPerson')} icon="👥" label="Par personne" count={sessionsByPerson.size} /> + {teamCollabSessions.length > 0 && ( + setActiveTab('team')} icon="🏢" label="Équipe" count={teamCollabSessions.length} /> )} +
+ +
- {/* Participant + Owner info */} -
- - {!session.isOwner && ( - - · par {session.user.name || session.user.email} - - )} -
+ {/* ── Vue Tableau flat (colonnes triables) ──────────────────── */} + {cardView === 'table' && activeTab !== 'byPerson' ? ( + - {/* Footer: Stats + Avatars + Date */} -
- {/* Stats */} -
- {isSwot ? ( - <> - {(session as SwotSession)._count.items} items - · - {(session as SwotSession)._count.actions} actions - - ) : isYearReview ? ( - <> - {(session as YearReviewSession)._count.items} items - · - Année {(session as YearReviewSession).year} - - ) : isWeeklyCheckIn ? ( - <> - {(session as WeeklyCheckInSession)._count.items} items - · - - {new Date((session as WeeklyCheckInSession).date).toLocaleDateString('fr-FR', { - day: 'numeric', - month: 'short', - })} - - - ) : isWeather ? ( - <> - {(session as WeatherSession)._count.entries} membres - · - - {new Date((session as WeatherSession).date).toLocaleDateString('fr-FR', { - day: 'numeric', - month: 'short', - })} - - - ) : isGifMood ? ( - <> - {(session as GifMoodSession)._count.items} GIFs - · - - {new Date((session as GifMoodSession).date).toLocaleDateString('fr-FR', { - day: 'numeric', - month: 'short', - })} - - - ) : ( - {(session as MotivatorSession)._count.cards}/10 + ) : cardView === 'timeline' && activeTab !== 'byPerson' ? ( + /* ── Vue Timeline ────────────────────────────────────────── */ + byMonth.size === 0 ? ( +
Aucun atelier pour le moment
+ ) : ( +
+ {Array.from(byMonth.entries()).map(([period, sessions]) => ( +
+
+
+ {period} +
+
+
+ {sessions.map((s) => ( + + ))} +
+
+ ))} +
+ ) + + ) : activeTab === 'byPerson' ? ( + /* ── Vue Par personne ───────────────────────────────────── */ + sortedPersons.length === 0 ? ( +
Aucun atelier pour le moment
+ ) : ( +
+ {sortedPersons.map(([personKey, sessions]) => { + const resolved = getResolvedCollaborator(sessions[0]); + return ( +
+
+ + + {sessions.length} atelier{sessions.length > 1 ? 's' : ''} + +
+ +
+ ); + })} +
+ ) + + ) : activeTab === 'team' ? ( + /* ── Vue Équipe ─────────────────────────────────────────── */ + teamCollabSessions.length === 0 ? ( +
Aucun atelier de vos collaborateurs (non partagés)
+ ) : ( +
+
+ +

+ En tant qu'admin d'équipe, vous voyez les ateliers de vos collaborateurs + qui ne vous sont pas encore partagés. +

+ +
+
+ ) + + ) : filteredSessions.length === 0 ? ( +
Aucun atelier de ce type pour le moment
+ + ) : ( + /* ── Vue normale (tous / par type) ─────────────────────── */ +
+ {ownedSessions.length > 0 && ( +
+ + +
+ )} + {sharedSessions.length > 0 && ( +
+ + +
+ )} + {activeTab === 'all' && teamCollabFiltered.length > 0 && ( +
+ + +
)}
- - {/* Date */} - - {new Date(session.updatedAt).toLocaleDateString('fr-FR', { - day: 'numeric', - month: 'short', - })} - -
- - {/* Shared with */} - {session.isOwner && session.shares.length > 0 && ( -
- Partagé -
- {session.shares.slice(0, 3).map((share) => ( -
- - {share.user.name?.split(' ')[0] || share.user.email.split('@')[0]} - - {share.role === 'EDITOR' ? '✏️' : '👁️'} -
- ))} - {session.shares.length > 3 && ( - +{session.shares.length - 3} - )} -
-
)} - - ); - - return ( - <> -
- - {cardContent} - - - {/* Edit: owner, EDITOR, or team admin | Delete: owner or team admin only (not EDITOR) */} - {(session.isOwner || session.role === 'EDITOR' || session.isTeamCollab) && ( -
- - {(session.isOwner || session.isTeamCollab) && ( - - )} -
- )} -
- - {/* Edit modal */} - setShowEditModal(false)} - title="Modifier l'atelier" - size="sm" - > -
{ - e.preventDefault(); - handleEdit(); - }} - className="space-y-4" - > -
- - setEditTitle(e.target.value)} - placeholder="Titre de l'atelier" - required - /> -
-
- - {!isWeather && !isGifMood && ( - setEditParticipant(e.target.value)} - placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'} - required - /> - )} -
- - - - -
-
- - {/* Delete confirmation modal */} - setShowDeleteModal(false)} - title="Supprimer l'atelier" - size="sm" - > -
-

- Êtes-vous sûr de vouloir supprimer l'atelier{' '} - "{session.title}" ? -

-

- Cette action est irréversible. Toutes les données seront perdues. -

- - - - -
-
- +
); } diff --git a/src/app/sessions/page.tsx b/src/app/sessions/page.tsx index b721d75..177d6f3 100644 --- a/src/app/sessions/page.tsx +++ b/src/app/sessions/page.tsx @@ -33,15 +33,15 @@ function WorkshopTabsSkeleton() { return (
{/* Tabs skeleton */} -
+
{[...Array(4)].map((_, i) => ( -
+
))}
{/* Cards skeleton */}
{[...Array(6)].map((_, i) => ( -
+
))}
@@ -110,14 +110,22 @@ export default async function SessionsPage() { ].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()); const hasNoSessions = allSessions.length === 0; + const totalCount = allSessions.length; return ( -
+
{/* Header */} -
+
-

Mes Ateliers

-

Tous vos ateliers en un seul endroit

+
+

Mes Ateliers

+ {totalCount > 0 && ( + + {totalCount} + + )} +
+

Tous vos ateliers en un seul endroit

diff --git a/src/app/sessions/workshop-session-helpers.ts b/src/app/sessions/workshop-session-helpers.ts new file mode 100644 index 0000000..878212e --- /dev/null +++ b/src/app/sessions/workshop-session-helpers.ts @@ -0,0 +1,93 @@ +import type { + AnySession, SortCol, ResolvedCollaborator, + SwotSession, MotivatorSession, YearReviewSession, + WeeklyCheckInSession, WeatherSession, GifMoodSession, +} from './workshop-session-types'; + +export function getResolvedCollaborator(session: AnySession): ResolvedCollaborator { + if (session.workshopType === 'swot') return (session as SwotSession).resolvedCollaborator; + if (session.workshopType === 'year-review') return (session as YearReviewSession).resolvedParticipant; + if (session.workshopType === 'weekly-checkin') return (session as WeeklyCheckInSession).resolvedParticipant; + if (session.workshopType === 'weather') { + const s = session as WeatherSession; + return { raw: s.user.name || s.user.email, matchedUser: { id: s.user.id, email: s.user.email, name: s.user.name } }; + } + if (session.workshopType === 'gif-mood') { + const s = session as GifMoodSession; + return { raw: s.user.name || s.user.email, matchedUser: { id: s.user.id, email: s.user.email, name: s.user.name } }; + } + return (session as MotivatorSession).resolvedParticipant; +} + +export function getGroupKey(session: AnySession): string { + const r = getResolvedCollaborator(session); + return r.matchedUser ? `user:${r.matchedUser.id}` : `raw:${r.raw.trim().toLowerCase()}`; +} + +export function groupByPerson(sessions: AnySession[]): Map { + const grouped = new Map(); + sessions.forEach((s) => { + const key = getGroupKey(s); + const existing = grouped.get(key); + if (existing) existing.push(s); + else grouped.set(key, [s]); + }); + grouped.forEach((arr) => arr.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())); + return grouped; +} + +export function formatDate(date: Date | string): string { + return new Date(date).toLocaleDateString('fr-FR', { day: 'numeric', month: 'short' }); +} + +export function getMonthGroup(date: Date | string): string { + return new Date(date).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' }); +} + +export function getStatsText(session: AnySession): string { + const isSwot = session.workshopType === 'swot'; + const isYearReview = session.workshopType === 'year-review'; + const isWeeklyCheckIn = session.workshopType === 'weekly-checkin'; + const isWeather = session.workshopType === 'weather'; + const isGifMood = session.workshopType === 'gif-mood'; + + if (isSwot) return `${(session as SwotSession)._count.items} items · ${(session as SwotSession)._count.actions} actions`; + if (isYearReview) return `${(session as YearReviewSession)._count.items} items · ${(session as YearReviewSession).year}`; + if (isWeeklyCheckIn) return `${(session as WeeklyCheckInSession)._count.items} items · ${formatDate((session as WeeklyCheckInSession).date)}`; + if (isWeather) return `${(session as WeatherSession)._count.entries} membres · ${formatDate((session as WeatherSession).date)}`; + if (isGifMood) return `${(session as GifMoodSession)._count.items} GIFs · ${formatDate((session as GifMoodSession).date)}`; + return `${(session as MotivatorSession)._count.cards}/10 motivateurs`; +} + +function getStatsSortValue(session: AnySession): number { + if (session.workshopType === 'swot') return (session as SwotSession)._count.items; + if (session.workshopType === 'year-review') return (session as YearReviewSession)._count.items; + if (session.workshopType === 'weekly-checkin') return (session as WeeklyCheckInSession)._count.items; + if (session.workshopType === 'weather') return (session as WeatherSession)._count.entries; + if (session.workshopType === 'gif-mood') return (session as GifMoodSession)._count.items; + return (session as MotivatorSession)._count.cards; +} + +function getParticipantSortName(session: AnySession): string { + const r = getResolvedCollaborator(session); + return (r.matchedUser?.name || r.matchedUser?.email?.split('@')[0] || r.raw).toLowerCase(); +} + +function getCreatorName(session: AnySession): string { + return (session.user.name || session.user.email).toLowerCase(); +} + +export function sortSessions(sessions: AnySession[], col: SortCol, dir: 'asc' | 'desc'): AnySession[] { + return [...sessions].sort((a, b) => { + let cmp = 0; + switch (col) { + case 'type': cmp = a.workshopType.localeCompare(b.workshopType); break; + case 'titre': cmp = a.title.localeCompare(b.title, 'fr'); break; + case 'createur': cmp = getCreatorName(a).localeCompare(getCreatorName(b), 'fr'); break; + case 'participant': cmp = getParticipantSortName(a).localeCompare(getParticipantSortName(b), 'fr'); break; + case 'stats': cmp = getStatsSortValue(a) - getStatsSortValue(b); break; + case 'date': cmp = new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(); break; + } + return dir === 'asc' ? cmp : -cmp; + }); +} diff --git a/src/app/sessions/workshop-session-types.ts b/src/app/sessions/workshop-session-types.ts new file mode 100644 index 0000000..15ebdf5 --- /dev/null +++ b/src/app/sessions/workshop-session-types.ts @@ -0,0 +1,94 @@ +import { WORKSHOPS } from '@/lib/workshops'; +import type { Share } from '@/lib/share-utils'; + +export type CardView = 'grid' | 'list' | 'table' | 'timeline'; +export type SortCol = 'type' | 'titre' | 'createur' | 'participant' | 'stats' | 'date'; + +// Colonnes tableau : type | titre | créateur | participant | stats | date +export const TABLE_COLS = '160px 1fr 160px 160px 160px 76px'; + +export const SORT_COLUMNS: { key: SortCol; label: string }[] = [ + { key: 'type', label: 'Type' }, + { key: 'titre', label: 'Titre' }, + { key: 'createur', label: 'Créateur' }, + { key: 'participant', label: 'Participant' }, + { key: 'stats', label: 'Stats' }, + { key: 'date', label: 'Date' }, +]; + +export const TYPE_TABS = [ + { value: 'all' as const, icon: '📋', label: 'Tous' }, + { value: 'team' as const, icon: '🏢', label: 'Équipe' }, + ...WORKSHOPS.map((w) => ({ value: w.id, icon: w.icon, label: w.labelShort })), +]; + +export interface ResolvedCollaborator { + raw: string; + matchedUser: { id: string; email: string; name: string | null } | null; +} + +export interface SwotSession { + id: string; title: string; collaborator: string; + resolvedCollaborator: ResolvedCollaborator; updatedAt: Date; + isOwner: boolean; role: 'OWNER' | 'VIEWER' | 'EDITOR'; + user: { id: string; name: string | null; email: string }; + shares: Share[]; _count: { items: number; actions: number }; + workshopType: 'swot'; isTeamCollab?: true; canEdit?: boolean; +} + +export interface MotivatorSession { + id: string; title: string; participant: string; + resolvedParticipant: ResolvedCollaborator; updatedAt: Date; + isOwner: boolean; role: 'OWNER' | 'VIEWER' | 'EDITOR'; + user: { id: string; name: string | null; email: string }; + shares: Share[]; _count: { cards: number }; + workshopType: 'motivators'; isTeamCollab?: true; canEdit?: boolean; +} + +export interface YearReviewSession { + id: string; title: string; participant: string; + resolvedParticipant: ResolvedCollaborator; year: number; updatedAt: Date; + isOwner: boolean; role: 'OWNER' | 'VIEWER' | 'EDITOR'; + user: { id: string; name: string | null; email: string }; + shares: Share[]; _count: { items: number }; + workshopType: 'year-review'; isTeamCollab?: true; canEdit?: boolean; +} + +export interface WeeklyCheckInSession { + id: string; title: string; participant: string; + resolvedParticipant: ResolvedCollaborator; date: Date; updatedAt: Date; + isOwner: boolean; role: 'OWNER' | 'VIEWER' | 'EDITOR'; + user: { id: string; name: string | null; email: string }; + shares: Share[]; _count: { items: number }; + workshopType: 'weekly-checkin'; isTeamCollab?: true; canEdit?: boolean; +} + +export interface WeatherSession { + id: string; title: string; date: Date; updatedAt: Date; + isOwner: boolean; role: 'OWNER' | 'VIEWER' | 'EDITOR'; + user: { id: string; name: string | null; email: string }; + shares: Share[]; _count: { entries: number }; + workshopType: 'weather'; isTeamCollab?: true; canEdit?: boolean; +} + +export interface GifMoodSession { + id: string; title: string; date: Date; updatedAt: Date; + isOwner: boolean; role: 'OWNER' | 'VIEWER' | 'EDITOR'; + user: { id: string; name: string | null; email: string }; + shares: Share[]; _count: { items: number }; + workshopType: 'gif-mood'; isTeamCollab?: true; canEdit?: boolean; +} + +export type AnySession = + | SwotSession | MotivatorSession | YearReviewSession + | WeeklyCheckInSession | WeatherSession | GifMoodSession; + +export interface WorkshopTabsProps { + swotSessions: SwotSession[]; + motivatorSessions: MotivatorSession[]; + yearReviewSessions: YearReviewSession[]; + weeklyCheckInSessions: WeeklyCheckInSession[]; + weatherSessions: WeatherSession[]; + gifMoodSessions: GifMoodSession[]; + teamCollabSessions?: (AnySession & { isTeamCollab?: true })[]; +}