Checkpoint: Implémentation de la signature manuscrite tactile pour l'émargement via QR code :

- 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
This commit is contained in:
Manus
2026-01-27 04:29:01 -05:00
parent b3b808145c
commit 1e915acd5b
16 changed files with 4936 additions and 11 deletions

View File

@@ -0,0 +1,118 @@
import { useEffect, useRef, useState } from "react";
import SignaturePad from "signature_pad";
import { Button } from "./ui/button";
import { X, RotateCcw, Check } from "lucide-react";
interface SignaturePadComponentProps {
onSave: (dataUrl: string) => void;
onCancel: () => void;
}
export default function SignaturePadComponent({
onSave,
onCancel,
}: SignaturePadComponentProps) {
const canvasRef = useRef<HTMLCanvasElement>(null);
const signaturePadRef = useRef<SignaturePad | null>(null);
const [isEmpty, setIsEmpty] = useState(true);
useEffect(() => {
if (!canvasRef.current) return;
const canvas = canvasRef.current;
const signaturePad = new SignaturePad(canvas, {
backgroundColor: "rgb(255, 255, 255)",
penColor: "rgb(0, 0, 0)",
});
signaturePadRef.current = signaturePad;
// Redimensionner le canvas pour correspondre à sa taille d'affichage
const resizeCanvas = () => {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d")!.scale(ratio, ratio);
signaturePad.clear();
};
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
// Détecter les changements
signaturePad.addEventListener("endStroke", () => {
setIsEmpty(signaturePad.isEmpty());
});
return () => {
window.removeEventListener("resize", resizeCanvas);
signaturePad.off();
};
}, []);
const handleClear = () => {
if (signaturePadRef.current) {
signaturePadRef.current.clear();
setIsEmpty(true);
}
};
const handleSave = () => {
if (signaturePadRef.current && !signaturePadRef.current.isEmpty()) {
const dataUrl = signaturePadRef.current.toDataURL("image/png");
onSave(dataUrl);
}
};
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl animate-in fade-in zoom-in duration-300">
<div className="p-4 border-b border-gray-200 flex items-center justify-between">
<h2 className="text-xl font-semibold text-gray-900">
Signez votre attestation
</h2>
<button
onClick={onCancel}
className="text-gray-400 hover:text-gray-600 transition-colors"
>
<X className="w-6 h-6" />
</button>
</div>
<div className="p-6">
<p className="text-sm text-gray-600 mb-4">
Dessinez votre signature ci-dessous avec votre doigt ou votre stylet
</p>
<div className="border-2 border-gray-300 rounded-lg overflow-hidden bg-white">
<canvas
ref={canvasRef}
className="w-full touch-none"
style={{ height: "300px" }}
/>
</div>
<div className="mt-6 flex gap-3">
<Button
variant="outline"
onClick={handleClear}
disabled={isEmpty}
className="flex-1"
>
<RotateCcw className="w-4 h-4 mr-2" />
Effacer
</Button>
<Button
onClick={handleSave}
disabled={isEmpty}
className="flex-1 bg-gradient-to-r from-blue-600 to-blue-700 hover:from-blue-700 hover:to-blue-800"
>
<Check className="w-4 h-4 mr-2" />
Valider la signature
</Button>
</div>
</div>
</div>
</div>
);
}