feat: enhance session management by resolving collaborators to users and integrating CollaboratorDisplay component across motivators and sessions pages

This commit is contained in:
Julien Froidefond
2025-11-28 11:04:58 +01:00
parent 941151553f
commit eaeb1335fa
10 changed files with 237 additions and 33 deletions

View File

@@ -3,7 +3,7 @@ import Link from 'next/link';
import { auth } from '@/lib/auth';
import { getMotivatorSessionById } from '@/services/moving-motivators';
import { MotivatorBoard, MotivatorLiveWrapper } from '@/components/moving-motivators';
import { Badge } from '@/components/ui';
import { Badge, CollaboratorDisplay } from '@/components/ui';
import { EditableMotivatorTitle } from './EditableTitle';
interface MotivatorSessionPageProps {
@@ -48,9 +48,13 @@ export default async function MotivatorSessionPage({ params }: MotivatorSessionP
initialTitle={session.title}
isOwner={session.isOwner}
/>
<p className="mt-1 text-lg text-muted">
👤 {session.participant}
</p>
<div className="mt-2">
<CollaboratorDisplay
collaborator={session.resolvedParticipant}
size="lg"
showEmail
/>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">

View File

@@ -1,7 +1,7 @@
import Link from 'next/link';
import { auth } from '@/lib/auth';
import { getMotivatorSessionsByUserId } from '@/services/moving-motivators';
import { Card, CardContent, Badge, Button } from '@/components/ui';
import { Card, CardContent, Badge, Button, CollaboratorDisplay } from '@/components/ui';
export default async function MotivatorsPage() {
const session = await auth();
@@ -95,7 +95,9 @@ function SessionCard({ session: s }: { session: SessionWithMeta }) {
<h3 className="font-semibold text-foreground line-clamp-1">
{s.title}
</h3>
<p className="text-sm text-muted">{s.participant}</p>
<div className="mt-1">
<CollaboratorDisplay collaborator={s.resolvedParticipant} size="sm" />
</div>
{!s.isOwner && (
<p className="text-xs text-muted mt-1">
Par {s.user.name || s.user.email}

View File

@@ -2,7 +2,7 @@
import { useState, useTransition } from 'react';
import Link from 'next/link';
import { Card, Badge, Button, Modal, ModalFooter, Input } from '@/components/ui';
import { Card, Badge, Button, Modal, ModalFooter, Input, CollaboratorDisplay } from '@/components/ui';
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
@@ -20,10 +20,20 @@ interface Share {
user: ShareUser;
}
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';
@@ -37,6 +47,7 @@ interface MotivatorSession {
id: string;
title: string;
participant: string;
resolvedParticipant: ResolvedCollaborator;
updatedAt: Date;
isOwner: boolean;
role: 'OWNER' | 'VIEWER' | 'EDITOR';
@@ -60,20 +71,45 @@ function getParticipant(session: AnySession): string {
: (session as MotivatorSession).participant;
}
// Group sessions by participant
// Helper to get resolved collaborator from any session
function getResolvedCollaborator(session: AnySession): ResolvedCollaborator {
return session.workshopType === 'swot'
? (session as SwotSession).resolvedCollaborator
: (session as MotivatorSession).resolvedParticipant;
}
// Get display name for grouping - prefer matched user name
function getDisplayName(session: AnySession): string {
const resolved = getResolvedCollaborator(session);
if (resolved.matchedUser?.name) {
return resolved.matchedUser.name;
}
return resolved.raw;
}
// 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 participant = getParticipant(session).trim().toLowerCase();
const displayName = getParticipant(session).trim();
const key = getGroupKey(session);
// Use normalized key but store with original display name
const existing = grouped.get(participant);
const existing = grouped.get(key);
if (existing) {
existing.push(session);
} else {
grouped.set(participant, [session]);
grouped.set(key, [session]);
}
});
@@ -155,16 +191,13 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
) : (
<div className="space-y-8">
{sortedPersons.map(([personKey, sessions]) => {
const displayName = getParticipant(sessions[0]);
const resolved = getResolvedCollaborator(sessions[0]);
return (
<section key={personKey}>
<h2 className="text-lg font-semibold text-foreground mb-4 flex items-center gap-2">
<span className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center text-primary text-sm">
{displayName.charAt(0).toUpperCase()}
</span>
{displayName}
<Badge variant="primary" className="ml-2">
{sessions.length}
<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">
@@ -336,12 +369,15 @@ function SessionCard({ session }: { session: AnySession }) {
</div>
{/* Participant + Owner info */}
<p className="text-sm text-muted mb-3 line-clamp-1">
👤 {participant}
<div className="mb-3 flex items-center gap-2">
<CollaboratorDisplay
collaborator={getResolvedCollaborator(session)}
size="sm"
/>
{!session.isOwner && (
<span className="text-xs"> · par {session.user.name || session.user.email}</span>
<span className="text-xs text-muted">· par {session.user.name || session.user.email}</span>
)}
</p>
</div>
{/* Footer: Stats + Avatars + Date */}
<div className="flex items-center justify-between text-xs">

View File

@@ -5,7 +5,7 @@ import { getSessionById } from '@/services/sessions';
import { SwotBoard } from '@/components/swot/SwotBoard';
import { SessionLiveWrapper } from '@/components/collaboration';
import { EditableTitle } from '@/components/session';
import { Badge } from '@/components/ui';
import { Badge, CollaboratorDisplay } from '@/components/ui';
interface SessionPageProps {
params: Promise<{ id: string }>;
@@ -49,9 +49,13 @@ export default async function SessionPage({ params }: SessionPageProps) {
initialTitle={session.title}
isOwner={session.isOwner}
/>
<p className="mt-1 text-lg text-muted">
👤 {session.collaborator}
</p>
<div className="mt-2">
<CollaboratorDisplay
collaborator={session.resolvedCollaborator}
size="lg"
showEmail
/>
</div>
</div>
<div className="flex items-center gap-3">
<Badge variant="primary">{session.items.length} items</Badge>