Add methodology settings page with golden ratios, discussion rules, and checklists
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
New /methodology page with 3 tabs for viewing and editing decision
writing methodology. Uses DB override pattern: hardcoded Python
constants serve as defaults, edits saved to appeal_type_rules table,
delete restores default.
Backend: 3 generic endpoints (GET/PUT/DELETE /api/methodology/{category}/{key})
with validation per category type.
Frontend: methodology.ts hooks, GoldenRatiosPanel (number inputs per
outcome/section), DiscussionRulesPanel (accordion with textarea per
rule), ContentChecklistsPanel (markdown editor with preview toggle).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
181
web-ui/src/components/methodology/content-checklists-panel.tsx
Normal file
181
web-ui/src/components/methodology/content-checklists-panel.tsx
Normal file
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Markdown } from "@/components/ui/markdown";
|
||||
import {
|
||||
useMethodology,
|
||||
useUpdateMethodology,
|
||||
useResetMethodology,
|
||||
} from "@/lib/api/methodology";
|
||||
import { toast } from "sonner";
|
||||
import { Save, RotateCcw, Eye, EyeOff, Loader2 } from "lucide-react";
|
||||
|
||||
const CHECKLIST_LABELS: Record<string, string> = {
|
||||
licensing_substantive: "ערר רישוי מהותי",
|
||||
licensing_threshold: "ערר רישוי סף/סמכות",
|
||||
licensing_property: "ערר רישוי קנייני",
|
||||
tama38: "תמ\"א 38",
|
||||
betterment_levy: "היטל השבחה",
|
||||
};
|
||||
|
||||
const CHECKLIST_ORDER = [
|
||||
"licensing_substantive",
|
||||
"licensing_threshold",
|
||||
"licensing_property",
|
||||
"tama38",
|
||||
"betterment_levy",
|
||||
];
|
||||
|
||||
type ChecklistItem = {
|
||||
key: string;
|
||||
label: string;
|
||||
original: string;
|
||||
draft: string;
|
||||
isOverride: boolean;
|
||||
dirty: boolean;
|
||||
};
|
||||
|
||||
export function ContentChecklistsPanel() {
|
||||
const { data, isLoading } = useMethodology<string>("content_checklists");
|
||||
const update = useUpdateMethodology("content_checklists");
|
||||
const reset = useResetMethodology("content_checklists");
|
||||
const [items, setItems] = useState<ChecklistItem[]>([]);
|
||||
const [active, setActive] = useState(CHECKLIST_ORDER[0]);
|
||||
const [preview, setPreview] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data?.items) return;
|
||||
setItems(
|
||||
CHECKLIST_ORDER
|
||||
.filter((k) => k in data.items)
|
||||
.map((key) => ({
|
||||
key,
|
||||
label: CHECKLIST_LABELS[key] ?? key,
|
||||
original: data.items[key].value,
|
||||
draft: data.items[key].value,
|
||||
isOverride: data.items[key].is_override,
|
||||
dirty: false,
|
||||
})),
|
||||
);
|
||||
}, [data]);
|
||||
|
||||
const current = items.find((i) => i.key === active);
|
||||
|
||||
const updateDraft = (text: string) => {
|
||||
setItems((prev) =>
|
||||
prev.map((i) =>
|
||||
i.key === active
|
||||
? { ...i, draft: text, dirty: text !== i.original }
|
||||
: i,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
if (!current) return;
|
||||
update.mutate(
|
||||
{ key: current.key, value: current.draft },
|
||||
{
|
||||
onSuccess: () => toast.success(`${current.label} נשמר`),
|
||||
onError: () => toast.error("שגיאה בשמירה"),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
if (!current) return;
|
||||
reset.mutate(current.key, {
|
||||
onSuccess: () => toast.success(`${current.label} אופס`),
|
||||
onError: () => toast.error("שגיאה באיפוס"),
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12 text-ink-faint">
|
||||
<Loader2 className="w-5 h-5 animate-spin ml-2" />
|
||||
טוען...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{/* Tab selector */}
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{items.map((item) => (
|
||||
<Button
|
||||
key={item.key}
|
||||
size="sm"
|
||||
variant={active === item.key ? "default" : "outline"}
|
||||
onClick={() => { setActive(item.key); setPreview(false); }}
|
||||
className="text-xs"
|
||||
>
|
||||
{item.label}
|
||||
{item.isOverride && (
|
||||
<Badge variant="secondary" className="text-[9px] mr-1.5 px-1">
|
||||
מותאם
|
||||
</Badge>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Editor / Preview */}
|
||||
{current && (
|
||||
<Card className="border-rule">
|
||||
<CardContent className="px-5 py-4 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-navy">{current.label}</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => setPreview(!preview)}
|
||||
className="text-xs"
|
||||
>
|
||||
{preview ? <EyeOff className="w-3.5 h-3.5 ml-1" /> : <Eye className="w-3.5 h-3.5 ml-1" />}
|
||||
{preview ? "עריכה" : "תצוגה מקדימה"}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{preview ? (
|
||||
<div className="border border-rule rounded-md p-4 bg-sand-soft/30 max-h-[500px] overflow-y-auto">
|
||||
<Markdown content={current.draft} />
|
||||
</div>
|
||||
) : (
|
||||
<Textarea
|
||||
value={current.draft}
|
||||
onChange={(e) => updateDraft(e.target.value)}
|
||||
className="min-h-[400px] font-mono text-sm leading-relaxed"
|
||||
dir="rtl"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button size="sm" disabled={!current.dirty || update.isPending} onClick={handleSave}>
|
||||
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />}
|
||||
שמור
|
||||
</Button>
|
||||
{current.isOverride && (
|
||||
<Button size="sm" variant="outline" disabled={reset.isPending} onClick={handleReset}>
|
||||
<RotateCcw className="w-3 h-3 ml-1" />
|
||||
איפוס לברירת מחדל
|
||||
</Button>
|
||||
)}
|
||||
<Badge
|
||||
variant={current.isOverride ? "default" : "secondary"}
|
||||
className="text-[10px] mr-auto"
|
||||
>
|
||||
{current.isOverride ? "מותאם" : "ברירת מחדל"}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user