feat: implement session deletion functionality with confirmation modal in WorkshopTabs component
This commit is contained in:
@@ -70,3 +70,24 @@ export async function updateSessionCollaborator(sessionId: string, collaborator:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteSwotSession(sessionId: string) {
|
||||||
|
const session = await auth();
|
||||||
|
if (!session?.user?.id) {
|
||||||
|
return { success: false, error: 'Non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await sessionsService.deleteSession(sessionId, session.user.id);
|
||||||
|
|
||||||
|
if (result.count === 0) {
|
||||||
|
return { success: false, error: 'Session non trouvée ou non autorisé' };
|
||||||
|
}
|
||||||
|
|
||||||
|
revalidatePath('/sessions');
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting session:', error);
|
||||||
|
return { success: false, error: 'Erreur lors de la suppression' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState, useTransition } from 'react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Card, Badge } from '@/components/ui';
|
import { Card, Badge, Button, Modal, ModalFooter } from '@/components/ui';
|
||||||
|
import { deleteSwotSession } from '@/actions/session';
|
||||||
|
import { deleteMotivatorSession } from '@/actions/moving-motivators';
|
||||||
|
|
||||||
type WorkshopType = 'all' | 'swot' | 'motivators';
|
type WorkshopType = 'all' | 'swot' | 'motivators';
|
||||||
|
|
||||||
@@ -172,6 +174,9 @@ function TabButton({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SessionCard({ session }: { session: AnySession }) {
|
function SessionCard({ session }: { session: AnySession }) {
|
||||||
|
const [showDeleteModal, setShowDeleteModal] = useState(false);
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
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 ? '📊' : '🎯';
|
||||||
@@ -180,93 +185,161 @@ function SessionCard({ session }: { session: AnySession }) {
|
|||||||
: (session as MotivatorSession).participant;
|
: (session as MotivatorSession).participant;
|
||||||
const accentColor = isSwot ? '#06b6d4' : '#8b5cf6';
|
const accentColor = isSwot ? '#06b6d4' : '#8b5cf6';
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
startTransition(async () => {
|
||||||
|
const result = isSwot
|
||||||
|
? await deleteSwotSession(session.id)
|
||||||
|
: await deleteMotivatorSession(session.id);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setShowDeleteModal(false);
|
||||||
|
} else {
|
||||||
|
console.error('Error deleting session:', result.error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href={href}>
|
<>
|
||||||
<Card hover className="h-full p-4 relative overflow-hidden">
|
<div className="relative group">
|
||||||
{/* Accent bar */}
|
<Link href={href}>
|
||||||
<div
|
<Card hover className="h-full p-4 relative overflow-hidden">
|
||||||
className="absolute top-0 left-0 right-0 h-1"
|
{/* Accent bar */}
|
||||||
style={{ backgroundColor: accentColor }}
|
<div
|
||||||
/>
|
className="absolute top-0 left-0 right-0 h-1"
|
||||||
|
style={{ backgroundColor: accentColor }}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Header: Icon + Title + Role badge */}
|
{/* Header: Icon + Title + Role badge */}
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<span className="text-xl">{icon}</span>
|
<span className="text-xl">{icon}</span>
|
||||||
<h3 className="font-semibold text-foreground line-clamp-1 flex-1">
|
<h3 className="font-semibold text-foreground line-clamp-1 flex-1">
|
||||||
{session.title}
|
{session.title}
|
||||||
</h3>
|
</h3>
|
||||||
{!session.isOwner && (
|
{!session.isOwner && (
|
||||||
<span
|
<span
|
||||||
className="text-xs px-1.5 py-0.5 rounded"
|
className="text-xs px-1.5 py-0.5 rounded"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: session.role === 'EDITOR' ? 'rgba(6,182,212,0.1)' : 'rgba(234,179,8,0.1)',
|
backgroundColor: session.role === 'EDITOR' ? 'rgba(6,182,212,0.1)' : 'rgba(234,179,8,0.1)',
|
||||||
color: session.role === 'EDITOR' ? '#06b6d4' : '#eab308',
|
color: session.role === 'EDITOR' ? '#06b6d4' : '#eab308',
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
{session.role === 'EDITOR' ? '✏️' : '👁️'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Participant + Owner info */}
|
|
||||||
<p className="text-sm text-muted mb-3 line-clamp-1">
|
|
||||||
👤 {participant}
|
|
||||||
{!session.isOwner && (
|
|
||||||
<span className="text-xs"> · par {session.user.name || session.user.email}</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{/* Footer: Stats + Avatars + Date */}
|
|
||||||
<div className="flex items-center justify-between text-xs">
|
|
||||||
{/* Stats */}
|
|
||||||
<div className="flex items-center gap-2 text-muted">
|
|
||||||
{isSwot ? (
|
|
||||||
<>
|
|
||||||
<span>{(session as SwotSession)._count.items} items</span>
|
|
||||||
<span>·</span>
|
|
||||||
<span>{(session as SwotSession)._count.actions} actions</span>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<span>{(session as MotivatorSession)._count.cards}/10</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Date */}
|
|
||||||
<span className="text-muted">
|
|
||||||
{new Date(session.updatedAt).toLocaleDateString('fr-FR', {
|
|
||||||
day: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Shared with */}
|
|
||||||
{session.isOwner && session.shares.length > 0 && (
|
|
||||||
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border">
|
|
||||||
<span className="text-[10px] text-muted uppercase tracking-wide">Partagé</span>
|
|
||||||
<div className="flex flex-wrap gap-1.5">
|
|
||||||
{session.shares.slice(0, 3).map((share) => (
|
|
||||||
<div
|
|
||||||
key={share.id}
|
|
||||||
className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-primary/10 text-[10px] text-primary"
|
|
||||||
title={share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
|
||||||
>
|
>
|
||||||
<span className="font-medium">
|
{session.role === 'EDITOR' ? '✏️' : '👁️'}
|
||||||
{share.user.name?.split(' ')[0] || share.user.email.split('@')[0]}
|
|
||||||
</span>
|
|
||||||
<span>{share.role === 'EDITOR' ? '✏️' : '👁️'}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{session.shares.length > 3 && (
|
|
||||||
<span className="text-[10px] text-muted">
|
|
||||||
+{session.shares.length - 3}
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Participant + Owner info */}
|
||||||
|
<p className="text-sm text-muted mb-3 line-clamp-1">
|
||||||
|
👤 {participant}
|
||||||
|
{!session.isOwner && (
|
||||||
|
<span className="text-xs"> · par {session.user.name || session.user.email}</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Footer: Stats + Avatars + Date */}
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
{/* Stats */}
|
||||||
|
<div className="flex items-center gap-2 text-muted">
|
||||||
|
{isSwot ? (
|
||||||
|
<>
|
||||||
|
<span>{(session as SwotSession)._count.items} items</span>
|
||||||
|
<span>·</span>
|
||||||
|
<span>{(session as SwotSession)._count.actions} actions</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span>{(session as MotivatorSession)._count.cards}/10</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<span className="text-muted">
|
||||||
|
{new Date(session.updatedAt).toLocaleDateString('fr-FR', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Shared with */}
|
||||||
|
{session.isOwner && session.shares.length > 0 && (
|
||||||
|
<div className="flex items-center gap-2 mt-3 pt-3 border-t border-border">
|
||||||
|
<span className="text-[10px] text-muted uppercase tracking-wide">Partagé</span>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{session.shares.slice(0, 3).map((share) => (
|
||||||
|
<div
|
||||||
|
key={share.id}
|
||||||
|
className="flex items-center gap-1 px-1.5 py-0.5 rounded-full bg-primary/10 text-[10px] text-primary"
|
||||||
|
title={share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
|
||||||
|
>
|
||||||
|
<span className="font-medium">
|
||||||
|
{share.user.name?.split(' ')[0] || share.user.email.split('@')[0]}
|
||||||
|
</span>
|
||||||
|
<span>{share.role === 'EDITOR' ? '✏️' : '👁️'}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{session.shares.length > 3 && (
|
||||||
|
<span className="text-[10px] text-muted">
|
||||||
|
+{session.shares.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Delete button - 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>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</div>
|
||||||
</Link>
|
|
||||||
|
{/* Delete confirmation modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={showDeleteModal}
|
||||||
|
onClose={() => setShowDeleteModal(false)}
|
||||||
|
title="Supprimer l'atelier"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<p className="text-muted">
|
||||||
|
Êtes-vous sûr de vouloir supprimer l'atelier <strong className="text-foreground">"{session.title}"</strong> ?
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-destructive">
|
||||||
|
Cette action est irréversible. Toutes les données seront perdues.
|
||||||
|
</p>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => setShowDeleteModal(false)}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleDelete}
|
||||||
|
disabled={isPending}
|
||||||
|
>
|
||||||
|
{isPending ? 'Suppression...' : 'Supprimer'}
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user