Compare commits

...

2 Commits

Author SHA1 Message Date
Julien Froidefond
cc7e73ce7b feat: refactor workshop management by centralizing workshop data and improving session navigation across components
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m0s
2026-02-17 09:43:08 +01:00
Julien Froidefond
a8f53bfe2a feat: replace individual workshop buttons with a dropdown for creating new workshops in SessionsPage and update WorkshopTabs for improved tab management 2026-02-17 09:30:46 +01:00
11 changed files with 419 additions and 275 deletions

View File

@@ -1,6 +1,7 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getMotivatorSessionById } from '@/services/moving-motivators'; import { getMotivatorSessionById } from '@/services/moving-motivators';
import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators'; import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators';
import { Badge, CollaboratorDisplay } from '@/components/ui'; import { Badge, CollaboratorDisplay } from '@/components/ui';
@@ -29,8 +30,8 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2"> <div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href="/sessions?tab=motivators" className="hover:text-foreground"> <Link href={getSessionsTabUrl('motivators')} className="hover:text-foreground">
Moving Motivators {getWorkshop('motivators').label}
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-foreground">{session.title}</span> <span className="text-foreground">{session.title}</span>

View File

@@ -1,4 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { WORKSHOPS, getSessionsTabUrl } from '@/lib/workshops';
export default function Home() { export default function Home() {
return ( return (
@@ -21,85 +22,19 @@ export default function Home() {
Choisissez votre atelier Choisissez votre atelier
</h2> </h2>
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 max-w-6xl mx-auto"> <div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3 max-w-6xl mx-auto">
{/* SWOT Workshop Card */} {WORKSHOPS.map((w) => (
<WorkshopCard <WorkshopCard
href="/sessions?tab=swot" key={w.id}
icon="📊" href={getSessionsTabUrl(w.id)}
title="Analyse SWOT" icon={w.icon}
tagline="Analysez. Planifiez. Progressez." title={w.cardLabel}
description="Cartographiez les forces et faiblesses de vos collaborateurs. Identifiez opportunités et menaces pour définir des actions concrètes." tagline={w.home.tagline}
features={[ description={w.home.description}
'Matrice interactive Forces/Faiblesses/Opportunités/Menaces', features={w.home.features}
'Actions croisées et plan de développement', accentColor={w.accentColor}
'Collaboration en temps réel', newHref={w.newPath}
]} />
accentColor="#06b6d4" ))}
newHref="/sessions/new"
/>
{/* Moving Motivators Workshop Card */}
<WorkshopCard
href="/sessions?tab=motivators"
icon="🎯"
title="Moving Motivators"
tagline="Révélez ce qui motive vraiment"
description="Explorez les 10 motivations intrinsèques de vos collaborateurs. Comprenez leur impact et alignez aspirations et missions."
features={[
'10 cartes de motivation à classer',
"Évaluation de l'influence positive/négative",
'Récapitulatif personnalisé des motivations',
]}
accentColor="#8b5cf6"
newHref="/motivators/new"
/>
{/* Year Review Workshop Card */}
<WorkshopCard
href="/sessions?tab=year-review"
icon="📅"
title="Year Review"
tagline="Faites le bilan de l'année"
description="Réalisez un bilan complet de l'année écoulée. Identifiez réalisations, défis, apprentissages et définissez vos objectifs pour l'année à venir."
features={[
'5 catégories : Réalisations, Défis, Apprentissages, Objectifs, Moments',
'Organisation par drag & drop',
'Vue d\'ensemble de l\'année',
]}
accentColor="#f59e0b"
newHref="/year-review/new"
/>
{/* Weekly Check-in Workshop Card */}
<WorkshopCard
href="/sessions?tab=weekly-checkin"
icon="📝"
title="Weekly Check-in"
tagline="Le point hebdomadaire avec vos collaborateurs"
description="Chaque semaine, faites le point avec vos collaborateurs sur ce qui s'est bien passé, ce qui s'est mal passé, les enjeux du moment et les prochains enjeux."
features={[
'4 catégories : Bien passé, Mal passé, Enjeux du moment, Prochains enjeux',
'Ajout d\'émotions à chaque item (fierté, joie, frustration, etc.)',
'Suivi hebdomadaire régulier',
]}
accentColor="#10b981"
newHref="/weekly-checkin/new"
/>
{/* Weather Workshop Card */}
<WorkshopCard
href="/sessions?tab=weather"
icon="🌤️"
title="Météo"
tagline="Votre état en un coup d'œil"
description="Créez votre météo personnelle sur 4 axes clés (Performance, Moral, Flux, Création de valeur) et partagez-la avec votre équipe pour une meilleure visibilité de votre état."
features={[
'4 axes : Performance, Moral, Flux, Création de valeur',
'Emojis météo pour exprimer votre état visuellement',
'Notes globales pour détailler votre ressenti',
]}
accentColor="#3b82f6"
newHref="/weather/new"
/>
</div> </div>
</section> </section>

View File

@@ -0,0 +1,51 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { Button } from '@/components/ui';
import { WORKSHOPS } from '@/lib/workshops';
export function NewWorkshopDropdown() {
const [open, setOpen] = useState(false);
return (
<div className="relative">
<Button
type="button"
variant="outline"
size="sm"
onClick={() => setOpen(!open)}
onBlur={() => setTimeout(() => setOpen(false), 150)}
className="gap-1.5"
>
Nouvel atelier
<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 right-0 z-20 mt-2 w-56 rounded-lg border border-border bg-card py-1 shadow-lg">
{WORKSHOPS.map((w) => (
<Link
key={w.id}
href={w.newPath}
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover"
onClick={() => setOpen(false)}
>
<span className="text-lg">{w.icon}</span>
<div>
<div className="font-medium">{w.label}</div>
<div className="text-xs text-muted">{w.description}</div>
</div>
</Link>
))}
</div>
)}
</div>
);
}

View File

@@ -17,17 +17,18 @@ import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving
import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review'; import { deleteYearReviewSession, updateYearReviewSession } from '@/actions/year-review';
import { deleteWeeklyCheckInSession, updateWeeklyCheckInSession } from '@/actions/weekly-checkin'; import { deleteWeeklyCheckInSession, updateWeeklyCheckInSession } from '@/actions/weekly-checkin';
import { deleteWeatherSession, updateWeatherSession } from '@/actions/weather'; import { deleteWeatherSession, updateWeatherSession } from '@/actions/weather';
import {
type WorkshopTabType,
type WorkshopTypeId,
WORKSHOPS,
VALID_TAB_PARAMS,
getWorkshop,
getSessionPath,
} from '@/lib/workshops';
type WorkshopType = 'all' | 'swot' | 'motivators' | 'year-review' | 'weekly-checkin' | 'weather' | 'byPerson'; const TYPE_TABS = [
{ value: 'all' as const, icon: '📋', label: 'Tous' },
const VALID_TABS: WorkshopType[] = [ ...WORKSHOPS.map((w) => ({ value: w.id, icon: w.icon, label: w.labelShort })),
'all',
'swot',
'motivators',
'year-review',
'weekly-checkin',
'weather',
'byPerson',
]; ];
interface ShareUser { interface ShareUser {
@@ -199,13 +200,16 @@ export function WorkshopTabs({
}: WorkshopTabsProps) { }: WorkshopTabsProps) {
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const router = useRouter(); const router = useRouter();
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
// Get tab from URL or default to 'all' // Get tab from URL or default to 'all'
const tabParam = searchParams.get('tab'); const tabParam = searchParams.get('tab');
const activeTab: WorkshopType = const activeTab: WorkshopTabType =
tabParam && VALID_TABS.includes(tabParam as WorkshopType) ? (tabParam as WorkshopType) : 'all'; tabParam && VALID_TAB_PARAMS.includes(tabParam as WorkshopTabType)
? (tabParam as WorkshopTabType)
: 'all';
const setActiveTab = (tab: WorkshopType) => { const setActiveTab = (tab: WorkshopTabType) => {
const params = new URLSearchParams(searchParams.toString()); const params = new URLSearchParams(searchParams.toString());
if (tab === 'all') { if (tab === 'all') {
params.delete('tab'); params.delete('tab');
@@ -251,7 +255,7 @@ export function WorkshopTabs({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Tabs */} {/* Tabs */}
<div className="flex gap-2 border-b border-border pb-4 flex-wrap"> <div className="flex gap-2 border-b border-border pb-4 flex-wrap items-center">
<TabButton <TabButton
active={activeTab === 'all'} active={activeTab === 'all'}
onClick={() => setActiveTab('all')} onClick={() => setActiveTab('all')}
@@ -266,40 +270,18 @@ export function WorkshopTabs({
label="Par personne" label="Par personne"
count={sessionsByPerson.size} count={sessionsByPerson.size}
/> />
<TabButton <TypeFilterDropdown
active={activeTab === 'swot'} activeTab={activeTab}
onClick={() => setActiveTab('swot')} setActiveTab={setActiveTab}
icon="📊" open={typeDropdownOpen}
label="SWOT" onOpenChange={setTypeDropdownOpen}
count={swotSessions.length} counts={{
/> swot: swotSessions.length,
<TabButton motivators: motivatorSessions.length,
active={activeTab === 'motivators'} 'year-review': yearReviewSessions.length,
onClick={() => setActiveTab('motivators')} 'weekly-checkin': weeklyCheckInSessions.length,
icon="🎯" weather: weatherSessions.length,
label="Moving Motivators" }}
count={motivatorSessions.length}
/>
<TabButton
active={activeTab === 'year-review'}
onClick={() => setActiveTab('year-review')}
icon="📅"
label="Year Review"
count={yearReviewSessions.length}
/>
<TabButton
active={activeTab === 'weekly-checkin'}
onClick={() => setActiveTab('weekly-checkin')}
icon="📝"
label="Weekly Check-in"
count={weeklyCheckInSessions.length}
/>
<TabButton
active={activeTab === 'weather'}
onClick={() => setActiveTab('weather')}
icon="🌤️"
label="Météo"
count={weatherSessions.length}
/> />
</div> </div>
@@ -367,6 +349,92 @@ export function WorkshopTabs({
); );
} }
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);
return (
<div className="relative">
<button
type="button"
onClick={() => onOpenChange(!open)}
onBlur={() => setTimeout(() => onOpenChange(false), 150)}
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({ function TabButton({
active, active,
onClick, onClick,
@@ -382,9 +450,10 @@ function TabButton({
}) { }) {
return ( return (
<button <button
type="button"
onClick={onClick} onClick={onClick}
className={` className={`
flex items-center gap-2 px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 px-3 py-2 rounded-lg font-medium text-sm transition-colors
${ ${
active active
? 'bg-primary text-primary-foreground' ? 'bg-primary text-primary-foreground'
@@ -394,7 +463,7 @@ function TabButton({
> >
<span>{icon}</span> <span>{icon}</span>
<span>{label}</span> <span>{label}</span>
<Badge variant={active ? 'default' : 'primary'} className="ml-1"> <Badge variant={active ? 'default' : 'primary'} className="ml-1 text-xs">
{count} {count}
</Badge> </Badge>
</button> </button>
@@ -418,20 +487,12 @@ function SessionCard({ session }: { session: AnySession }) {
: (session as MotivatorSession).participant : (session as MotivatorSession).participant
); );
const workshop = getWorkshop(session.workshopType as WorkshopTypeId);
const isSwot = session.workshopType === 'swot'; const isSwot = session.workshopType === 'swot';
const isYearReview = session.workshopType === 'year-review'; const isYearReview = session.workshopType === 'year-review';
const isWeeklyCheckIn = session.workshopType === 'weekly-checkin'; const isWeeklyCheckIn = session.workshopType === 'weekly-checkin';
const isWeather = session.workshopType === 'weather'; const isWeather = session.workshopType === 'weather';
const href = isSwot const href = getSessionPath(session.workshopType as WorkshopTypeId, session.id);
? `/sessions/${session.id}`
: isYearReview
? `/year-review/${session.id}`
: isWeeklyCheckIn
? `/weekly-checkin/${session.id}`
: isWeather
? `/weather/${session.id}`
: `/motivators/${session.id}`;
const icon = isSwot ? '📊' : isYearReview ? '📅' : isWeeklyCheckIn ? '📝' : isWeather ? '🌤️' : '🎯';
const participant = isSwot const participant = isSwot
? (session as SwotSession).collaborator ? (session as SwotSession).collaborator
: isYearReview : isYearReview
@@ -441,15 +502,7 @@ function SessionCard({ session }: { session: AnySession }) {
: isWeather : isWeather
? (session as WeatherSession).user.name || (session as WeatherSession).user.email ? (session as WeatherSession).user.name || (session as WeatherSession).user.email
: (session as MotivatorSession).participant; : (session as MotivatorSession).participant;
const accentColor = isSwot const accentColor = workshop.accentColor;
? '#06b6d4'
: isYearReview
? '#f59e0b'
: isWeeklyCheckIn
? '#10b981'
: isWeather
? '#3b82f6'
: '#8b5cf6';
const handleDelete = () => { const handleDelete = () => {
startTransition(async () => { startTransition(async () => {
@@ -507,7 +560,7 @@ function SessionCard({ session }: { session: AnySession }) {
setShowEditModal(true); setShowEditModal(true);
}; };
const editParticipantLabel = isSwot ? 'Collaborateur' : isWeather ? '' : 'Participant'; const editParticipantLabel = workshop.participantLabel;
return ( return (
<> <>
@@ -522,7 +575,7 @@ function SessionCard({ session }: { session: AnySession }) {
{/* Header: Icon + Title + Role badge */} {/* Header: Icon + Title + Role badge */}
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<span className="text-xl">{icon}</span> <span className="text-xl">{workshop.icon}</span>
<h3 className="font-semibold text-foreground line-clamp-1 flex-1">{session.title}</h3> <h3 className="font-semibold text-foreground line-clamp-1 flex-1">{session.title}</h3>
{!session.isOwner && ( {!session.isOwner && (
<span <span

View File

@@ -1,6 +1,7 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getSessionById } from '@/services/sessions'; import { getSessionById } from '@/services/sessions';
import { SwotBoard } from '@/components/swot/SwotBoard'; import { SwotBoard } from '@/components/swot/SwotBoard';
import { SessionLiveWrapper } from '@/components/collaboration'; import { SessionLiveWrapper } from '@/components/collaboration';
@@ -30,8 +31,8 @@ export default async function SessionPage({ params }: SessionPageProps) {
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2"> <div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href="/sessions?tab=swot" className="hover:text-foreground"> <Link href={getSessionsTabUrl('swot')} className="hover:text-foreground">
SWOT {getWorkshop('swot').labelShort}
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-foreground">{session.title}</span> <span className="text-foreground">{session.title}</span>

View File

@@ -1,13 +1,14 @@
import { Suspense } from 'react'; import { Suspense } from 'react';
import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getSessionsByUserId } from '@/services/sessions'; import { getSessionsByUserId } from '@/services/sessions';
import { getMotivatorSessionsByUserId } from '@/services/moving-motivators'; import { getMotivatorSessionsByUserId } from '@/services/moving-motivators';
import { getYearReviewSessionsByUserId } from '@/services/year-review'; import { getYearReviewSessionsByUserId } from '@/services/year-review';
import { getWeeklyCheckInSessionsByUserId } from '@/services/weekly-checkin'; import { getWeeklyCheckInSessionsByUserId } from '@/services/weekly-checkin';
import { getWeatherSessionsByUserId } from '@/services/weather'; import { getWeatherSessionsByUserId } from '@/services/weather';
import { Card, Button } from '@/components/ui'; import { Card } from '@/components/ui';
import { withWorkshopType } from '@/lib/workshops';
import { WorkshopTabs } from './WorkshopTabs'; import { WorkshopTabs } from './WorkshopTabs';
import { NewWorkshopDropdown } from './NewWorkshopDropdown';
function WorkshopTabsSkeleton() { function WorkshopTabsSkeleton() {
return ( return (
@@ -45,31 +46,12 @@ export default async function SessionsPage() {
getWeatherSessionsByUserId(session.user.id), getWeatherSessionsByUserId(session.user.id),
]); ]);
// Add type to each session for unified display // Add workshopType to each session for unified display
const allSwotSessions = swotSessions.map((s) => ({ const allSwotSessions = withWorkshopType(swotSessions, 'swot');
...s, const allMotivatorSessions = withWorkshopType(motivatorSessions, 'motivators');
workshopType: 'swot' as const, const allYearReviewSessions = withWorkshopType(yearReviewSessions, 'year-review');
})); const allWeeklyCheckInSessions = withWorkshopType(weeklyCheckInSessions, 'weekly-checkin');
const allWeatherSessions = withWorkshopType(weatherSessions, 'weather');
const allMotivatorSessions = motivatorSessions.map((s) => ({
...s,
workshopType: 'motivators' as const,
}));
const allYearReviewSessions = yearReviewSessions.map((s) => ({
...s,
workshopType: 'year-review' as const,
}));
const allWeeklyCheckInSessions = weeklyCheckInSessions.map((s) => ({
...s,
workshopType: 'weekly-checkin' as const,
}));
const allWeatherSessions = weatherSessions.map((s) => ({
...s,
workshopType: 'weather' as const,
}));
// Combine and sort by updatedAt // Combine and sort by updatedAt
const allSessions = [ const allSessions = [
@@ -90,38 +72,7 @@ export default async function SessionsPage() {
<h1 className="text-3xl font-bold text-foreground">Mes Ateliers</h1> <h1 className="text-3xl font-bold text-foreground">Mes Ateliers</h1>
<p className="mt-1 text-muted">Tous vos ateliers en un seul endroit</p> <p className="mt-1 text-muted">Tous vos ateliers en un seul endroit</p>
</div> </div>
<div className="flex gap-2"> <NewWorkshopDropdown />
<Link href="/sessions/new">
<Button variant="outline">
<span>📊</span>
Nouveau SWOT
</Button>
</Link>
<Link href="/motivators/new">
<Button variant="outline">
<span>🎯</span>
Nouveau Motivators
</Button>
</Link>
<Link href="/year-review/new">
<Button variant="outline">
<span>📅</span>
Nouveau Year Review
</Button>
</Link>
<Link href="/weekly-checkin/new">
<Button variant="outline">
<span>📝</span>
Nouveau Check-in
</Button>
</Link>
<Link href="/weather/new">
<Button>
<span>🌤</span>
Nouvelle Météo
</Button>
</Link>
</div>
</div> </div>
{/* Content */} {/* Content */}
@@ -136,37 +87,8 @@ export default async function SessionsPage() {
découvrir les motivations, un Year Review pour faire le bilan de l&apos;année, ou un découvrir les motivations, un Year Review pour faire le bilan de l&apos;année, ou un
Weekly Check-in pour le suivi hebdomadaire. Weekly Check-in pour le suivi hebdomadaire.
</p> </p>
<div className="flex gap-3 justify-center flex-wrap"> <div className="flex justify-center">
<Link href="/sessions/new"> <NewWorkshopDropdown />
<Button variant="outline">
<span>📊</span>
Créer un SWOT
</Button>
</Link>
<Link href="/motivators/new">
<Button variant="outline">
<span>🎯</span>
Créer un Moving Motivators
</Button>
</Link>
<Link href="/year-review/new">
<Button variant="outline">
<span>📅</span>
Créer un Year Review
</Button>
</Link>
<Link href="/weekly-checkin/new">
<Button variant="outline">
<span>📝</span>
Créer un Check-in
</Button>
</Link>
<Link href="/weather/new">
<Button>
<span>🌤</span>
Créer une Météo
</Button>
</Link>
</div> </div>
</Card> </Card>
) : ( ) : (

View File

@@ -1,6 +1,7 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getWeatherSessionById } from '@/services/weather'; import { getWeatherSessionById } from '@/services/weather';
import { getUserTeams } from '@/services/teams'; import { getUserTeams } from '@/services/teams';
import { WeatherBoard, WeatherLiveWrapper, WeatherInfoPanel } from '@/components/weather'; import { WeatherBoard, WeatherLiveWrapper, WeatherInfoPanel } from '@/components/weather';
@@ -33,8 +34,8 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2"> <div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href="/sessions?tab=weather" className="hover:text-foreground"> <Link href={getSessionsTabUrl('weather')} className="hover:text-foreground">
Météo {getWorkshop('weather').labelShort}
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-foreground">{session.title}</span> <span className="text-foreground">{session.title}</span>

View File

@@ -1,6 +1,7 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getWeeklyCheckInSessionById } from '@/services/weekly-checkin'; import { getWeeklyCheckInSessionById } from '@/services/weekly-checkin';
import { getUserOKRsForPeriod } from '@/services/okrs'; import { getUserOKRsForPeriod } from '@/services/okrs';
import { getCurrentQuarterPeriod } from '@/lib/okr-utils'; import { getCurrentQuarterPeriod } from '@/lib/okr-utils';
@@ -44,8 +45,8 @@ export default async function WeeklyCheckInSessionPage({ params }: WeeklyCheckIn
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2"> <div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href="/sessions?tab=weekly-checkin" className="hover:text-foreground"> <Link href={getSessionsTabUrl('weekly-checkin')} className="hover:text-foreground">
Weekly Check-in {getWorkshop('weekly-checkin').label}
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-foreground">{session.title}</span> <span className="text-foreground">{session.title}</span>

View File

@@ -1,6 +1,7 @@
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getWorkshop, getSessionsTabUrl } from '@/lib/workshops';
import { getYearReviewSessionById } from '@/services/year-review'; import { getYearReviewSessionById } from '@/services/year-review';
import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review'; import { YearReviewBoard, YearReviewLiveWrapper } from '@/components/year-review';
import { Badge, CollaboratorDisplay } from '@/components/ui'; import { Badge, CollaboratorDisplay } from '@/components/ui';
@@ -29,8 +30,8 @@ export default async function YearReviewSessionPage({ params }: YearReviewSessio
{/* Header */} {/* Header */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center gap-2 text-sm text-muted mb-2"> <div className="flex items-center gap-2 text-sm text-muted mb-2">
<Link href="/sessions?tab=year-review" className="hover:text-foreground"> <Link href={getSessionsTabUrl('year-review')} className="hover:text-foreground">
Year Review {getWorkshop('year-review').label}
</Link> </Link>
<span>/</span> <span>/</span>
<span className="text-foreground">{session.title}</span> <span className="text-foreground">{session.title}</span>

View File

@@ -6,6 +6,7 @@ import { useSession, signOut } from 'next-auth/react';
import { useTheme } from '@/contexts/ThemeContext'; import { useTheme } from '@/contexts/ThemeContext';
import { useState } from 'react'; import { useState } from 'react';
import { Avatar, RocketIcon } from '@/components/ui'; import { Avatar, RocketIcon } from '@/components/ui';
import { WORKSHOPS } from '@/lib/workshops';
export function Header() { export function Header() {
const { theme, toggleTheme } = useTheme(); const { theme, toggleTheme } = useTheme();
@@ -59,18 +60,18 @@ export function Header() {
👥 Équipes 👥 Équipes
</Link> </Link>
{/* Workshops Dropdown */} {/* New Workshop Dropdown */}
<div className="relative"> <div className="relative">
<button <button
onClick={() => setWorkshopsOpen(!workshopsOpen)} onClick={() => setWorkshopsOpen(!workshopsOpen)}
onBlur={() => setTimeout(() => setWorkshopsOpen(false), 150)} onBlur={() => setTimeout(() => setWorkshopsOpen(false), 150)}
className={`flex items-center gap-1 text-sm font-medium transition-colors ${ className={`flex items-center gap-1 text-sm font-medium transition-colors ${
isActiveLink('/sessions/') || isActiveLink('/motivators') WORKSHOPS.some((w) => isActiveLink(w.path))
? 'text-primary' ? 'text-primary'
: 'text-muted hover:text-foreground' : 'text-muted hover:text-foreground'
}`} }`}
> >
Ateliers Nouvel atelier
<svg <svg
className={`h-4 w-4 transition-transform ${workshopsOpen ? 'rotate-180' : ''}`} className={`h-4 w-4 transition-transform ${workshopsOpen ? 'rotate-180' : ''}`}
fill="none" fill="none"
@@ -88,28 +89,20 @@ export function Header() {
{workshopsOpen && ( {workshopsOpen && (
<div className="absolute left-0 z-20 mt-2 w-56 rounded-lg border border-border bg-card py-1 shadow-lg"> <div className="absolute left-0 z-20 mt-2 w-56 rounded-lg border border-border bg-card py-1 shadow-lg">
<Link {WORKSHOPS.map((w) => (
href="/sessions/new" <Link
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover" key={w.id}
onClick={() => setWorkshopsOpen(false)} href={w.newPath}
> className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover"
<span className="text-lg">📊</span> onClick={() => setWorkshopsOpen(false)}
<div> >
<div className="font-medium">Analyse SWOT</div> <span className="text-lg">{w.icon}</span>
<div className="text-xs text-muted">Forces, faiblesses, opportunités</div> <div>
</div> <div className="font-medium">{w.label}</div>
</Link> <div className="text-xs text-muted">{w.description}</div>
<Link </div>
href="/motivators/new" </Link>
className="flex items-center gap-3 px-4 py-2.5 text-sm text-foreground hover:bg-card-hover" ))}
onClick={() => setWorkshopsOpen(false)}
>
<span className="text-lg">🎯</span>
<div>
<div className="font-medium">Moving Motivators</div>
<div className="text-xs text-muted">Motivations intrinsèques</div>
</div>
</Link>
</div> </div>
)} )}
</div> </div>

185
src/lib/workshops.ts Normal file
View File

@@ -0,0 +1,185 @@
/**
* Single source of truth for workshop types and metadata.
* Used by: Header, NewWorkshopDropdown, WorkshopTabs, sessions page.
*/
export const WORKSHOP_TYPE_IDS = [
'swot',
'motivators',
'year-review',
'weekly-checkin',
'weather',
] as const;
export type WorkshopTypeId = (typeof WORKSHOP_TYPE_IDS)[number];
export type WorkshopTabType = WorkshopTypeId | 'all' | 'byPerson';
export const VALID_TAB_PARAMS: WorkshopTabType[] = [
'all',
...WORKSHOP_TYPE_IDS,
'byPerson',
];
export interface WorkshopConfig {
id: WorkshopTypeId;
icon: string;
label: string;
labelShort: string;
cardLabel: string; // For home page cards (e.g. "Météo" vs "Météo d'équipe")
description: string;
path: string; // e.g. /sessions, /motivators
newPath: string; // e.g. /sessions/new
accentColor: string;
hasParticipant: boolean; // false for weather
participantLabel: string; // 'Collaborateur' | 'Participant' | ''
/** Home page marketing content */
home: {
tagline: string;
description: string;
features: string[];
};
}
export const WORKSHOPS: WorkshopConfig[] = [
{
id: 'swot',
icon: '📊',
label: 'Analyse SWOT',
labelShort: 'SWOT',
cardLabel: 'Analyse SWOT',
description: 'Forces, faiblesses, opportunités',
path: '/sessions',
newPath: '/sessions/new',
accentColor: '#06b6d4',
hasParticipant: true,
participantLabel: 'Collaborateur',
home: {
tagline: 'Analysez. Planifiez. Progressez.',
description:
"Cartographiez les forces et faiblesses de vos collaborateurs. Identifiez opportunités et menaces pour définir des actions concrètes.",
features: [
'Matrice interactive Forces/Faiblesses/Opportunités/Menaces',
'Actions croisées et plan de développement',
'Collaboration en temps réel',
],
},
},
{
id: 'motivators',
icon: '🎯',
label: 'Moving Motivators',
labelShort: 'Motivators',
cardLabel: 'Moving Motivators',
description: 'Motivations intrinsèques',
path: '/motivators',
newPath: '/motivators/new',
accentColor: '#8b5cf6',
hasParticipant: true,
participantLabel: 'Participant',
home: {
tagline: 'Révélez ce qui motive vraiment',
description:
"Explorez les 10 motivations intrinsèques de vos collaborateurs. Comprenez leur impact et alignez aspirations et missions.",
features: [
'10 cartes de motivation à classer',
"Évaluation de l'influence positive/négative",
'Récapitulatif personnalisé des motivations',
],
},
},
{
id: 'year-review',
icon: '📅',
label: 'Year Review',
labelShort: 'Year Review',
cardLabel: 'Year Review',
description: "Bilan de l'année",
path: '/year-review',
newPath: '/year-review/new',
accentColor: '#f59e0b',
hasParticipant: true,
participantLabel: 'Participant',
home: {
tagline: "Faites le bilan de l'année",
description:
"Réalisez un bilan complet de l'année écoulée. Identifiez réalisations, défis, apprentissages et définissez vos objectifs pour l'année à venir.",
features: [
"5 catégories : Réalisations, Défis, Apprentissages, Objectifs, Moments",
'Organisation par drag & drop',
"Vue d'ensemble de l'année",
],
},
},
{
id: 'weekly-checkin',
icon: '📝',
label: 'Weekly Check-in',
labelShort: 'Check-in',
cardLabel: 'Weekly Check-in',
description: 'Suivi hebdomadaire',
path: '/weekly-checkin',
newPath: '/weekly-checkin/new',
accentColor: '#10b981',
hasParticipant: true,
participantLabel: 'Participant',
home: {
tagline: 'Le point hebdomadaire avec vos collaborateurs',
description:
"Chaque semaine, faites le point avec vos collaborateurs sur ce qui s'est bien passé, ce qui s'est mal passé, les enjeux du moment et les prochains enjeux.",
features: [
"4 catégories : Bien passé, Mal passé, Enjeux du moment, Prochains enjeux",
"Ajout d'émotions à chaque item (fierté, joie, frustration, etc.)",
'Suivi hebdomadaire régulier',
],
},
},
{
id: 'weather',
icon: '🌤️',
label: "Météo d'équipe",
labelShort: 'Météo',
cardLabel: 'Météo',
description: 'Humeur et énergie',
path: '/weather',
newPath: '/weather/new',
accentColor: '#3b82f6',
hasParticipant: false,
participantLabel: '',
home: {
tagline: "Votre état en un coup d'œil",
description:
"Créez votre météo personnelle sur 4 axes clés (Performance, Moral, Flux, Création de valeur) et partagez-la avec votre équipe pour une meilleure visibilité de votre état.",
features: [
'4 axes : Performance, Moral, Flux, Création de valeur',
'Emojis météo pour exprimer votre état visuellement',
'Notes globales pour détailler votre ressenti',
],
},
},
];
export const WORKSHOP_BY_ID = Object.fromEntries(WORKSHOPS.map((w) => [w.id, w])) as Record<
WorkshopTypeId,
WorkshopConfig
>;
export function getWorkshop(id: WorkshopTypeId): WorkshopConfig {
return WORKSHOP_BY_ID[id];
}
export function getSessionPath(id: WorkshopTypeId, sessionId: string): string {
return `${getWorkshop(id).path}/${sessionId}`;
}
export function getSessionsTabUrl(id: WorkshopTypeId): string {
return `/sessions?tab=${id}`;
}
/** Add workshopType to session objects for unified display. Preserves literal type for type safety. */
export function withWorkshopType<T, K extends WorkshopTypeId>(
sessions: T[],
type: K
): (T & { workshopType: K })[] {
return sessions.map((s) => ({ ...s, workshopType: type })) as (T & { workshopType: K })[];
}