import { useEffect, useRef, useState } from 'react'; import { useAuth } from '@/context/useAuth'; import { can } from '@/lib/permissions'; import { Check, BarChart3, ChevronDown, ClipboardList, Globe2, Info, LockKeyhole, MessageCircleQuestionMark, Pencil, Plus, RotateCcw, Search, StickyNote, Trash2, Users, } from 'lucide-react'; import { notesApi } from '../api/notes'; import PollCard from '@/components/notes/PollCard'; import PollEditor from '@/components/notes/PollEditor'; import { emptyPoll, pollForm } from '@/components/notes/pollUtils'; import MainCard from '@/components/MainCard'; import LoadingCard from '@/components/LoadingCard'; import ConfirmDialog from '@/components/ConfirmDialog'; import { Alert, AlertDescription, } from '@/components/ui/alert'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Card, CardContent, } from '@/components/ui/card'; import { Checkbox } from '@/components/ui/checkbox'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { NativeSelect, NativeSelectOption, } from '@/components/ui/native-select'; import { Popover, PopoverContent, PopoverTrigger, } from '@/components/ui/popover'; import { Switch } from '@/components/ui/switch'; import { Textarea } from '@/components/ui/textarea'; const kinds = { poll: { label: 'Sondage', icon: BarChart3, color: 'text-cyan-700 bg-cyan-50 dark:text-cyan-300 dark:bg-cyan-950/40' }, task: { label: 'Tâche', icon: ClipboardList, color: 'text-blue-700 bg-blue-50 dark:text-blue-300 dark:bg-blue-950/40', }, info: { label: 'Information', icon: Info, color: 'text-emerald-700 bg-emerald-50 dark:text-emerald-300 dark:bg-emerald-950/40', }, memo: { label: 'Mémo', icon: StickyNote, color: 'text-amber-700 bg-amber-50 dark:text-amber-300 dark:bg-amber-950/40', }, sugg: { label: 'Suggestion', icon: MessageCircleQuestionMark, color: 'text-violet-700 bg-violet-50 dark:text-violet-300 dark:bg-violet-950/40', }, }; const emptyForm = () => ({ title: '', content: '', kind: 'memo', poll_settings: emptyPoll(), recipient_ids: [], is_public: true, }); const userLabel = (user) => user ? `${user.first_name} ${user.last_name}`.trim() || user.username : 'Compte supprimé'; const dateLabel = (value) => new Date(value).toLocaleString( 'fr-FR', { dateStyle: 'medium', timeStyle: 'short', }, ); export default function NotesPage() { const { user } = useAuth(); const canCreate = can(user, 'create_publication'); const [result, setResult] = useState({ count: 0, results: [], next: null, previous: null, }); const [ recipients, setRecipients, ] = useState([]); const [ recipientsError, setRecipientsError, ] = useState(''); const [ filters, setFilters, ] = useState({ kind: '', scope: 'all', state: 'all', search: '', }); const [ searchText, setSearchText, ] = useState(''); const [page, setPage] = useState(1); const [ revision, setRevision, ] = useState(0); const [ isLoading, setIsLoading, ] = useState(true); const [error, setError] = useState(''); const [notice, setNotice] = useState(''); const [ dialogOpen, setDialogOpen, ] = useState(false); const [ recipientPopoverOpen, setRecipientPopoverOpen, ] = useState(false); const [ editing, setEditing, ] = useState(null); const [ form, setForm, ] = useState(emptyForm); const [ formError, setFormError, ] = useState(''); const [ saving, setSaving, ] = useState(false); const [ busyId, setBusyId, ] = useState(null); const mutationInFlight = useRef(false); /* * ============================ * Chargement des publications * ============================ */ useEffect(() => { const controller = new AbortController(); notesApi .list( { ...filters, page, }, { signal: controller.signal, }, ) .then((data) => { if (!controller.signal.aborted) { setResult(data); setError(''); } }) .catch((requestError) => { if ( controller.signal.aborted ) { return; } if ( requestError.response?.status === 404 && page > 1 ) { setPage(1); } else { setError( requestError.message, ); } }) .finally(() => { if ( !controller.signal.aborted ) { setIsLoading(false); } }); return () => controller.abort(); }, [ filters, page, revision, ]); /* * ============================ * Annuaire * ============================ */ useEffect(() => { const controller = new AbortController(); notesApi .recipients({ signal: controller.signal, }) .then((data) => { if (!controller.signal.aborted) { setRecipients(data); setRecipientsError(''); } }) .catch((requestError) => { if ( !controller.signal.aborted ) { setRecipientsError( requestError.message, ); } }); return () => controller.abort(); }, [revision]); const reload = () => { setIsLoading(true); setRevision( (value) => value + 1, ); }; const changeFilters = ( changes, ) => { setIsLoading(true); setPage(1); setFilters( (current) => ({ ...current, ...changes, }), ); }; /* * ============================ * Éditeur * ============================ */ const openEditor = ( note = null, ) => { if (!note && !canCreate) return; setEditing(note); setForm( note ? { title: note.title, content: note.content, kind: note.kind, poll_settings: pollForm(note.poll), recipient_ids: note.recipients?.map( (person) => person.id, ) ?? [], is_public: note.is_public !== false, } : emptyForm(), ); setFormError(''); setRecipientPopoverOpen(false); setDialogOpen(true); }; /* * ============================ * Destinataires * ============================ */ const toggleRecipient = ( id, ) => { setForm( (current) => { const selected = current.recipient_ids.includes( id, ); return { ...current, recipient_ids: selected ? current.recipient_ids.filter( (recipientId) => recipientId !== id, ) : [ ...current.recipient_ids, id, ], }; }, ); }; const recipientButtonLabel = () => { const count = form.recipient_ids.length; if (count === 0) { return form.is_public ? 'Toute l’équipe' : 'Aucun destinataire'; } if (count === 1) { const person = recipients.find( (recipient) => recipient.id === form.recipient_ids[0], ); return person ? userLabel(person) : '1 personne'; } return `${count} personnes`; }; /* * ============================ * Public / privé * ============================ */ const changeVisibility = ( checked, ) => { setForm((current) => ({ ...current, is_public: checked, })); setRecipientPopoverOpen(false); setFormError(''); }; /* * ============================ * Enregistrement * ============================ */ const save = async (event) => { event.preventDefault(); if (!editing && !canCreate) { setFormError('La création de publications n’est pas activée pour ton compte.'); return; } if ( mutationInFlight.current ) { return; } if ( form.kind !== 'poll' && !form.is_public && form.recipient_ids.length === 0 ) { setFormError( 'Une publication privée doit avoir au moins un destinataire.', ); return; } mutationInFlight.current = true; setSaving(true); setFormError(''); const payload = { ...form, title: form.title.trim(), content: form.content.trim(), }; if (form.kind === 'poll') { payload.is_public = true; payload.recipient_ids = []; payload.poll_settings = { ...form.poll_settings, options: form.poll_settings.options.map((option) => option.trim()) }; } else { delete payload.poll_settings; } try { if (editing) { await notesApi.update( editing.id, payload, ); } else { await notesApi.create( payload, ); } setDialogOpen(false); setNotice( editing ? 'Publication modifiée.' : 'Publication créée.', ); setPage(1); reload(); } catch (requestError) { setFormError( requestError.message, ); } finally { mutationInFlight.current = false; setSaving(false); } }; /* * ============================ * Suppression / complétion * ============================ */ const mutateNote = async ( note, operation, ) => { if ( mutationInFlight.current ) { return; } mutationInFlight.current = true; setBusyId(note.id); setError(''); setNotice(''); try { if ( operation === 'delete' ) { await notesApi.remove( note.id, ); setNotice( 'Publication supprimée.', ); setPage(1); } else { await notesApi.complete( note.id, !note.is_done, ); setNotice( note.is_done ? 'Tâche remise à faire.' : 'Tâche terminée.', ); } reload(); } catch (requestError) { setError( requestError.message, ); } finally { mutationInFlight.current = false; setBusyId(null); } }; if (isLoading) { return ( ); } return ( openEditor() } > Nouvelle publication } > {!canCreate &&

Tu peux consulter les publications et répondre aux sondages. Pour publier, demande l’accès à un responsable.

} {error && ( {error} )} {notice && (

{notice}

)} {/* ===================== */} {/* FILTRES */} {/* ===================== */}
changeFilters({ scope: event.target.value, }) } > Toute l’équipe Pour moi Mes publications
changeFilters({ kind: event.target.value, state: 'all', }) } > Tous les types {Object.entries( kinds, ).map( ([key, kind]) => ( {kind.label} ), )}
changeFilters({ state: event.target.value, }) } > Tous les états À faire Terminées
{ event.preventDefault(); changeFilters({ search: searchText.trim(), }); }} >
setSearchText( event.target.value, ) } placeholder="Titre ou contenu…" />
{/* ===================== */} {/* PUBLICATIONS */} {/* ===================== */} {error ? null : result.results.length === 0 ? (

Aucune publication à afficher

Créez une tâche, partagez une information ou ajustez les filtres.

) : ( <>
{result.results.map( (note) => { const kind = kinds[note.kind]; const Icon = kind.icon; return ( {/* Badges */}
{kind.label} {note.kind === 'task' && ( {note.is_done ? 'Terminée' : 'À faire'} )} {note.is_public ? ( ) : ( )} {note.is_public ? 'Public' : 'Privé'}
{/* Titre */}

{note.title}

{/* Contenu */} {note.content && (

{note.content}

)} {note.kind === 'poll' && setResult((current) => ({ ...current, results: current.results.map((item) => item.id === updated.id ? updated : item) }))} />} {/* Destinataires */}
{note.recipients?.length ? `Pour ${note.recipients .map( userLabel, ) .join(', ')}` : note.is_public ? 'Pour toute l’équipe' : 'Aucun destinataire'}
{/* Métadonnées */}

Par{' '} {userLabel( note.author, )} {' · '}

{note.completed_at && (

Terminée le{' '} {dateLabel( note.completed_at, )}

)}
{/* Actions */} {(note.can_complete || note.can_edit) && (
{note.can_complete && ( )} {note.can_edit && ( <> mutateNote( note, 'delete', ) } > )}
)}
); }, )}
{(result.next || result.previous) && (
Page {page}
)} )}
{/* ========================= */} {/* DIALOGUE */} {/* ========================= */} { if (!saving) { setDialogOpen(open); } }} >
{editing ? 'Modifier la publication' : 'Nouvelle publication'} {form.kind === 'poll' ? 'Le sondage est automatiquement public et accessible à toute l’équipe.' : 'Choisis à qui tu t’adresses et qui peut voir la publication. Une publication publique reste visible par toute l’équipe, même lorsqu’elle est destinée à certaines personnes.'} {formError && ( {formError} )} {form.kind !== 'poll' && recipientsError && ( Annuaire indisponible :{' '} {recipientsError} )} {/* Type */}
{ const kind = event.target.value; setForm({ ...form, kind, ...(kind === 'poll' ? { is_public: true, recipient_ids: [] } : {}), }); if (kind === 'poll') setRecipientPopoverOpen(false); }} > {Object.entries( kinds, ).map( ([key, kind]) => ( {kind.label} ), )}
{/* Destinataires */}
{recipientButtonLabel()} } />
{/* Utilisateurs */}
{recipients.length === 0 ? (

Aucun utilisateur disponible

) : ( recipients.map( (person) => { const checked = form.recipient_ids.includes( person.id, ); return ( ); }, ) )}

{form.is_public ? 'Facultatif : sélectionnez les personnes concernées. Sans sélection, la publication s’adresse à toute l’équipe, y compris les futurs inscrits.' : 'En privé, choisissez les personnes autorisées à voir la publication.'}

{/* Public / privé */}

{form.is_public ? "Visible par toute l’équipe, y compris les futurs utilisateurs" : form.recipient_ids.length ? 'Visible uniquement par vous et les destinataires' : 'Sélectionnez au moins un destinataire'}

{/* Titre */}
setForm({ ...form, title: event.target.value, }) } placeholder="Ex. Préparer la commande de demain" />
{/* Contenu */}