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:
parent
c0142d167e
commit
543cc2da95
2 changed files with 214 additions and 16 deletions
123
app/api/staff/contrats/[id]/delete-signed-pdf/route.ts
Normal file
123
app/api/staff/contrats/[id]/delete-signed-pdf/route.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue