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/${sessionId}`);
revalidatePath('/motivators'); revalidatePath('/motivators');
revalidatePath('/sessions'); // Also revalidate unified workshops page
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error updating motivator session:', error); console.error('Error updating motivator session:', error);
@@ -65,6 +66,7 @@ export async function deleteMotivatorSession(sessionId: string) {
try { try {
await motivatorsService.deleteMotivatorSession(sessionId, authSession.user.id); await motivatorsService.deleteMotivatorSession(sessionId, authSession.user.id);
revalidatePath('/motivators'); revalidatePath('/motivators');
revalidatePath('/sessions'); // Also revalidate unified workshops page
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
console.error('Error deleting motivator session:', 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) { export async function deleteSwotSession(sessionId: string) {
const session = await auth(); const session = await auth();
if (!session?.user?.id) { if (!session?.user?.id) {

View File

@@ -2,11 +2,11 @@
import { useState, useTransition } from 'react'; import { useState, useTransition } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { Card, Badge, Button, Modal, ModalFooter } from '@/components/ui'; import { Card, Badge, Button, Modal, ModalFooter, Input } from '@/components/ui';
import { deleteSwotSession } from '@/actions/session'; import { deleteSwotSession, updateSwotSession } from '@/actions/session';
import { deleteMotivatorSession } from '@/actions/moving-motivators'; import { deleteMotivatorSession, updateMotivatorSession } from '@/actions/moving-motivators';
type WorkshopType = 'all' | 'swot' | 'motivators'; type WorkshopType = 'all' | 'swot' | 'motivators' | 'byPerson';
interface ShareUser { interface ShareUser {
id: string; id: string;
@@ -53,6 +53,38 @@ interface WorkshopTabsProps {
motivatorSessions: MotivatorSession[]; 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) { export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsProps) {
const [activeTab, setActiveTab] = useState<WorkshopType>('all'); 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() (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 = const filteredSessions =
activeTab === 'all' activeTab === 'all' || activeTab === 'byPerson'
? allSessions ? allSessions
: activeTab === 'swot' : activeTab === 'swot'
? swotSessions ? swotSessions
@@ -73,10 +105,16 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
const ownedSessions = filteredSessions.filter((s) => s.isOwner); const ownedSessions = filteredSessions.filter((s) => s.isOwner);
const sharedSessions = 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Tabs */} {/* 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 <TabButton
active={activeTab === 'all'} active={activeTab === 'all'}
onClick={() => setActiveTab('all')} onClick={() => setActiveTab('all')}
@@ -84,6 +122,13 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
label="Tous" label="Tous"
count={allSessions.length} count={allSessions.length}
/> />
<TabButton
active={activeTab === 'byPerson'}
onClick={() => setActiveTab('byPerson')}
icon="👥"
label="Par personne"
count={sessionsByPerson.size}
/>
<TabButton <TabButton
active={activeTab === 'swot'} active={activeTab === 'swot'}
onClick={() => setActiveTab('swot')} onClick={() => setActiveTab('swot')}
@@ -101,7 +146,38 @@ export function WorkshopTabs({ swotSessions, motivatorSessions }: WorkshopTabsPr
</div> </div>
{/* Sessions */} {/* 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"> <div className="text-center py-12 text-muted">
Aucun atelier de ce type pour le moment Aucun atelier de ce type pour le moment
</div> </div>
@@ -175,7 +251,16 @@ function TabButton({
function SessionCard({ session }: { session: AnySession }) { function SessionCard({ session }: { session: AnySession }) {
const [showDeleteModal, setShowDeleteModal] = useState(false); const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [isPending, startTransition] = useTransition(); 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 isSwot = session.workshopType === 'swot';
const href = isSwot ? `/sessions/${session.id}` : `/motivators/${session.id}`; const href = isSwot ? `/sessions/${session.id}` : `/motivators/${session.id}`;
@@ -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 ( return (
<> <>
<div className="relative group"> <div className="relative group">
@@ -289,24 +395,96 @@ function SessionCard({ session }: { session: AnySession }) {
</Card> </Card>
</Link> </Link>
{/* Delete button - only for owner */} {/* Action buttons - only for owner */}
{session.isOwner && ( {session.isOwner && (
<button <div className="absolute top-3 right-3 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
onClick={(e) => { <button
e.preventDefault(); onClick={(e) => {
e.stopPropagation(); e.preventDefault();
setShowDeleteModal(true); e.stopPropagation();
}} openEditModal();
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" }}
title="Supprimer" 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="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 className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</svg> <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" />
</button> </svg>
</button>
<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> </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 */} {/* Delete confirmation modal */}
<Modal <Modal
isOpen={showDeleteModal} isOpen={showDeleteModal}