feat: Ajouter la suppression du contrat signé depuis staff/contrats/[id]

- Créer l'API DELETE /api/staff/contrats/[id]/delete-signed-pdf
- Supprimer le fichier S3 et mettre à jour la BDD
- Ajouter un bouton de suppression dans la card Documents
- Afficher un modal de confirmation avant suppression
- Invalider les queries React Query après suppression
This commit is contained in:
odentas 2025-12-11 19:10:36 +01:00
parent c0142d167e
commit 543cc2da95
2 changed files with 214 additions and 16 deletions

View file

@ -0,0 +1,123 @@
// app/api/staff/contrats/[id]/delete-signed-pdf/route.ts
import { NextRequest, NextResponse } from "next/server";
import { createSbServer } from "@/lib/supabaseServer";
import { S3Client, DeleteObjectCommand } from "@aws-sdk/client-s3";
const REGION = process.env.AWS_REGION || "eu-west-3";
const BUCKET = (process.env.AWS_S3_BUCKET || "odentas-docs").trim();
const s3Client = new S3Client({
region: REGION,
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
});
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const sb = createSbServer();
// Vérification de l'authentification
const { data: { user }, error: authError } = await sb.auth.getUser();
if (authError || !user) {
return NextResponse.json(
{ error: "Non authentifié" },
{ status: 401 }
);
}
// Vérification de l'accès staff
const { data: staffUser } = await sb
.from("staff_users")
.select("is_staff")
.eq("user_id", user.id)
.maybeSingle();
if (!staffUser?.is_staff) {
return NextResponse.json(
{ error: "Accès refusé - staff uniquement" },
{ status: 403 }
);
}
// Récupération du contrat
const { data: contract, error: contractError } = await sb
.from("cddu_contracts")
.select("id, contract_pdf_s3_key, contract_number")
.eq("id", params.id)
.single();
if (contractError || !contract) {
return NextResponse.json(
{ error: "Contrat introuvable" },
{ status: 404 }
);
}
// Vérification de la présence de la clé S3
if (!contract.contract_pdf_s3_key) {
return NextResponse.json(
{ error: "Aucun contrat signé à supprimer" },
{ status: 404 }
);
}
console.log("🗑️ Suppression du contrat signé:", {
contractId: contract.id,
contractNumber: contract.contract_number,
s3Key: contract.contract_pdf_s3_key,
});
// Suppression du fichier dans S3
try {
const deleteCommand = new DeleteObjectCommand({
Bucket: BUCKET,
Key: contract.contract_pdf_s3_key,
});
await s3Client.send(deleteCommand);
console.log("✅ Fichier supprimé de S3:", contract.contract_pdf_s3_key);
} catch (s3Error) {
console.error("❌ Erreur lors de la suppression S3:", s3Error);
return NextResponse.json(
{ error: "Erreur lors de la suppression du fichier" },
{ status: 500 }
);
}
// Mise à jour du contrat (retrait de la clé S3 et du statut signé)
const { error: updateError } = await sb
.from("cddu_contracts")
.update({
contract_pdf_s3_key: null,
contrat_signe: null, // Remettre à null au lieu de "Non"
})
.eq("id", contract.id);
if (updateError) {
console.error("❌ Erreur lors de la mise à jour du contrat:", updateError);
return NextResponse.json(
{ error: "Erreur lors de la mise à jour du contrat" },
{ status: 500 }
);
}
console.log("✅ Contrat mis à jour, clé S3 retirée:", contract.id);
return NextResponse.json({
success: true,
message: "Contrat signé supprimé avec succès",
});
} catch (error) {
console.error("❌ Erreur lors de la suppression du contrat signé:", error);
return NextResponse.json(
{ error: "Erreur interne du serveur" },
{ status: 500 }
);
}
}

View file

@ -9,6 +9,7 @@ import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Separator } from "@/components/ui/separator";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { toast } from "sonner";
import { CalendarRange, FilePlus2, FileText, Save, Search, FileDown, PenTool, RefreshCw, Mail, Clock, CheckCircle2, XCircle, Users, Send, Check, Upload, Ban, Euro, StickyNote } from "lucide-react";
import PayslipForm from "./PayslipForm";
@ -582,6 +583,10 @@ export default function ContractEditor({
const [showCancelModal, setShowCancelModal] = useState(false);
const [isCancelling, setIsCancelling] = useState(false);
// Delete signed PDF states
const [showDeleteSignedPdfModal, setShowDeleteSignedPdfModal] = useState(false);
const [isDeletingSignedPdf, setIsDeletingSignedPdf] = useState(false);
// Handler pour annuler le contrat
const handleCancelContract = async () => {
setIsCancelling(true);
@ -599,18 +604,42 @@ export default function ContractEditor({
toast.success("Contrat annulé avec succès");
setShowCancelModal(false);
// Recharger la page pour afficher les mises à jour
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (error: any) {
console.error("Erreur annulation contrat:", error);
toast.error(`Erreur: ${error.message}`);
// Rafraîchir la page ou rediriger
window.location.reload();
} catch (error) {
console.error("Erreur lors de l'annulation du contrat:", error);
toast.error(error instanceof Error ? error.message : "Erreur lors de l'annulation");
} finally {
setIsCancelling(false);
}
};
// Handler pour supprimer le contrat signé
const handleDeleteSignedPdf = async () => {
setIsDeletingSignedPdf(true);
try {
const response = await fetch(`/api/staff/contrats/${contract.id}/delete-signed-pdf`, {
method: "DELETE",
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || "Erreur lors de la suppression");
}
toast.success("Contrat signé supprimé avec succès");
setShowDeleteSignedPdfModal(false);
// Invalider les queries React Query pour rafraîchir les données
queryClient.invalidateQueries({ queryKey: ["signed-contract-pdf", contract.id] });
} catch (error) {
console.error("Erreur lors de la suppression du contrat signé:", error);
toast.error(error instanceof Error ? error.message : "Erreur lors de la suppression");
} finally {
setIsDeletingSignedPdf(false);
}
};
// Handler pour le calendrier des dates de représentations
// Ouvrir le modal de quantités pour permettre la précision par date
const handleDatesRepresentationsApply = (result: {
@ -3234,20 +3263,26 @@ export default function ContractEditor({
</div>
</div>
) : signedPdfData?.hasSignedPdf && signedPdfData?.signedUrl ? (
<div
onClick={openSignedPdf}
className="flex items-center gap-3 p-3 border-2 border-green-200 bg-green-50 rounded-lg cursor-pointer hover:bg-green-100 transition-colors"
>
<CheckCircle2 className="size-5 text-green-600" />
<div className="flex-1">
<div className="flex items-center gap-3 p-3 border-2 border-green-200 bg-green-50 rounded-lg">
<CheckCircle2 className="size-5 text-green-600 flex-shrink-0" />
<div
onClick={openSignedPdf}
className="flex-1 cursor-pointer hover:underline"
>
<p className="font-medium text-green-800">Contrat CDDU signé</p>
<p className="text-sm text-green-600">
Document électroniquement signé
</p>
</div>
<div className="text-xs text-green-600">
Cliquer pour ouvrir
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setShowDeleteSignedPdfModal(true)}
className="text-red-600 hover:text-red-700 hover:bg-red-50"
title="Supprimer le contrat signé"
>
<Ban className="size-4" />
</Button>
</div>
) : null}
@ -3544,6 +3579,46 @@ export default function ContractEditor({
employee_name: contract.employee_name || contract.salaries?.salarie,
}}
/>
{/* Delete Signed PDF Confirmation Modal */}
<Dialog open={showDeleteSignedPdfModal} onOpenChange={setShowDeleteSignedPdfModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Supprimer le contrat signé</DialogTitle>
<DialogDescription>
Êtes-vous sûr de vouloir supprimer le contrat signé ?
Cette action est irréversible et supprimera le fichier du stockage.
</DialogDescription>
</DialogHeader>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={() => setShowDeleteSignedPdfModal(false)}
disabled={isDeletingSignedPdf}
>
Annuler
</Button>
<Button
variant="default"
onClick={handleDeleteSignedPdf}
disabled={isDeletingSignedPdf}
className="bg-red-600 hover:bg-red-700 text-white"
>
{isDeletingSignedPdf ? (
<>
<RefreshCw className="size-4 mr-2 animate-spin" />
Suppression...
</>
) : (
<>
<Ban className="size-4 mr-2" />
Supprimer
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}