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/${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);
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,8 +251,17 @@ 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}`;
|
||||||
const icon = isSwot ? '📊' : '🎯';
|
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 (
|
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 && (
|
||||||
|
<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
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setShowDeleteModal(true);
|
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"
|
title="Supprimer"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<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" />
|
<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>
|
</svg>
|
||||||
</button>
|
</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}
|
||||||
|
|||||||
Reference in New Issue
Block a user