feat: add session editing functionality with modal in WorkshopTabs component and enhance session revalidation
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 && (
|
||||
<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"
|
||||
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 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="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}
|
||||
|
||||
Reference in New Issue
Block a user