import { useMemo, useState } from 'react' import { format } from 'date-fns' import { fr } from 'date-fns/locale' import { supa } from '../lib/supabase' import { humanize, mutate, run } from '../lib/db' import { useApp } from '../lib/AppContext' import { useFeedback } from '../lib/feedback' import { notifyBug } from '../lib/notify' import { Bug, BugSeverity, BugStatus, BUG_STATUS_LABELS, BUG_STATUS_ORDER, Pole, SEVERITY_HINTS, SEVERITY_LABELS, SEVERITY_ORDER, severityBadge } from '../lib/types' import { Badge, Button, Field, Input } from './ui' import { Modal, ModalActions } from './Modal' import { Select } from './Select' import { AssigneePicker, PoleChips } from './people' import { AttachmentPanel, CommentThread, TimeSection } from './EntityExtras' interface Draft { title: string description: string severity: BugSeverity status: BugStatus poles: Pole[] assignees: string[] } function draftOf(bug: Bug | null): Draft { return { title: bug?.title ?? '', description: bug?.description ?? '', severity: bug?.severity ?? 'medium', status: bug?.status ?? 'open', poles: bug?.poles ?? [], assignees: bug?.assignee_ids ?? [] } } export default function BugModal({ bug, projectId, onClose, onSaved }: { bug: Bug | null projectId: string onClose: () => void onSaved: () => Promise | void }): JSX.Element { const { profile, members, membersLoading, memberById } = useApp() const { toast, confirm } = useFeedback() const initial = useMemo(() => draftOf(bug), [bug]) const [d, setD] = useState(initial) const [busy, setBusy] = useState(false) const patch = (p: Partial): void => setD((cur) => ({ ...cur, ...p })) const dirty = useMemo(() => JSON.stringify(d) !== JSON.stringify(initial), [d, initial]) const save = async (): Promise => { if (!d.title.trim()) { toast('Donne un titre au bug.', 'error') return } setBusy(true) const payload = { project_id: projectId, title: d.title.trim(), description: d.description.trim() || null, severity: d.severity, status: d.status, poles: d.poles, assignee_ids: d.assignees } try { let entityId = bug?.id ?? null if (bug) { await mutate(supa().from('bugs').update(payload).eq('id', bug.id)) } else { const created = await run<{ id: string }>( supa() .from('bugs') .insert({ ...payload, reporter_id: profile?.id ?? null }) .select('id') .single() ) entityId = created?.id ?? null } if (entityId) { const newPoles = d.poles.filter((p) => !(bug?.poles ?? []).includes(p)) const newAssignees = d.assignees.filter((a) => !(bug?.assignee_ids ?? []).includes(a)) if (newPoles.length || newAssignees.length) { await notifyBug({ projectId, bugId: entityId, title: d.title.trim(), members, poles: newPoles, assigneeIds: newAssignees, actorId: profile?.id ?? null, isNew: !bug }) } } await onSaved() toast(bug ? 'Bug enregistré.' : 'Bug signalé.', 'success') onClose() } catch (e) { toast(humanize(e), 'error') } finally { setBusy(false) } } const remove = async (): Promise => { if (!bug) return const ok = await confirm({ title: 'Supprimer ce bug ?', message: `« ${bug.title} » et tout ce qui y est rattaché (temps, discussion, captures) seront supprimés.`, danger: true, confirmLabel: 'Supprimer' }) if (!ok) return setBusy(true) try { await mutate(supa().from('bugs').delete().eq('id', bug.id)) await onSaved() toast('Bug supprimé.', 'success') onClose() } catch (e) { toast(humanize(e), 'error') setBusy(false) } } /** Passe le bug en résolu en un clic : le geste le plus fréquent. */ const resolve = async (): Promise => { if (!bug) return setBusy(true) try { await mutate(supa().from('bugs').update({ status: 'resolved' }).eq('id', bug.id)) await onSaved() toast('Bug marqué comme résolu.', 'success') onClose() } catch (e) { toast(humanize(e), 'error') setBusy(false) } } const reporter = memberById(bug?.reporter_id) const subtitle = bug ? ( Signalé {reporter ? `par ${reporter.full_name} ` : ''} le {format(new Date(bug.created_at), 'd MMM yyyy', { locale: fr })} ) : null return ( {bug && bug.status !== 'resolved' && bug.status !== 'closed' && ( )} } >
{({ id }) => ( patch({ title: e.target.value })} /> )} {({ id }) => (