All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m14s
880 lines
30 KiB
TypeScript
880 lines
30 KiB
TypeScript
'use client';
|
||
|
||
import { useState, useTransition, useRef } from 'react';
|
||
import Link from 'next/link';
|
||
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 {
|
||
type WorkshopTabType,
|
||
type WorkshopTypeId,
|
||
WORKSHOPS,
|
||
VALID_TAB_PARAMS,
|
||
getWorkshop,
|
||
getSessionPath,
|
||
} from '@/lib/workshops';
|
||
import { useClickOutside } from '@/hooks/useClickOutside';
|
||
|
||
import type { Share } from '@/lib/share-utils';
|
||
|
||
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;
|
||
}
|
||
|
||
type AnySession = SwotSession | MotivatorSession | YearReviewSession | WeeklyCheckInSession | WeatherSession;
|
||
|
||
interface WorkshopTabsProps {
|
||
swotSessions: SwotSession[];
|
||
motivatorSessions: MotivatorSession[];
|
||
yearReviewSessions: YearReviewSession[];
|
||
weeklyCheckInSessions: WeeklyCheckInSession[];
|
||
weatherSessions: WeatherSession[];
|
||
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') {
|
||
// For weather sessions, use the owner as the "participant" since it's a personal 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 {
|
||
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<string, AnySession[]> {
|
||
const grouped = new Map<string, AnySession[]>();
|
||
|
||
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,
|
||
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,
|
||
].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
|
||
: 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);
|
||
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')
|
||
);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
{/* Tabs */}
|
||
<div className="flex gap-2 border-b border-border pb-4 flex-wrap items-center">
|
||
<TabButton
|
||
active={activeTab === 'all'}
|
||
onClick={() => setActiveTab('all')}
|
||
icon="📋"
|
||
label="Tous"
|
||
count={allSessions.length}
|
||
/>
|
||
<TabButton
|
||
active={activeTab === 'byPerson'}
|
||
onClick={() => setActiveTab('byPerson')}
|
||
icon="👥"
|
||
label="Par personne"
|
||
count={sessionsByPerson.size}
|
||
/>
|
||
{teamCollabSessions.length > 0 && (
|
||
<TabButton
|
||
active={activeTab === 'team'}
|
||
onClick={() => setActiveTab('team')}
|
||
icon="🏢"
|
||
label="Équipe"
|
||
count={teamCollabSessions.length}
|
||
/>
|
||
)}
|
||
<TypeFilterDropdown
|
||
activeTab={activeTab}
|
||
setActiveTab={setActiveTab}
|
||
open={typeDropdownOpen}
|
||
onOpenChange={setTypeDropdownOpen}
|
||
counts={{
|
||
swot: swotSessions.length,
|
||
motivators: motivatorSessions.length,
|
||
'year-review': yearReviewSessions.length,
|
||
'weekly-checkin': weeklyCheckInSessions.length,
|
||
weather: weatherSessions.length,
|
||
team: teamCollabSessions.length,
|
||
}}
|
||
/>
|
||
</div>
|
||
|
||
{/* Sessions */}
|
||
{activeTab === 'byPerson' ? (
|
||
// By Person View
|
||
sortedPersons.length === 0 ? (
|
||
<div className="text-center py-12 text-muted">Aucun atelier pour le moment</div>
|
||
) : (
|
||
<div className="space-y-8">
|
||
{sortedPersons.map(([personKey, sessions]) => {
|
||
const resolved = getResolvedCollaborator(sessions[0]);
|
||
return (
|
||
<section key={personKey}>
|
||
<h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-3">
|
||
<CollaboratorDisplay collaborator={resolved} size="md" />
|
||
<Badge variant="primary">
|
||
{sessions.length} atelier{sessions.length > 1 ? 's' : ''}
|
||
</Badge>
|
||
</h2>
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{sessions.map((s) => (
|
||
<SessionCard key={s.id} session={s} />
|
||
))}
|
||
</div>
|
||
</section>
|
||
);
|
||
})}
|
||
</div>
|
||
)
|
||
) : activeTab === 'team' ? (
|
||
teamCollabSessions.length === 0 ? (
|
||
<div className="text-center py-12 text-muted">
|
||
Aucun atelier de vos collaborateurs d'équipe (non partagés avec vous)
|
||
</div>
|
||
) : (
|
||
<div className="space-y-8">
|
||
<section>
|
||
<h2 className="text-lg font-semibold text-muted mb-4">
|
||
🏢 Ateliers de l'équipe – non partagés ({teamCollabSessions.length})
|
||
</h2>
|
||
<p className="text-sm text-muted mb-4">
|
||
En tant qu'admin d'équipe, vous voyez les ateliers de vos collaborateurs qui ne vous sont pas encore partagés.
|
||
</p>
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{teamCollabSessions.map((s) => (
|
||
<SessionCard key={s.id} session={s} isTeamCollab />
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
)
|
||
) : filteredSessions.length === 0 ? (
|
||
<div className="text-center py-12 text-muted">Aucun atelier de ce type pour le moment</div>
|
||
) : (
|
||
<div className="space-y-8">
|
||
{/* My Sessions */}
|
||
{ownedSessions.length > 0 && (
|
||
<section>
|
||
<h2 className="text-lg font-semibold text-foreground mb-4">
|
||
📁 Mes ateliers ({ownedSessions.length})
|
||
</h2>
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{ownedSessions.map((s) => (
|
||
<SessionCard key={s.id} session={s} />
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Shared Sessions */}
|
||
{sharedSessions.length > 0 && (
|
||
<section>
|
||
<h2 className="text-lg font-semibold text-foreground mb-4">
|
||
🤝 Partagés avec moi ({sharedSessions.length})
|
||
</h2>
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{sharedSessions.map((s) => (
|
||
<SessionCard key={s.id} session={s} />
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
|
||
{/* Team collab sessions (non-shared) - grayed out, admin view only */}
|
||
{activeTab === 'all' && teamCollabFiltered.length > 0 && (
|
||
<section>
|
||
<h2 className="text-lg font-semibold text-muted mb-4">
|
||
🏢 Équipe – non partagés ({teamCollabFiltered.length})
|
||
</h2>
|
||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
{teamCollabFiltered.map((s) => (
|
||
<SessionCard key={s.id} session={s} isTeamCollab />
|
||
))}
|
||
</div>
|
||
</section>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TypeFilterDropdown({
|
||
activeTab,
|
||
setActiveTab,
|
||
open,
|
||
onOpenChange,
|
||
counts,
|
||
}: {
|
||
activeTab: WorkshopTabType;
|
||
setActiveTab: (t: WorkshopTabType) => void;
|
||
open: boolean;
|
||
onOpenChange: (v: boolean) => void;
|
||
counts: Record<string, number>;
|
||
}) {
|
||
const typeTabs = TYPE_TABS.filter((t) => t.value !== 'all');
|
||
const current = TYPE_TABS.find((t) => t.value === activeTab) ?? TYPE_TABS[0];
|
||
const isTypeSelected = activeTab !== 'all' && activeTab !== 'byPerson';
|
||
const totalCount = typeTabs.reduce((s, t) => s + (counts[t.value] ?? 0), 0);
|
||
const containerRef = useRef<HTMLDivElement>(null);
|
||
useClickOutside(containerRef, () => onOpenChange(false), open);
|
||
|
||
return (
|
||
<div ref={containerRef} className="relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => onOpenChange(!open)}
|
||
className={`
|
||
flex items-center gap-2 px-3 py-2 rounded-lg font-medium text-sm transition-colors
|
||
${isTypeSelected ? 'bg-primary text-primary-foreground' : 'text-muted hover:bg-card-hover hover:text-foreground'}
|
||
`}
|
||
>
|
||
<span>{current.icon}</span>
|
||
<span>{current.label}</span>
|
||
<Badge variant={isTypeSelected ? 'default' : 'primary'} className="ml-1 text-xs">
|
||
{isTypeSelected ? counts[activeTab] ?? 0 : totalCount}
|
||
</Badge>
|
||
<svg
|
||
className={`h-4 w-4 transition-transform ${open ? 'rotate-180' : ''}`}
|
||
fill="none"
|
||
viewBox="0 0 24 24"
|
||
stroke="currentColor"
|
||
>
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||
</svg>
|
||
</button>
|
||
{open && (
|
||
<div className="absolute left-0 z-20 mt-2 w-44 rounded-lg border border-border bg-card py-1 shadow-lg">
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
setActiveTab('all');
|
||
onOpenChange(false);
|
||
}}
|
||
className="flex w-full items-center justify-between gap-2 px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover border-b border-border"
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<span>📋</span>
|
||
<span>Tous</span>
|
||
</span>
|
||
<Badge variant="primary" className="text-xs">
|
||
{totalCount}
|
||
</Badge>
|
||
</button>
|
||
{typeTabs.map((t) => (
|
||
<button
|
||
key={t.value}
|
||
type="button"
|
||
onClick={() => {
|
||
setActiveTab(t.value);
|
||
onOpenChange(false);
|
||
}}
|
||
className="flex w-full items-center justify-between gap-2 px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover"
|
||
>
|
||
<span className="flex items-center gap-2">
|
||
<span>{t.icon}</span>
|
||
<span>{t.label}</span>
|
||
</span>
|
||
<Badge variant="primary" className="text-xs">
|
||
{counts[t.value] ?? 0}
|
||
</Badge>
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function TabButton({
|
||
active,
|
||
onClick,
|
||
icon,
|
||
label,
|
||
count,
|
||
}: {
|
||
active: boolean;
|
||
onClick: () => void;
|
||
icon: string;
|
||
label: string;
|
||
count: number;
|
||
}) {
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={onClick}
|
||
className={`
|
||
flex items-center gap-2 px-3 py-2 rounded-lg font-medium text-sm transition-colors
|
||
${
|
||
active
|
||
? 'bg-primary text-primary-foreground'
|
||
: 'text-muted hover:bg-card-hover hover:text-foreground'
|
||
}
|
||
`}
|
||
>
|
||
<span>{icon}</span>
|
||
<span>{label}</span>
|
||
<Badge variant={active ? 'default' : 'primary'} className="ml-1 text-xs">
|
||
{count}
|
||
</Badge>
|
||
</button>
|
||
);
|
||
}
|
||
|
||
function SessionCard({ session, isTeamCollab = false }: { session: AnySession; isTeamCollab?: boolean }) {
|
||
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 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 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
|
||
: (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)
|
||
: 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 })
|
||
: 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 = (
|
||
<Card hover={!isTeamCollab} className={`h-full p-4 relative overflow-hidden ${isTeamCollab ? 'opacity-60' : ''}`}>
|
||
{/* Accent bar */}
|
||
<div
|
||
className="absolute top-0 left-0 right-0 h-1"
|
||
style={{ backgroundColor: accentColor }}
|
||
/>
|
||
|
||
{/* Header: Icon + Title + Role badge */}
|
||
<div className="flex items-center gap-2 mb-2">
|
||
<span className="text-xl">{workshop.icon}</span>
|
||
<h3 className="font-semibold text-foreground line-clamp-1 flex-1">{session.title}</h3>
|
||
{!session.isOwner && (
|
||
<span
|
||
className="text-xs px-1.5 py-0.5 rounded"
|
||
style={{
|
||
backgroundColor:
|
||
session.role === 'EDITOR' ? 'rgba(6,182,212,0.1)' : 'rgba(234,179,8,0.1)',
|
||
color: session.role === 'EDITOR' ? '#06b6d4' : '#eab308',
|
||
}}
|
||
>
|
||
{session.role === 'EDITOR' ? '✏️' : '👁️'}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Participant + Owner info */}
|
||
<div className="mb-3 flex items-center gap-2">
|
||
<CollaboratorDisplay collaborator={getResolvedCollaborator(session)} size="sm" />
|
||
{!session.isOwner && (
|
||
<span className="text-xs text-muted">
|
||
· par {session.user.name || session.user.email}
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Footer: Stats + Avatars + Date */}
|
||
<div className="flex items-center justify-between text-xs">
|
||
{/* Stats */}
|
||
<div className="flex items-center gap-2 text-muted">
|
||
{isSwot ? (
|
||
<>
|
||
<span>{(session as SwotSession)._count.items} items</span>
|
||
<span>·</span>
|
||
<span>{(session as SwotSession)._count.actions} actions</span>
|
||
</>
|
||
) : isYearReview ? (
|
||
<>
|
||
<span>{(session as YearReviewSession)._count.items} items</span>
|
||
<span>·</span>
|
||
<span>Année {(session as YearReviewSession).year}</span>
|
||
</>
|
||
) : isWeeklyCheckIn ? (
|
||
<>
|
||
<span>{(session as WeeklyCheckInSession)._count.items} items</span>
|
||
<span>·</span>
|
||
<span>
|
||
{new Date((session as WeeklyCheckInSession).date).toLocaleDateString('fr-FR', {
|
||
day: 'numeric',
|
||
month: 'short',
|
||
})}
|
||
</span>
|
||
</>
|
||
) : isWeather ? (
|
||
<>
|
||
<span>{(session as WeatherSession)._count.entries} membres</span>
|
||
<span>·</span>
|
||
<span>
|
||
{new Date((session as WeatherSession).date).toLocaleDateString('fr-FR', {
|
||
day: 'numeric',
|
||
month: 'short',
|
||
})}
|
||
</span>
|
||
</>
|
||
) : (
|
||
<span>{(session as MotivatorSession)._count.cards}/10</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* Date */}
|
||
<span className="text-muted">
|
||
{new Date(session.updatedAt).toLocaleDateString('fr-FR', {
|
||
day: 'numeric',
|
||
month: 'short',
|
||
})}
|
||
</span>
|
||
</div>
|
||
|
||
{/* Shared with */}
|
||
{session.isOwner && session.shares.length > 0 && (
|
||
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border">
|
||
<span className="text-[10px] text-muted uppercase tracking-wide">Partagé</span>
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{session.shares.slice(0, 3).map((share) => (
|
||
<div
|
||
key={share.id}
|
||
className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-primary/10 text-[10px] text-primary"
|
||
title={share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
||
>
|
||
<span className="font-medium">
|
||
{share.user.name?.split(' ')[0] || share.user.email.split('@')[0]}
|
||
</span>
|
||
<span>{share.role === 'EDITOR' ? '✏️' : '👁️'}</span>
|
||
</div>
|
||
))}
|
||
{session.shares.length > 3 && (
|
||
<span className="text-[10px] text-muted">+{session.shares.length - 3}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</Card>
|
||
);
|
||
|
||
return (
|
||
<>
|
||
<div className="relative group">
|
||
<Link
|
||
href={href}
|
||
className={isTeamCollab ? 'cursor-pointer' : ''}
|
||
title={isTeamCollab ? "Atelier de l'équipe – éditable en tant qu'admin" : undefined}
|
||
>
|
||
{cardContent}
|
||
</Link>
|
||
|
||
{/* Edit: owner, EDITOR, or team admin | Delete: owner or team admin only (not EDITOR) */}
|
||
{(session.isOwner || session.role === 'EDITOR' || session.isTeamCollab) && (
|
||
<div className="absolute top-3 right-3 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||
<button
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
openEditModal();
|
||
}}
|
||
className="p-1.5 rounded-lg bg-primary/10 text-primary hover:bg-primary/20"
|
||
title="Modifier"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
{(session.isOwner || session.isTeamCollab) && (
|
||
<button
|
||
onClick={(e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
setShowDeleteModal(true);
|
||
}}
|
||
className="p-1.5 rounded-lg bg-destructive/10 text-destructive hover:bg-destructive/20"
|
||
title="Supprimer"
|
||
>
|
||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path
|
||
strokeLinecap="round"
|
||
strokeLinejoin="round"
|
||
strokeWidth={2}
|
||
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
|
||
/>
|
||
</svg>
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* Edit modal */}
|
||
<Modal
|
||
isOpen={showEditModal}
|
||
onClose={() => setShowEditModal(false)}
|
||
title="Modifier l'atelier"
|
||
size="sm"
|
||
>
|
||
<form
|
||
onSubmit={(e) => {
|
||
e.preventDefault();
|
||
handleEdit();
|
||
}}
|
||
className="space-y-4"
|
||
>
|
||
<div>
|
||
<label htmlFor="edit-title" className="block text-sm font-medium text-foreground mb-1">
|
||
Titre
|
||
</label>
|
||
<Input
|
||
id="edit-title"
|
||
value={editTitle}
|
||
onChange={(e) => setEditTitle(e.target.value)}
|
||
placeholder="Titre de l'atelier"
|
||
required
|
||
/>
|
||
</div>
|
||
<div>
|
||
<label
|
||
htmlFor="edit-participant"
|
||
className="block text-sm font-medium text-foreground mb-1"
|
||
>
|
||
{editParticipantLabel}
|
||
</label>
|
||
{!isWeather && (
|
||
<Input
|
||
id="edit-participant"
|
||
value={editParticipant}
|
||
onChange={(e) => setEditParticipant(e.target.value)}
|
||
placeholder={isSwot ? 'Nom du collaborateur' : 'Nom du participant'}
|
||
required
|
||
/>
|
||
)}
|
||
</div>
|
||
<ModalFooter>
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
onClick={() => setShowEditModal(false)}
|
||
disabled={isPending}
|
||
>
|
||
Annuler
|
||
</Button>
|
||
<Button
|
||
type="submit"
|
||
disabled={isPending || !editTitle.trim() || (!isWeather && !editParticipant.trim())}
|
||
>
|
||
{isPending ? 'Enregistrement...' : 'Enregistrer'}
|
||
</Button>
|
||
</ModalFooter>
|
||
</form>
|
||
</Modal>
|
||
|
||
{/* Delete confirmation modal */}
|
||
<Modal
|
||
isOpen={showDeleteModal}
|
||
onClose={() => setShowDeleteModal(false)}
|
||
title="Supprimer l'atelier"
|
||
size="sm"
|
||
>
|
||
<div className="space-y-4">
|
||
<p className="text-muted">
|
||
Êtes-vous sûr de vouloir supprimer l'atelier{' '}
|
||
<strong className="text-foreground">"{session.title}"</strong> ?
|
||
</p>
|
||
<p className="text-sm text-destructive">
|
||
Cette action est irréversible. Toutes les données seront perdues.
|
||
</p>
|
||
<ModalFooter>
|
||
<Button variant="ghost" onClick={() => setShowDeleteModal(false)} disabled={isPending}>
|
||
Annuler
|
||
</Button>
|
||
<Button variant="destructive" onClick={handleDelete} disabled={isPending}>
|
||
{isPending ? 'Suppression...' : 'Supprimer'}
|
||
</Button>
|
||
</ModalFooter>
|
||
</div>
|
||
</Modal>
|
||
</>
|
||
);
|
||
}
|