diff --git a/apps/admin/src/components/content/ContentReviewControl.tsx b/apps/admin/src/components/content/ContentReviewControl.tsx new file mode 100644 index 0000000..9278b02 --- /dev/null +++ b/apps/admin/src/components/content/ContentReviewControl.tsx @@ -0,0 +1,179 @@ +import { useEffect, useState } from 'react'; +import type { ContentReviewStatus } from '@sauci/shared'; +import { Archive, CheckCircle2, ClipboardCheck, Loader2 } from 'lucide-react'; +import { toast } from 'sonner'; +import { useAuth } from '@/contexts/AuthContext'; +import { Badge } from '@/components/ui/badge'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { + CONTENT_STATUS_LABELS, + type ReviewableContentTable, + updateContentReviewStatus, +} from '@/services/contentReviews'; + +interface ContentReviewControlProps { + table: ReviewableContentTable; + entityId: string; + entityLabel: string; + status?: ContentReviewStatus | null; + reason?: string | null; + onChanged: () => void | Promise; + compact?: boolean; +} + +const badgeClasses: Record = { + unreviewed: 'border-amber-500 text-amber-500', + allowed: 'border-green-500 text-green-500', + archived: 'border-slate-500 text-slate-400', +}; + +const statusIcons: Record = { + unreviewed: ClipboardCheck, + allowed: CheckCircle2, + archived: Archive, +}; + +export function ContentReviewControl({ + table, + entityId, + entityLabel, + status, + reason, + onChanged, + compact = false, +}: ContentReviewControlProps) { + const { isSuperAdmin } = useAuth(); + const resolvedStatus = status ?? 'unreviewed'; + const StatusIcon = statusIcons[resolvedStatus]; + const [open, setOpen] = useState(false); + const [nextStatus, setNextStatus] = useState(resolvedStatus); + const [nextReason, setNextReason] = useState(reason ?? ''); + const [saving, setSaving] = useState(false); + + useEffect(() => { + if (!open) { + setNextStatus(resolvedStatus); + setNextReason(reason ?? ''); + } + }, [open, reason, resolvedStatus]); + + const save = async () => { + if (!nextReason.trim()) { + toast.error('A review reason is required'); + return; + } + + setSaving(true); + try { + await updateContentReviewStatus(table, entityId, nextStatus, nextReason); + toast.success(`${entityLabel} marked ${CONTENT_STATUS_LABELS[nextStatus].toLowerCase()}`); + setOpen(false); + await onChanged(); + } catch (error) { + toast.error('Failed to update catalogue status'); + console.error(error); + } finally { + setSaving(false); + } + }; + + return ( +
+ + + {CONTENT_STATUS_LABELS[resolvedStatus]} + + + {isSuperAdmin && ( + <> + + + + + Review catalogue status + + This records a reversible editorial decision for {entityLabel}. + Current customer apps do not filter this status yet. + + + +
+
+ + +
+ +
+ +