import { useEffect, useRef, useState } from 'react'; import { Check, ChevronDown, ClipboardList, Globe2, Info, LockKeyhole, MessageCircleQuestionMark, Pencil, Plus, RotateCcw, Search, StickyNote, Trash2, Users, } from 'lucide-react'; import { notesApi } from '../api/notes'; 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, DialogFooter, 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 = { 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', 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 [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, ) => { setEditing(note); setForm( note ? { title: note.title, content: note.content, kind: note.kind, recipient_ids: note.recipients?.map( (person) => person.id, ) ?? [], is_public: note.is_public !== false, } : emptyForm(), ); setFormError(''); setRecipientPopoverOpen(false); setDialogOpen(true); }; /* * ============================ * Destinataires * ============================ */ const allRecipientIds = recipients.map( (person) => person.id, ); const allRecipientsSelected = recipients.length > 0 && recipients.every( (person) => form.recipient_ids.includes( person.id, ), ); 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 toggleAllRecipients = () => { /* * "Tout le monde" n'est disponible * que pour une publication publique. */ if (!form.is_public) { return; } setForm( (current) => ({ ...current, recipient_ids: allRecipientsSelected ? [] : allRecipientIds, }), ); }; const recipientButtonLabel = () => { const count = form.recipient_ids.length; /* * On n'affiche "Tout le monde" * que pour une publication publique. * * En privé, même si tous les comptes * ont été sélectionnés manuellement, * on affiche le nombre de personnes. */ if ( form.is_public && count > 0 && allRecipientsSelected ) { return 'Tout le monde'; } if (count === 0) { return '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) => { /* * Si "Tout le monde" était sélectionné * en mode public et que l'on passe * en privé, on vide les destinataires. * * Cela oblige à choisir explicitement * les personnes autorisées à voir * la publication privée. */ const everyoneWasSelected = recipients.length > 0 && recipients.every( (person) => current.recipient_ids.includes( person.id, ), ); return { ...current, is_public: checked, recipient_ids: !checked && everyoneWasSelected ? [] : current.recipient_ids, }; }, ); setFormError(''); }; /* * ============================ * Enregistrement * ============================ */ const save = async (event) => { event.preventDefault(); if ( mutationInFlight.current ) { return; } if ( !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(), }; 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 } > {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 && ( Privé )}
{/* Titre */}

{note.title}

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

{note.content}

)} {/* Destinataires */}
{note.recipients?.length ? `Pour ${note.recipients .map( userLabel, ) .join(', ')}` : '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'} Sélectionnez les personnes concernées et choisissez la visibilité de la publication. {formError && ( {formError} )} {recipientsError && ( Annuaire indisponible :{' '} {recipientsError} )} {/* Type */}
setForm({ ...form, kind: event.target.value, }) } > {Object.entries( kinds, ).map( ([key, kind]) => ( {kind.label} ), )}
{/* Destinataires */}
{/* * "Tout le monde" n'est proposé * que pour les publications publiques. */} {form.is_public && (
)} {/* Utilisateurs */}
{recipients.length === 0 ? (

Aucun utilisateur disponible

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

En privé, choisissez explicitement les personnes autorisées à voir la publication.

)}
{/* Public / privé */}

{form.is_public ? "Visible par toute l'équipe" : 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 */}