Checkpoint: Implémentation de l'émargement numérique avec deux signatures par jour (matin et après-midi):

- Ajout du champ 'periode' dans la table presences
- Modification des procédures backend pour gérer les deux périodes
- Mise à jour de l'interface EmargementScan avec choix de période
- Mise à jour de l'interface FormateurEmargement avec validation séparée matin/après-midi
- La feuille de présence PDF était déjà configurée avec deux colonnes
This commit is contained in:
Manus
2026-01-25 08:26:42 -05:00
parent 1460b41f2d
commit 3e14adcf70
10 changed files with 2359 additions and 63 deletions

View File

@@ -20,6 +20,7 @@ export default function EmargementScan() {
const [email, setEmail] = useState("");
const [selectedInscriptionId, setSelectedInscriptionId] = useState<number | null>(null);
const [selectedDateId, setSelectedDateId] = useState<number | null>(null);
const [selectedPeriode, setSelectedPeriode] = useState<"matin" | "apres_midi">("matin");
const [success, setSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
@@ -70,6 +71,7 @@ export default function EmargementScan() {
token,
inscriptionId: selectedInscriptionId,
dateFormationId: selectedDateId,
periode: selectedPeriode,
modeValidation: "qrcode",
});
};
@@ -209,29 +211,47 @@ export default function EmargementScan() {
</div>
{selectedInscriptionId && datesDisponibles.length > 0 && (
<div>
<Label>Sélectionnez la date</Label>
<Select
value={selectedDateId?.toString() || ""}
onValueChange={(value) => setSelectedDateId(parseInt(value))}
>
<SelectTrigger>
<SelectValue placeholder="Choisir une date..." />
</SelectTrigger>
<SelectContent>
{datesDisponibles.map((date: any) => (
<SelectItem key={date.id} value={date.id.toString()}>
{new Date(date.dateDebut).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<>
<div>
<Label>Sélectionnez la date</Label>
<Select
value={selectedDateId?.toString() || ""}
onValueChange={(value) => setSelectedDateId(parseInt(value))}
>
<SelectTrigger>
<SelectValue placeholder="Choisir une date..." />
</SelectTrigger>
<SelectContent>
{datesDisponibles.map((date: any) => (
<SelectItem key={date.id} value={date.id.toString()}>
{new Date(date.dateDebut).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Période de la journée</Label>
<Select
value={selectedPeriode}
onValueChange={(value) => setSelectedPeriode(value as "matin" | "apres_midi")}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="matin">🌅 Matin</SelectItem>
<SelectItem value="apres_midi">🌆 Après-midi</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
<Button

View File

@@ -76,19 +76,20 @@ export default function FormateurEmargement() {
generateQR({ sequenceId: selectedSequenceId });
};
const handleValiderPresence = (inscriptionId: number, dateFormationId: number) => {
const handleValiderPresence = (inscriptionId: number, dateFormationId: number, periode: "matin" | "apres_midi") => {
validerPresence({
inscriptionId,
dateFormationId,
periode,
modeValidation: "manuel",
validateurId: user?.id,
});
};
// Vérifier si un apprenant a validé sa présence pour une date
const isPresent = (inscriptionId: number, dateFormationId: number) => {
// Vérifier si un apprenant a validé sa présence pour une date et une période
const isPresent = (inscriptionId: number, dateFormationId: number, periode: "matin" | "apres_midi") => {
return presences?.some(
(p) => p.inscriptionId === inscriptionId && p.dateFormationId === dateFormationId
(p) => p.inscriptionId === inscriptionId && p.dateFormationId === dateFormationId && p.periode === periode
);
};
@@ -223,46 +224,80 @@ export default function FormateurEmargement() {
<div className="space-y-2">
{inscrit.dates && Array.isArray(inscrit.dates) && inscrit.dates.length > 0 ? inscrit.dates.map((date: any) => {
if (!date || !date.id) return null;
const present = isPresent(inscrit.inscription.id, date.id);
const presentMatin = isPresent(inscrit.inscription.id, date.id, "matin");
const presentApresMidi = isPresent(inscrit.inscription.id, date.id, "apres_midi");
return (
<div
key={date.id}
className="flex items-center justify-between bg-muted/50 p-3 rounded"
className="bg-muted/50 p-3 rounded space-y-2"
>
<div className="flex items-center gap-3">
{present ? (
<CheckCircle2 className="h-5 w-5 text-green-600" />
) : (
<Circle className="h-5 w-5 text-muted-foreground" />
)}
<div>
<p className="text-sm font-medium">
Date {date.ordre}
</p>
<p className="text-xs text-muted-foreground">
{new Date(date.dateDebut).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
</div>
<div>
<p className="text-sm font-medium">
Date {date.ordre}
</p>
<p className="text-xs text-muted-foreground">
{new Date(date.dateDebut).toLocaleDateString("fr-FR", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
})}
</p>
</div>
{!present && (
<Button
size="sm"
variant="outline"
onClick={() => handleValiderPresence(inscrit.inscription.id, date.id)}
disabled={validating}
>
{validating ? (
<Loader2 className="h-4 w-4 animate-spin" />
{/* Matin */}
<div className="flex items-center justify-between pl-4">
<div className="flex items-center gap-2">
{presentMatin ? (
<CheckCircle2 className="h-4 w-4 text-green-600" />
) : (
"Valider"
<Circle className="h-4 w-4 text-muted-foreground" />
)}
</Button>
)}
<span className="text-xs">🌅 Matin</span>
</div>
{!presentMatin && (
<Button
size="sm"
variant="outline"
className="h-7 text-xs"
onClick={() => handleValiderPresence(inscrit.inscription.id, date.id, "matin")}
disabled={validating}
>
{validating ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
"Valider"
)}
</Button>
)}
</div>
{/* Après-midi */}
<div className="flex items-center justify-between pl-4">
<div className="flex items-center gap-2">
{presentApresMidi ? (
<CheckCircle2 className="h-4 w-4 text-green-600" />
) : (
<Circle className="h-4 w-4 text-muted-foreground" />
)}
<span className="text-xs">🌆 Après-midi</span>
</div>
{!presentApresMidi && (
<Button
size="sm"
variant="outline"
className="h-7 text-xs"
onClick={() => handleValiderPresence(inscrit.inscription.id, date.id, "apres_midi")}
disabled={validating}
>
{validating ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
"Valider"
)}
</Button>
)}
</div>
</div>
);
}) : (