Persist sessions view mode in localStorage
This commit is contained in:
@@ -1,16 +1,24 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useRef } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { useSearchParams, useRouter } from 'next/navigation';
|
import { useSearchParams, useRouter } from 'next/navigation';
|
||||||
import { CollaboratorDisplay } from '@/components/ui';
|
import { CollaboratorDisplay } from '@/components/ui';
|
||||||
import { type WorkshopTabType, WORKSHOPS, VALID_TAB_PARAMS } from '@/lib/workshops';
|
import { type WorkshopTabType, VALID_TAB_PARAMS } from '@/lib/workshops';
|
||||||
import { useClickOutside } from '@/hooks/useClickOutside';
|
import { useClickOutside } from '@/hooks/useClickOutside';
|
||||||
import {
|
import {
|
||||||
type CardView, type SortCol, type WorkshopTabsProps, type AnySession,
|
type CardView,
|
||||||
TABLE_COLS, SORT_COLUMNS, TYPE_TABS,
|
type SortCol,
|
||||||
|
type WorkshopTabsProps,
|
||||||
|
type AnySession,
|
||||||
|
TABLE_COLS,
|
||||||
|
SORT_COLUMNS,
|
||||||
|
TYPE_TABS,
|
||||||
} from './workshop-session-types';
|
} from './workshop-session-types';
|
||||||
import {
|
import {
|
||||||
getResolvedCollaborator, groupByPerson, getMonthGroup, sortSessions,
|
getResolvedCollaborator,
|
||||||
|
groupByPerson,
|
||||||
|
getMonthGroup,
|
||||||
|
sortSessions,
|
||||||
} from './workshop-session-helpers';
|
} from './workshop-session-helpers';
|
||||||
import { SessionCard } from './SessionCard';
|
import { SessionCard } from './SessionCard';
|
||||||
|
|
||||||
@@ -33,7 +41,13 @@ function SectionHeader({ label, count }: { label: string; count: number }) {
|
|||||||
function SortIcon({ active, dir }: { active: boolean; dir: 'asc' | 'desc' }) {
|
function SortIcon({ active, dir }: { active: boolean; dir: 'asc' | 'desc' }) {
|
||||||
if (!active) {
|
if (!active) {
|
||||||
return (
|
return (
|
||||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="currentColor" className="opacity-30 flex-shrink-0">
|
<svg
|
||||||
|
width="10"
|
||||||
|
height="10"
|
||||||
|
viewBox="0 0 10 10"
|
||||||
|
fill="currentColor"
|
||||||
|
className="opacity-30 flex-shrink-0"
|
||||||
|
>
|
||||||
<path d="M5 1.5L8 5H2L5 1.5Z" />
|
<path d="M5 1.5L8 5H2L5 1.5Z" />
|
||||||
<path d="M5 8.5L2 5H8L5 8.5Z" />
|
<path d="M5 8.5L2 5H8L5 8.5Z" />
|
||||||
</svg>
|
</svg>
|
||||||
@@ -72,27 +86,61 @@ function ViewToggle({ view, setView }: { view: CardView; setView: (v: CardView)
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-0.5 p-0.5 bg-card border border-border rounded-lg ml-auto flex-shrink-0 shadow-sm">
|
<div className="flex items-center gap-0.5 p-0.5 bg-card border border-border rounded-lg ml-auto flex-shrink-0 shadow-sm">
|
||||||
{btn('grid', 'Grille',
|
{btn(
|
||||||
|
'grid',
|
||||||
|
'Grille',
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
||||||
<rect x="0" y="0" width="4" height="4" rx="0.5" /><rect x="5" y="0" width="4" height="4" rx="0.5" /><rect x="10" y="0" width="4" height="4" rx="0.5" />
|
<rect x="0" y="0" width="4" height="4" rx="0.5" />
|
||||||
<rect x="0" y="5" width="4" height="4" rx="0.5" /><rect x="5" y="5" width="4" height="4" rx="0.5" /><rect x="10" y="5" width="4" height="4" rx="0.5" />
|
<rect x="5" y="0" width="4" height="4" rx="0.5" />
|
||||||
<rect x="0" y="10" width="4" height="4" rx="0.5" /><rect x="5" y="10" width="4" height="4" rx="0.5" /><rect x="10" y="10" width="4" height="4" rx="0.5" />
|
<rect x="10" y="0" width="4" height="4" rx="0.5" />
|
||||||
|
<rect x="0" y="5" width="4" height="4" rx="0.5" />
|
||||||
|
<rect x="5" y="5" width="4" height="4" rx="0.5" />
|
||||||
|
<rect x="10" y="5" width="4" height="4" rx="0.5" />
|
||||||
|
<rect x="0" y="10" width="4" height="4" rx="0.5" />
|
||||||
|
<rect x="5" y="10" width="4" height="4" rx="0.5" />
|
||||||
|
<rect x="10" y="10" width="4" height="4" rx="0.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{btn('list', 'Liste',
|
{btn(
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
'list',
|
||||||
<line x1="0" y1="2.5" x2="14" y2="2.5" /><line x1="0" y1="7" x2="14" y2="7" /><line x1="0" y1="11.5" x2="14" y2="11.5" />
|
'Liste',
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
>
|
||||||
|
<line x1="0" y1="2.5" x2="14" y2="2.5" />
|
||||||
|
<line x1="0" y1="7" x2="14" y2="7" />
|
||||||
|
<line x1="0" y1="11.5" x2="14" y2="11.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{btn('table', 'Tableau',
|
{btn(
|
||||||
|
'table',
|
||||||
|
'Tableau',
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
||||||
<rect x="0" y="0" width="14" height="3.5" rx="0.5" />
|
<rect x="0" y="0" width="14" height="3.5" rx="0.5" />
|
||||||
<rect x="0" y="5" width="6" height="2.5" rx="0.5" /><rect x="8" y="5" width="6" height="2.5" rx="0.5" />
|
<rect x="0" y="5" width="6" height="2.5" rx="0.5" />
|
||||||
<rect x="0" y="9.5" width="6" height="2.5" rx="0.5" /><rect x="8" y="9.5" width="6" height="2.5" rx="0.5" />
|
<rect x="8" y="5" width="6" height="2.5" rx="0.5" />
|
||||||
|
<rect x="0" y="9.5" width="6" height="2.5" rx="0.5" />
|
||||||
|
<rect x="8" y="9.5" width="6" height="2.5" rx="0.5" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{btn('timeline', 'Chronologique',
|
{btn(
|
||||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round">
|
'timeline',
|
||||||
|
'Chronologique',
|
||||||
|
<svg
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 14 14"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="1.5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
>
|
||||||
<line x1="3" y1="0" x2="3" y2="14" />
|
<line x1="3" y1="0" x2="3" y2="14" />
|
||||||
<circle cx="3" cy="2.5" r="1.5" fill="currentColor" stroke="none" />
|
<circle cx="3" cy="2.5" r="1.5" fill="currentColor" stroke="none" />
|
||||||
<line x1="5" y1="2.5" x2="14" y2="2.5" />
|
<line x1="5" y1="2.5" x2="14" y2="2.5" />
|
||||||
@@ -108,11 +156,23 @@ function ViewToggle({ view, setView }: { view: CardView; setView: (v: CardView)
|
|||||||
|
|
||||||
// ─── TabButton ────────────────────────────────────────────────────────────────
|
// ─── TabButton ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function TabButton({ active, onClick, icon, label, count }: {
|
function TabButton({
|
||||||
active: boolean; onClick: () => void; icon: string; label: string; count: number;
|
active,
|
||||||
|
onClick,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
count,
|
||||||
|
}: {
|
||||||
|
active: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
icon: string;
|
||||||
|
label: string;
|
||||||
|
count: number;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<button type="button" onClick={onClick}
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-full font-medium text-sm transition-all duration-150 shadow-sm ${
|
className={`flex items-center gap-1.5 px-3.5 py-1.5 rounded-full font-medium text-sm transition-all duration-150 shadow-sm ${
|
||||||
active
|
active
|
||||||
? 'bg-primary text-primary-foreground shadow-md'
|
? 'bg-primary text-primary-foreground shadow-md'
|
||||||
@@ -121,7 +181,9 @@ function TabButton({ active, onClick, icon, label, count }: {
|
|||||||
>
|
>
|
||||||
<span>{icon}</span>
|
<span>{icon}</span>
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
<span className={`text-[11px] font-semibold px-1.5 py-0.5 rounded-full ${active ? 'bg-white/20 text-white' : 'bg-primary/10 text-primary'}`}>
|
<span
|
||||||
|
className={`text-[11px] font-semibold px-1.5 py-0.5 rounded-full ${active ? 'bg-white/20 text-white' : 'bg-primary/10 text-primary'}`}
|
||||||
|
>
|
||||||
{count}
|
{count}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -131,10 +193,17 @@ function TabButton({ active, onClick, icon, label, count }: {
|
|||||||
// ─── TypeFilterDropdown ───────────────────────────────────────────────────────
|
// ─── TypeFilterDropdown ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
function TypeFilterDropdown({
|
function TypeFilterDropdown({
|
||||||
activeTab, setActiveTab, open, onOpenChange, counts,
|
activeTab,
|
||||||
|
setActiveTab,
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
counts,
|
||||||
}: {
|
}: {
|
||||||
activeTab: WorkshopTabType; setActiveTab: (t: WorkshopTabType) => void;
|
activeTab: WorkshopTabType;
|
||||||
open: boolean; onOpenChange: (v: boolean) => void; counts: Record<string, number>;
|
setActiveTab: (t: WorkshopTabType) => void;
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (v: boolean) => void;
|
||||||
|
counts: Record<string, number>;
|
||||||
}) {
|
}) {
|
||||||
const typeTabs = TYPE_TABS.filter((t) => t.value !== 'all' && t.value !== 'team');
|
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 current = TYPE_TABS.find((t) => t.value === activeTab) ?? TYPE_TABS[0];
|
||||||
@@ -156,25 +225,55 @@ function TypeFilterDropdown({
|
|||||||
>
|
>
|
||||||
<span>{isTypeSelected ? current.icon : '🔖'}</span>
|
<span>{isTypeSelected ? current.icon : '🔖'}</span>
|
||||||
<span>{isTypeSelected ? current.label : 'Type'}</span>
|
<span>{isTypeSelected ? current.label : 'Type'}</span>
|
||||||
<span className={`text-[11px] font-semibold px-1.5 py-0.5 rounded-full ${isTypeSelected ? 'bg-white/20 text-white' : 'bg-primary/10 text-primary'}`}>
|
<span
|
||||||
|
className={`text-[11px] font-semibold px-1.5 py-0.5 rounded-full ${isTypeSelected ? 'bg-white/20 text-white' : 'bg-primary/10 text-primary'}`}
|
||||||
|
>
|
||||||
{isTypeSelected ? (counts[activeTab] ?? 0) : totalCount}
|
{isTypeSelected ? (counts[activeTab] ?? 0) : totalCount}
|
||||||
</span>
|
</span>
|
||||||
<svg className={`h-3.5 w-3.5 transition-transform ${open ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
|
className={`h-3.5 w-3.5 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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{open && (
|
{open && (
|
||||||
<div className="absolute left-0 z-20 mt-2 w-48 rounded-xl border border-border bg-card py-1.5 shadow-lg">
|
<div className="absolute left-0 z-20 mt-2 w-48 rounded-xl border border-border bg-card py-1.5 shadow-lg">
|
||||||
<button type="button" onClick={() => { setActiveTab('all'); onOpenChange(false); }}
|
<button
|
||||||
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 transition-colors">
|
type="button"
|
||||||
<span className="flex items-center gap-2"><span>📋</span><span>Tous les types</span></span>
|
onClick={() => {
|
||||||
<span className="text-[11px] font-semibold px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">{totalCount}</span>
|
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 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span>📋</span>
|
||||||
|
<span>Tous les types</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] font-semibold px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
|
||||||
|
{totalCount}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{typeTabs.map((t) => (
|
{typeTabs.map((t) => (
|
||||||
<button key={t.value} type="button" onClick={() => { setActiveTab(t.value); onOpenChange(false); }}
|
<button
|
||||||
className="flex w-full items-center justify-between gap-2 px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover transition-colors">
|
key={t.value}
|
||||||
<span className="flex items-center gap-2"><span>{t.icon}</span><span>{t.label}</span></span>
|
type="button"
|
||||||
<span className="text-[11px] font-semibold px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">{counts[t.value] ?? 0}</span>
|
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 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<span>{t.icon}</span>
|
||||||
|
<span>{t.label}</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] font-semibold px-1.5 py-0.5 rounded-full bg-primary/10 text-primary">
|
||||||
|
{counts[t.value] ?? 0}
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -186,16 +285,25 @@ function TypeFilterDropdown({
|
|||||||
// ─── SessionsGrid ─────────────────────────────────────────────────────────────
|
// ─── SessionsGrid ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SessionsGrid({
|
function SessionsGrid({
|
||||||
sessions, view, isTeamCollab = false,
|
sessions,
|
||||||
|
view,
|
||||||
|
isTeamCollab = false,
|
||||||
}: {
|
}: {
|
||||||
sessions: AnySession[]; view: CardView; isTeamCollab?: boolean;
|
sessions: AnySession[];
|
||||||
|
view: CardView;
|
||||||
|
isTeamCollab?: boolean;
|
||||||
}) {
|
}) {
|
||||||
if (view === 'table') {
|
if (view === 'table') {
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border overflow-hidden overflow-x-auto bg-card">
|
<div className="rounded-xl border border-border overflow-hidden overflow-x-auto bg-card">
|
||||||
<div className="grid text-[11px] font-semibold text-muted uppercase tracking-wider bg-card-hover/60 border-b border-border" style={{ gridTemplateColumns: TABLE_COLS }}>
|
<div
|
||||||
|
className="grid text-[11px] font-semibold text-muted uppercase tracking-wider bg-card-hover/60 border-b border-border"
|
||||||
|
style={{ gridTemplateColumns: TABLE_COLS }}
|
||||||
|
>
|
||||||
{SORT_COLUMNS.map((col) => (
|
{SORT_COLUMNS.map((col) => (
|
||||||
<div key={col.key} className="px-4 py-2.5">{col.label}</div>
|
<div key={col.key} className="px-4 py-2.5">
|
||||||
|
{col.label}
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{sessions.map((s) => (
|
{sessions.map((s) => (
|
||||||
@@ -205,7 +313,11 @@ function SessionsGrid({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={view === 'list' ? 'flex flex-col gap-2' : 'grid gap-4 md:grid-cols-2 lg:grid-cols-3'}>
|
<div
|
||||||
|
className={
|
||||||
|
view === 'list' ? 'flex flex-col gap-2' : 'grid gap-4 md:grid-cols-2 lg:grid-cols-3'
|
||||||
|
}
|
||||||
|
>
|
||||||
{sessions.map((s) => (
|
{sessions.map((s) => (
|
||||||
<SessionCard key={s.id} session={s} isTeamCollab={isTeamCollab} view={view} />
|
<SessionCard key={s.id} session={s} isTeamCollab={isTeamCollab} view={view} />
|
||||||
))}
|
))}
|
||||||
@@ -216,7 +328,10 @@ function SessionsGrid({
|
|||||||
// ─── SortableTableView ────────────────────────────────────────────────────────
|
// ─── SortableTableView ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function SortableTableView({
|
function SortableTableView({
|
||||||
sessions, sortCol, sortDir, onSort,
|
sessions,
|
||||||
|
sortCol,
|
||||||
|
sortDir,
|
||||||
|
onSort,
|
||||||
}: {
|
}: {
|
||||||
sessions: AnySession[];
|
sessions: AnySession[];
|
||||||
sortCol: SortCol;
|
sortCol: SortCol;
|
||||||
@@ -228,7 +343,10 @@ function SortableTableView({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className="rounded-xl border border-border overflow-hidden overflow-x-auto bg-card">
|
<div className="rounded-xl border border-border overflow-hidden overflow-x-auto bg-card">
|
||||||
<div className="grid bg-card-hover/60 border-b border-border" style={{ gridTemplateColumns: TABLE_COLS }}>
|
<div
|
||||||
|
className="grid bg-card-hover/60 border-b border-border"
|
||||||
|
style={{ gridTemplateColumns: TABLE_COLS }}
|
||||||
|
>
|
||||||
{SORT_COLUMNS.map((col) => (
|
{SORT_COLUMNS.map((col) => (
|
||||||
<button
|
<button
|
||||||
key={col.key}
|
key={col.key}
|
||||||
@@ -258,20 +376,39 @@ function SortableTableView({
|
|||||||
// ─── WorkshopTabs ─────────────────────────────────────────────────────────────
|
// ─── WorkshopTabs ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function WorkshopTabs({
|
export function WorkshopTabs({
|
||||||
swotSessions, motivatorSessions, yearReviewSessions,
|
swotSessions,
|
||||||
weeklyCheckInSessions, weatherSessions, gifMoodSessions,
|
motivatorSessions,
|
||||||
|
yearReviewSessions,
|
||||||
|
weeklyCheckInSessions,
|
||||||
|
weatherSessions,
|
||||||
|
gifMoodSessions,
|
||||||
teamCollabSessions = [],
|
teamCollabSessions = [],
|
||||||
}: WorkshopTabsProps) {
|
}: WorkshopTabsProps) {
|
||||||
|
const CARD_VIEW_STORAGE_KEY = 'sessions:cardView';
|
||||||
|
const isCardView = (value: string): value is CardView =>
|
||||||
|
value === 'grid' || value === 'list' || value === 'table' || value === 'timeline';
|
||||||
|
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
|
const [typeDropdownOpen, setTypeDropdownOpen] = useState(false);
|
||||||
const [cardView, setCardView] = useState<CardView>('grid');
|
const [cardView, setCardView] = useState<CardView>(() => {
|
||||||
|
if (typeof window === 'undefined') return 'grid';
|
||||||
|
const storedView = localStorage.getItem(CARD_VIEW_STORAGE_KEY);
|
||||||
|
return storedView && isCardView(storedView) ? storedView : 'grid';
|
||||||
|
});
|
||||||
const [sortCol, setSortCol] = useState<SortCol>('date');
|
const [sortCol, setSortCol] = useState<SortCol>('date');
|
||||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(CARD_VIEW_STORAGE_KEY, cardView);
|
||||||
|
}, [cardView]);
|
||||||
|
|
||||||
const handleSort = (col: SortCol) => {
|
const handleSort = (col: SortCol) => {
|
||||||
if (sortCol === col) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
if (sortCol === col) setSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
||||||
else { setSortCol(col); setSortDir('asc'); }
|
else {
|
||||||
|
setSortCol(col);
|
||||||
|
setSortDir('asc');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tabParam = searchParams.get('tab');
|
const tabParam = searchParams.get('tab');
|
||||||
@@ -288,19 +425,30 @@ export function WorkshopTabs({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const allSessions: AnySession[] = [
|
const allSessions: AnySession[] = [
|
||||||
...swotSessions, ...motivatorSessions, ...yearReviewSessions,
|
...swotSessions,
|
||||||
...weeklyCheckInSessions, ...weatherSessions, ...gifMoodSessions,
|
...motivatorSessions,
|
||||||
|
...yearReviewSessions,
|
||||||
|
...weeklyCheckInSessions,
|
||||||
|
...weatherSessions,
|
||||||
|
...gifMoodSessions,
|
||||||
].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||||
|
|
||||||
const filteredSessions: AnySession[] =
|
const filteredSessions: AnySession[] =
|
||||||
activeTab === 'all' || activeTab === 'byPerson' ? allSessions
|
activeTab === 'all' || activeTab === 'byPerson'
|
||||||
: activeTab === 'team' ? teamCollabSessions
|
? allSessions
|
||||||
: activeTab === 'swot' ? swotSessions
|
: activeTab === 'team'
|
||||||
: activeTab === 'motivators' ? motivatorSessions
|
? teamCollabSessions
|
||||||
: activeTab === 'year-review' ? yearReviewSessions
|
: activeTab === 'swot'
|
||||||
: activeTab === 'weekly-checkin' ? weeklyCheckInSessions
|
? swotSessions
|
||||||
: activeTab === 'gif-mood' ? gifMoodSessions
|
: activeTab === 'motivators'
|
||||||
: weatherSessions;
|
? motivatorSessions
|
||||||
|
: activeTab === 'year-review'
|
||||||
|
? yearReviewSessions
|
||||||
|
: activeTab === 'weekly-checkin'
|
||||||
|
? weeklyCheckInSessions
|
||||||
|
: activeTab === 'gif-mood'
|
||||||
|
? gifMoodSessions
|
||||||
|
: weatherSessions;
|
||||||
|
|
||||||
const ownedSessions = filteredSessions.filter((s) => s.isOwner);
|
const ownedSessions = filteredSessions.filter((s) => s.isOwner);
|
||||||
const sharedSessions = filteredSessions.filter(
|
const sharedSessions = filteredSessions.filter(
|
||||||
@@ -309,11 +457,14 @@ export function WorkshopTabs({
|
|||||||
const teamCollabFiltered = activeTab === 'all' ? teamCollabSessions : [];
|
const teamCollabFiltered = activeTab === 'all' ? teamCollabSessions : [];
|
||||||
|
|
||||||
const sessionsByPerson = groupByPerson(allSessions);
|
const sessionsByPerson = groupByPerson(allSessions);
|
||||||
const sortedPersons = Array.from(sessionsByPerson.entries()).sort((a, b) => a[0].localeCompare(b[0], 'fr'));
|
const sortedPersons = Array.from(sessionsByPerson.entries()).sort((a, b) =>
|
||||||
|
a[0].localeCompare(b[0], 'fr')
|
||||||
|
);
|
||||||
|
|
||||||
// Timeline grouping
|
// Timeline grouping
|
||||||
const timelineSessions = [...(activeTab === 'all' ? [...filteredSessions, ...teamCollabSessions] : filteredSessions)]
|
const timelineSessions = [
|
||||||
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
...(activeTab === 'all' ? [...filteredSessions, ...teamCollabSessions] : filteredSessions),
|
||||||
|
].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
|
||||||
const byMonth = new Map<string, AnySession[]>();
|
const byMonth = new Map<string, AnySession[]>();
|
||||||
timelineSessions.forEach((s) => {
|
timelineSessions.forEach((s) => {
|
||||||
const key = getMonthGroup(s.updatedAt);
|
const key = getMonthGroup(s.updatedAt);
|
||||||
@@ -326,7 +477,8 @@ export function WorkshopTabs({
|
|||||||
cardView === 'table' && activeTab !== 'byPerson'
|
cardView === 'table' && activeTab !== 'byPerson'
|
||||||
? sortSessions(
|
? sortSessions(
|
||||||
activeTab === 'all' ? [...filteredSessions, ...teamCollabSessions] : filteredSessions,
|
activeTab === 'all' ? [...filteredSessions, ...teamCollabSessions] : filteredSessions,
|
||||||
sortCol, sortDir,
|
sortCol,
|
||||||
|
sortDir
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@@ -334,19 +486,42 @@ export function WorkshopTabs({
|
|||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{/* Tabs + vue toggle */}
|
{/* Tabs + vue toggle */}
|
||||||
<div className="flex gap-1.5 items-center flex-wrap">
|
<div className="flex gap-1.5 items-center flex-wrap">
|
||||||
<TabButton active={activeTab === 'all'} onClick={() => setActiveTab('all')} icon="📋" label="Tous" count={allSessions.length} />
|
<TabButton
|
||||||
<TabButton active={activeTab === 'byPerson'} onClick={() => setActiveTab('byPerson')} icon="👥" label="Par personne" count={sessionsByPerson.size} />
|
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 && (
|
{teamCollabSessions.length > 0 && (
|
||||||
<TabButton active={activeTab === 'team'} onClick={() => setActiveTab('team')} icon="🏢" label="Équipe" count={teamCollabSessions.length} />
|
<TabButton
|
||||||
|
active={activeTab === 'team'}
|
||||||
|
onClick={() => setActiveTab('team')}
|
||||||
|
icon="🏢"
|
||||||
|
label="Équipe"
|
||||||
|
count={teamCollabSessions.length}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="h-5 w-px bg-border mx-0.5 self-center" />
|
<div className="h-5 w-px bg-border mx-0.5 self-center" />
|
||||||
<TypeFilterDropdown
|
<TypeFilterDropdown
|
||||||
activeTab={activeTab} setActiveTab={setActiveTab}
|
activeTab={activeTab}
|
||||||
open={typeDropdownOpen} onOpenChange={setTypeDropdownOpen}
|
setActiveTab={setActiveTab}
|
||||||
|
open={typeDropdownOpen}
|
||||||
|
onOpenChange={setTypeDropdownOpen}
|
||||||
counts={{
|
counts={{
|
||||||
swot: swotSessions.length, motivators: motivatorSessions.length,
|
swot: swotSessions.length,
|
||||||
'year-review': yearReviewSessions.length, 'weekly-checkin': weeklyCheckInSessions.length,
|
motivators: motivatorSessions.length,
|
||||||
weather: weatherSessions.length, 'gif-mood': gifMoodSessions.length,
|
'year-review': yearReviewSessions.length,
|
||||||
|
'weekly-checkin': weeklyCheckInSessions.length,
|
||||||
|
weather: weatherSessions.length,
|
||||||
|
'gif-mood': gifMoodSessions.length,
|
||||||
team: teamCollabSessions.length,
|
team: teamCollabSessions.length,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -355,8 +530,12 @@ export function WorkshopTabs({
|
|||||||
|
|
||||||
{/* ── Vue Tableau flat (colonnes triables) ──────────────────── */}
|
{/* ── Vue Tableau flat (colonnes triables) ──────────────────── */}
|
||||||
{cardView === 'table' && activeTab !== 'byPerson' ? (
|
{cardView === 'table' && activeTab !== 'byPerson' ? (
|
||||||
<SortableTableView sessions={flatTableSessions} sortCol={sortCol} sortDir={sortDir} onSort={handleSort} />
|
<SortableTableView
|
||||||
|
sessions={flatTableSessions}
|
||||||
|
sortCol={sortCol}
|
||||||
|
sortDir={sortDir}
|
||||||
|
onSort={handleSort}
|
||||||
|
/>
|
||||||
) : cardView === 'timeline' && activeTab !== 'byPerson' ? (
|
) : cardView === 'timeline' && activeTab !== 'byPerson' ? (
|
||||||
/* ── Vue Timeline ────────────────────────────────────────── */
|
/* ── Vue Timeline ────────────────────────────────────────── */
|
||||||
byMonth.size === 0 ? (
|
byMonth.size === 0 ? (
|
||||||
@@ -367,19 +546,25 @@ export function WorkshopTabs({
|
|||||||
<section key={period}>
|
<section key={period}>
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<div className="h-px flex-1 bg-border" />
|
<div className="h-px flex-1 bg-border" />
|
||||||
<span className="text-xs font-semibold text-muted uppercase tracking-widest px-2 capitalize">{period}</span>
|
<span className="text-xs font-semibold text-muted uppercase tracking-widest px-2 capitalize">
|
||||||
|
{period}
|
||||||
|
</span>
|
||||||
<div className="h-px flex-1 bg-border" />
|
<div className="h-px flex-1 bg-border" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{sessions.map((s) => (
|
{sessions.map((s) => (
|
||||||
<SessionCard key={s.id} session={s} isTeamCollab={(s as AnySession & { isTeamCollab?: boolean }).isTeamCollab} view="list" />
|
<SessionCard
|
||||||
|
key={s.id}
|
||||||
|
session={s}
|
||||||
|
isTeamCollab={(s as AnySession & { isTeamCollab?: boolean }).isTeamCollab}
|
||||||
|
view="list"
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
) : activeTab === 'byPerson' ? (
|
) : activeTab === 'byPerson' ? (
|
||||||
/* ── Vue Par personne ───────────────────────────────────── */
|
/* ── Vue Par personne ───────────────────────────────────── */
|
||||||
sortedPersons.length === 0 ? (
|
sortedPersons.length === 0 ? (
|
||||||
@@ -396,21 +581,28 @@ export function WorkshopTabs({
|
|||||||
{sessions.length} atelier{sessions.length > 1 ? 's' : ''}
|
{sessions.length} atelier{sessions.length > 1 ? 's' : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<SessionsGrid sessions={sessions} view={cardView === 'timeline' ? 'list' : cardView} />
|
<SessionsGrid
|
||||||
|
sessions={sessions}
|
||||||
|
view={cardView === 'timeline' ? 'list' : cardView}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
) : activeTab === 'team' ? (
|
) : activeTab === 'team' ? (
|
||||||
/* ── Vue Équipe ─────────────────────────────────────────── */
|
/* ── Vue Équipe ─────────────────────────────────────────── */
|
||||||
teamCollabSessions.length === 0 ? (
|
teamCollabSessions.length === 0 ? (
|
||||||
<div className="text-center py-12 text-muted">Aucun atelier de vos collaborateurs (non partagés)</div>
|
<div className="text-center py-12 text-muted">
|
||||||
|
Aucun atelier de vos collaborateurs (non partagés)
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<section>
|
<section>
|
||||||
<SectionHeader label="Ateliers de l'équipe – non partagés" count={teamCollabSessions.length} />
|
<SectionHeader
|
||||||
|
label="Ateliers de l'équipe – non partagés"
|
||||||
|
count={teamCollabSessions.length}
|
||||||
|
/>
|
||||||
<p className="text-sm text-muted mb-5 -mt-2">
|
<p className="text-sm text-muted mb-5 -mt-2">
|
||||||
En tant qu'admin d'équipe, vous voyez les ateliers de vos collaborateurs
|
En tant qu'admin d'équipe, vous voyez les ateliers de vos collaborateurs
|
||||||
qui ne vous sont pas encore partagés.
|
qui ne vous sont pas encore partagés.
|
||||||
@@ -419,10 +611,8 @@ export function WorkshopTabs({
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
) : filteredSessions.length === 0 ? (
|
) : filteredSessions.length === 0 ? (
|
||||||
<div className="text-center py-12 text-muted">Aucun atelier de ce type pour le moment</div>
|
<div className="text-center py-12 text-muted">Aucun atelier de ce type pour le moment</div>
|
||||||
|
|
||||||
) : (
|
) : (
|
||||||
/* ── Vue normale (tous / par type) ─────────────────────── */
|
/* ── Vue normale (tous / par type) ─────────────────────── */
|
||||||
<div className="space-y-10">
|
<div className="space-y-10">
|
||||||
|
|||||||
Reference in New Issue
Block a user