- Intégration du composant SignaturePad dans EmargementScan.tsx - Ajout des champs de signature dans la table presences (signatureUrl, signatureS3Key, dateSigned) - Modification de la procédure presences.valider pour accepter et uploader la signature en S3 - Stockage automatique de l'URL et de la clé S3 dans la base de données - Correction des types nullables dans exportService.ts (codeEtablissement, fonction) - Les apprenants doivent maintenant signer avec le doigt sur l'écran avant de valider leur présence
193 lines
6.8 KiB
TypeScript
193 lines
6.8 KiB
TypeScript
import { useState } from "react";
|
|
import { useParams } from "wouter";
|
|
import { trpc } from "@/lib/trpc";
|
|
import SignaturePadComponent from "@/components/SignaturePad";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card } from "@/components/ui/card";
|
|
import { FileText, Loader2, CheckCircle2, PenTool } from "lucide-react";
|
|
|
|
export default function SignerAttestation() {
|
|
const params = useParams();
|
|
const attestationId = params.id ? parseInt(params.id) : null;
|
|
|
|
const [showSignaturePad, setShowSignaturePad] = useState(false);
|
|
const [isSigned, setIsSigned] = useState(false);
|
|
|
|
const { data: attestation, isLoading } = trpc.attestations.get.useQuery(
|
|
{
|
|
apprenantId: 0, // TODO: récupérer depuis le token
|
|
sequenceId: 0,
|
|
},
|
|
{
|
|
enabled: !!attestationId,
|
|
}
|
|
);
|
|
|
|
const uploadSignatureMutation = trpc.attestations.uploadSignature.useMutation({
|
|
onSuccess: () => {
|
|
setIsSigned(true);
|
|
setShowSignaturePad(false);
|
|
},
|
|
});
|
|
|
|
const handleSaveSignature = async (dataUrl: string) => {
|
|
if (!attestationId) return;
|
|
|
|
await uploadSignatureMutation.mutateAsync({
|
|
attestationId,
|
|
signatureDataUrl: dataUrl,
|
|
});
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 flex items-center justify-center p-4">
|
|
<Card className="p-8 max-w-md w-full text-center">
|
|
<Loader2 className="w-12 h-12 animate-spin mx-auto text-blue-600 mb-4" />
|
|
<p className="text-gray-600">Chargement de votre attestation...</p>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!attestation) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 flex items-center justify-center p-4">
|
|
<Card className="p-8 max-w-md w-full text-center">
|
|
<FileText className="w-16 h-16 mx-auto text-gray-400 mb-4" />
|
|
<h1 className="text-2xl font-bold text-gray-900 mb-2">
|
|
Attestation introuvable
|
|
</h1>
|
|
<p className="text-gray-600">
|
|
Le lien que vous avez suivi n'est pas valide ou a expiré.
|
|
</p>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (isSigned) {
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-50 flex items-center justify-center p-4">
|
|
<Card className="p-8 max-w-md w-full text-center animate-in fade-in zoom-in duration-500">
|
|
<div className="w-20 h-20 bg-gradient-to-br from-green-500 to-emerald-600 rounded-full flex items-center justify-center mx-auto mb-6 animate-in zoom-in duration-700">
|
|
<CheckCircle2 className="w-12 h-12 text-white" />
|
|
</div>
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-3">
|
|
Attestation signée !
|
|
</h1>
|
|
<p className="text-gray-600 mb-6">
|
|
Votre signature a été enregistrée avec succès. Vous recevrez votre
|
|
attestation signée par email.
|
|
</p>
|
|
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
|
|
<p className="text-sm text-green-800">
|
|
<strong>Date de signature :</strong>{" "}
|
|
{new Date().toLocaleDateString("fr-FR", {
|
|
day: "numeric",
|
|
month: "long",
|
|
year: "numeric",
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
})}
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-50 flex items-center justify-center p-4">
|
|
<Card className="p-8 max-w-2xl w-full">
|
|
<div className="text-center mb-8">
|
|
<div className="w-16 h-16 bg-gradient-to-br from-blue-600 to-indigo-700 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<FileText className="w-8 h-8 text-white" />
|
|
</div>
|
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">
|
|
Signature de votre attestation
|
|
</h1>
|
|
<p className="text-gray-600">
|
|
Signez électroniquement votre attestation de formation
|
|
</p>
|
|
</div>
|
|
|
|
<div className="bg-white border-2 border-gray-200 rounded-lg p-6 mb-6">
|
|
<h2 className="text-lg font-semibold text-gray-900 mb-4">
|
|
Informations de l'attestation
|
|
</h2>
|
|
<div className="space-y-2 text-sm">
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Formation :</span>
|
|
<span className="font-medium text-gray-900">
|
|
{/* TODO: afficher le nom de la formation */}
|
|
Formation Manager Itinova
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Séquence :</span>
|
|
<span className="font-medium text-gray-900">
|
|
{/* TODO: afficher le nom de la séquence */}
|
|
Groupe A
|
|
</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-gray-600">Date de génération :</span>
|
|
<span className="font-medium text-gray-900">
|
|
{attestation.dateGeneration
|
|
? new Date(attestation.dateGeneration).toLocaleDateString(
|
|
"fr-FR"
|
|
)
|
|
: "N/A"}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{attestation.urlPdf && (
|
|
<div className="mb-6">
|
|
<Button
|
|
variant="outline"
|
|
className="w-full"
|
|
onClick={() => window.open(attestation.urlPdf!, "_blank")}
|
|
>
|
|
<FileText className="w-4 h-4 mr-2" />
|
|
Prévisualiser l'attestation
|
|
</Button>
|
|
</div>
|
|
)}
|
|
|
|
<Button
|
|
onClick={() => setShowSignaturePad(true)}
|
|
disabled={uploadSignatureMutation.isPending}
|
|
className="w-full h-14 text-lg bg-gradient-to-r from-blue-600 to-indigo-700 hover:from-blue-700 hover:to-indigo-800"
|
|
>
|
|
{uploadSignatureMutation.isPending ? (
|
|
<>
|
|
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
|
|
Enregistrement...
|
|
</>
|
|
) : (
|
|
<>
|
|
<PenTool className="w-5 h-5 mr-2" />
|
|
Signer l'attestation
|
|
</>
|
|
)}
|
|
</Button>
|
|
|
|
<p className="text-xs text-gray-500 text-center mt-4">
|
|
En signant, vous certifiez avoir suivi cette formation et acceptez
|
|
que votre signature soit intégrée à l'attestation.
|
|
</p>
|
|
</Card>
|
|
|
|
{showSignaturePad && (
|
|
<SignaturePadComponent
|
|
onSave={handleSaveSignature}
|
|
onCancel={() => setShowSignaturePad(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|