57 lines
5.7 KiB
JavaScript
57 lines
5.7 KiB
JavaScript
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>;
|
||
}
|