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:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}) : (
|
||||
|
||||
Reference in New Issue
Block a user