feat: add session editing functionality with modal in WorkshopTabs component and enhance session revalidation

This commit is contained in:
Julien Froidefond
2025-11-28 10:48:36 +01:00
parent ac079ed8b2
commit cb4873cd40
4 changed files with 242 additions and 22 deletions

BIN
dev.db

Binary file not shown.

View File

@@ -49,6 +49,7 @@ export async function updateMotivatorSession(
revalidatePath(`/motivators/${sessionId}`);
revalidatePath('/motivators');
revalidatePath('/sessions'); // Also revalidate unified workshops page
return { success: true };
} catch (error) {
console.error('Error updating motivator session:', error);
@@ -65,6 +66,7 @@ export async function deleteMotivatorSession(sessionId: string) {
try {
await motivatorsService.deleteMotivatorSession(sessionId, authSession.user.id);
revalidatePath('/motivators');
revalidatePath('/sessions'); // Also revalidate unified workshops page
return { success: true };
} catch (error) {
console.error('Error deleting motivator session:', error);

View File

@@ -70,6 +70,46 @@ export async function updateSessionCollaborator(sessionId: string, collaborator:
}
}
export async function updateSwotSession(
sessionId: string,
data: { title?: string; collaborator?: string }
) {
const session = await auth();
if (!session?.user?.id) {
return { success: false, error: 'Non autorisé' };
}
if (data.title !== undefined && !data.title.trim()) {
return { success: false, error: 'Le titre ne peut pas être vide' };
}
if (data.collaborator !== undefined && !data.collaborator.trim()) {
return { success: false, error: 'Le nom du collaborateur ne peut pas être vide' };
}
try {
const updateData: { title?: string; collaborator?: string } = {};
if (data.title) updateData.title = data.title.trim();
if (data.collaborator) updateData.collaborator = data.collaborator.trim();
const result = await sessionsService.updateSession(sessionId, session.user.id, updateData);
if (result.count === 0) {
return { success: false, error: 'Session non trouvée ou non autorisé' };
}
// Emit event for real-time sync
await sessionsService.createSessionEvent(sessionId, session.user.id, 'SESSION_UPDATED', updateData);
revalidatePath(`/sessions/${sessionId}`);
revalidatePath('/sessions');
return { success: true };
} catch (error) {
console.error('Error updating session:', error);
return { success: false, error: 'Erreur lors de la mise à jour' };
}
}
export async function deleteSwotSession(sessionId: string) {
const session = await auth();
if (!session?.user?.id) {

View File

@@ -2,11 +2,11 @@
import { useState, useTransition } from 'react';
import Link from 'next/link';
import { Card, Badge, Button, Modal, ModalFooter } from '@/components/ui';
import { deleteSwotSession } from '@/actions/session';
import { deleteMotivatorSession } from '@/actions/moving-motivators';
import { Card, Badge, Button, Modal, ModalFooter, Input } from '@/components/ui';
import { deleteSwotSession, updateSwotSession } from '@/actions/session';
import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
type WorkshopType = 'all' | 'swot' | 'motivators';
type WorkshopType = 'all' | 'swot' | 'motivators' | 'byPerson';
interface ShareUser {
id: string;
@@ -53,6 +53,38 @@ interface WorkshopTabsProps {
motivatorSessions: MotivatorSession[];
}
// Helper to get participant name from any session
function getParticipant(session: AnySession): string {
return session.workshopType === 'swot'
? (session as SwotSession).collaborator
: (session as MotivatorSession).participant;
}
// Group sessions by participant
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();
// Use normalized key but store with original display name
const existing = grouped.get(participant);
if (existing) {
existing.push(session);
} else {
grouped.set(participant, [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 }: WorkshopTabsProps) {
const [activeTab, setActiveTab] = useState<WorkshopType>('all');
@@ -61,9 +93,9 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
);
// Filter based on active tab
// Filter based on active tab (for non-byPerson tabs)
const filteredSessions =
activeTab === 'all'
activeTab === 'all' || activeTab === 'byPerson'
? allSessions
: activeTab === 'swot'
? swotSessions
@@ -73,10 +105,16 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
const ownedSessions = filteredSessions.filter((s) => s.isOwner);
const sharedSessions = filteredSessions.filter((s) => !s.isOwner);
// 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">
<div className="flex gap-2 border-b border-border pb-4 flex-wrap">
<TabButton
active={activeTab === 'all'}
onClick={() => setActiveTab('all')}
@@ -84,6 +122,13 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
label="Tous"
count={allSessions.length}
/>
<TabButton
active={activeTab === 'byPerson'}
onClick={() => setActiveTab('byPerson')}
icon="👥"
label="Par personne"
count={sessionsByPerson.size}
/>
<TabButton
active={activeTab === 'swot'}
onClick={() => setActiveTab('swot')}
@@ -101,7 +146,38 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
</div>
{/* Sessions */}
{filteredSessions.length === 0 ? (
{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 displayName = getParticipant(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}
</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>
)
) : filteredSessions.length === 0 ? (
<div className="text-center py-12 text-muted">
Aucun atelier de ce type pour le moment
</div>
@@ -175,8 +251,17 @@ function TabButton({
function SessionCard({ session }: { session: AnySession }) {
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 as MotivatorSession).participant
);
const isSwot = session.workshopType === 'swot';
const href = isSwot ? `/sessions/${session.id}` : `/motivators/${session.id}`;
const icon = isSwot ? '📊' : '🎯';
@@ -199,6 +284,27 @@ function SessionCard({ session }: { session: AnySession }) {
});
};
const handleEdit = () => {
startTransition(async () => {
const result = isSwot
? await updateSwotSession(session.id, { title: editTitle, collaborator: editParticipant })
: 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);
};
return (
<>
<div className="relative group">
@@ -289,24 +395,96 @@ function SessionCard({ session }: { session: AnySession }) {
</Card>
</Link>
{/* Delete button - only for owner */}
{/* Action buttons - only for owner */}
{session.isOwner && (
<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>
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setShowDeleteModal(true);
}}
className="absolute top-3 right-3 p-1.5 rounded-lg opacity-0 group-hover:opacity-100 transition-opacity bg-destructive/10 text-destructive hover:bg-destructive/20"
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">
{isSwot ? 'Collaborateur' : 'Participant'}
</label>
<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() || !editParticipant.trim()}
>
{isPending ? 'Enregistrement...' : 'Enregistrer'}
</Button>
</ModalFooter>
</form>
</Modal>
{/* Delete confirmation modal */}
<Modal
isOpen={showDeleteModal}