Louis Mazin 4db822aebe big maj
2026-10-05 17:52:28 +02:00

57 lines
5.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useEffect, useRef, useState } from 'react';
import { BarChart3, Check, LockKeyhole, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import ConfirmDialog from '@/components/ConfirmDialog';
import { notesApi } from '@/api/notes';
import { personLabel } from '@/components/planning/utils';
export default function PollCard({ note, onChange }) {
const poll = note.poll;
const [selected, setSelected] = useState(poll?.my_option_ids || []);
const [busy, setBusy] = useState(false);
const pending = useRef(false);
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const ownKey = JSON.stringify(poll?.my_option_ids || []);
useEffect(() => { setSelected(JSON.parse(ownKey)); }, [ownKey]);
if (!poll) return <p className="text-sm text-destructive">Les données du sondage sont indisponibles.</p>;
const required = poll.vote_mode === 'single' ? 1 : poll.vote_mode === 'exact' ? poll.exact_choices : null;
const closed = poll.is_closed || Boolean(poll.closes_at && new Date(poll.closes_at) <= new Date());
const canVote = poll.can_vote && !closed;
const valid = required === null ? selected.length > 0 : selected.length === required;
const changed = [...selected].sort((a, b) => a - b).join(',') !== [...poll.my_option_ids].sort((a, b) => a - b).join(',');
const toggle = (id) => {
setError(''); setNotice('');
setSelected((current) => poll.vote_mode === 'single' ? [id] : current.includes(id) ? current.filter((item) => item !== id) : [...current, id]);
};
const mutate = async (action) => {
if (pending.current) return; pending.current = true; setBusy(true); setError(''); setNotice('');
try { const updated = action === 'vote' ? await notesApi.vote(note.id, selected) : await notesApi.closePoll(note.id); onChange(updated); setNotice(action === 'vote' ? 'Ton vote est enregistré.' : 'Sondage clôturé.'); }
catch (e) { setError(e.message); } finally { pending.current = false; setBusy(false); }
};
return <div className="space-y-3 border-t pt-3">
<div className="flex flex-wrap items-center gap-1.5"><Badge variant="secondary">{poll.is_anonymous ? <LockKeyhole className="size-3" /> : <Users className="size-3" />}{poll.is_anonymous ? 'Anonyme' : 'Noms visibles'}</Badge><Badge variant="outline">{closed ? 'Clôturé' : required === 1 ? '1 choix' : required ? `${required} choix exactement` : 'Choix multiples'}</Badge></div>
{(() => {
const choices = poll.options.map((option) => <div key={option.id} className={`relative overflow-hidden rounded-lg border transition-colors ${selected.includes(option.id) ? 'border-primary/50 bg-primary/5' : 'border-border hover:bg-muted/40'}`}>
<div aria-hidden="true" className="pointer-events-none absolute inset-y-0 left-0 bg-primary/10 transition-[width]" style={{ width: `${option.percent}%` }} />
<label className={`relative flex items-start gap-2 p-3 text-sm ${canVote ? 'cursor-pointer' : ''}`}>
{poll.vote_mode === 'single'
? <RadioGroupItem value={String(option.id)} className="mt-0.5" aria-labelledby={`poll-${note.id}-option-${option.id}-text`} />
: <Checkbox className="mt-0.5" aria-labelledby={`poll-${note.id}-option-${option.id}-text`} checked={selected.includes(option.id)} disabled={busy || !canVote} onCheckedChange={() => toggle(option.id)} />}
<span id={`poll-${note.id}-option-${option.id}-text`} className="min-w-0 flex-1 break-words">{option.label}</span><span className="shrink-0 tabular-nums text-xs text-muted-foreground">{option.count} · {option.percent}%</span>
</label>
{!poll.is_anonymous && option.voters?.length > 0 && <p className="relative break-words px-3 pb-2 text-xs text-muted-foreground">{option.voters.map(personLabel).join(', ')}</p>}
</div>);
return poll.vote_mode === 'single'
? <RadioGroup name={`poll-${note.id}`} aria-label="Choisir une réponse" value={selected[0] ? String(selected[0]) : ''} disabled={busy || !canVote} onValueChange={(value) => toggle(Number(value))}>{choices}</RadioGroup>
: <div role="group" aria-label="Choisir les réponses" className="space-y-2">{choices}</div>;
})()}
<div className="space-y-1 text-xs text-muted-foreground"><p className="flex items-center gap-1.5"><BarChart3 className="size-3.5" />{poll.total_voters} participant{poll.total_voters > 1 ? 's' : ''} · part des votants ayant choisi chaque réponse</p>{poll.closes_at && <p>Clôture : {new Date(poll.closes_at).toLocaleString('fr-FR')}</p>}{canVote && <p>{required ? `${selected.length} / ${required} choix sélectionné(s).` : `${selected.length} choix sélectionné(s).`} Tu peux modifier ton vote jusqu’à la clôture.</p>}{!canVote && !closed && <p>Ce sondage s’adresse à d’autres personnes. Tu peux consulter les résultats.</p>}</div>
{error && <p role="alert" className="text-sm text-destructive">{error}</p>}{notice && <p role="status" className="text-sm text-emerald-700 dark:text-emerald-300">{notice}</p>}
<div className="flex flex-wrap gap-2">{canVote && <Button type="button" size="sm" disabled={busy || !valid || !changed} onClick={() => mutate('vote')}><Check className="size-4" />{busy ? 'Enregistrement…' : poll.my_option_ids.length ? 'Modifier mon vote' : 'Voter'}</Button>}{note.can_edit && !closed && <ConfirmDialog title="Clôturer le sondage" description="Les résultats resteront visibles. Aucun nouveau vote ni changement de réponse ne sera possible." confirmText="Clôturer" onConfirm={() => mutate('close')}><Button type="button" variant="outline" size="sm" disabled={busy}>Clôturer</Button></ConfirmDialog>}</div>
</div>;
}