Files
legal-ai/web-ui/src/components/methodology/content-checklists-panel.tsx
Chaim f3b075d282
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
תיקון הגישה: יישום מלא ונאמן של עיצוב-המוקאפים המאושרים (Claude Design) על כל
הדפים — שינוי-הרכב אמיתי פר-מוקאפ, לא ליטוש-טוקנים. כל hook/query/mutation/טאב/
טופס/נתון נשמר (אומת: tsc נקי + בדיקת-נוכחות hooks קריטיים; 0 פונקציונליות נמחקה).

דפים (← מוקאפ):
- בית — לוח: KPI + "תיקים לפי סטטוס" (bars) + כרטיס-אישורים + CTA כפול.
- ארכיון — filter-bar שטוח + טבלה נקייה + צ'יפי-סוג/תוצאה.
- הערות יו״ר — פריסה דו-טורית + טופס-הוספה חי + כרטיסי-הערה.
- ספריית-פסיקה — tabs קו-תחתון + כרטיסי-תוצאה halacha/קטע + AuthorityBadge.
- דף-תקדים — באנר-meta parchment + דו-טורי + provenance pills.
- פסיקה-חסרה — pill פתוחים + צ'יפי-סטטוס + CTA העלאה.
- יומונים — אזור-העלאה מקווקו + כרטיסי-digest + "ממתין" כתווית פסיבית.
- גרף — פאנל-צד שכבות/אנליטיקה + canvas parchment.
- אימון-סגנון — פורטרט: banner + KPI + אנטומיה + ביטויי-חתימה.
- מתודולוגיה — עורך-צ'קליסט + "חל על:" + canon chip.
- מיומנויות/סקריפטים — טבלאות אמיתיות + צ'יפי-סטטוס.
- הגדרות — sidenav דו-טורי + env-rows עם "ממתין ל-redeploy".
- דף-תיק — באנר-תיק parchment + tabs + timeline + "פתח עורך החלטה".
- תפעול — SectionHeaders + טבלת-שירותים + כרטיסי-שער gold-wash.
- compose — באנר-תיק + SOT pill + פריסה דו-טורית + "השלמה והעברה".

תיקונים שלי אחרי הסוכנים: documents-panel (הוצאת רכיב Shell מ-render — React
Compiler), scripts useMemo deps. /approvals כבר נבנה מחדש נאמנה (commit קודם).

בדיקות: npx tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109 קיים-מראש).
שימור-פונקציונליות אומת. CI Docker build = שער סופי לפני deploy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 23:00:25 +00:00

241 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect } from "react";
import { Card } 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",
];
// MET-6 (INV-IA6/INV-IA5): which case selects each checklist — mirrors the
// server's get_content_checklist() routing (lessons.py:580-622) so the chair
// sees *when* a checklist is consumed, not five unlabelled types.
const CHECKLIST_APPLIES: Record<string, string> = {
licensing_substantive: "ברירת-מחדל — כל ערר רישוי שאינו נופל לקטגוריה אחרת",
licensing_threshold: "עררי רישוי בנושא סמכות / סף / סילוק-על-הסף / זכות-ערר",
licensing_property: "עררי רישוי בנושא תימוכין קנייניים / בעלות / הסכמת-דיירים",
tama38: 'עררים בנושא תמ"א 38 / חיזוק',
betterment_levy: "תיקי היטל השבחה (8xxx)",
};
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;
// eslint-disable-next-line react-hooks/set-state-in-effect -- sync from server
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>
);
}
const itemCount = current
? current.draft.split("\n").filter((l) => /^\s*-\s*\[/.test(l)).length
: 0;
return (
<div className="space-y-[18px]">
{/* Type buttons — gold active (mockup 13) */}
<div className="flex gap-2.5 flex-wrap">
{items.map((item) => {
const isActive = active === item.key;
return (
<button
key={item.key}
type="button"
onClick={() => {
setActive(item.key);
setPreview(false);
}}
className={
isActive
? "rounded-lg border border-gold bg-gold px-4 py-2 text-[0.84rem] font-semibold text-white"
: "rounded-lg border border-rule bg-surface px-4 py-2 text-[0.84rem] font-medium text-ink-soft hover:border-gold/50"
}
>
{item.label}
{item.isOverride && (
<Badge
variant="secondary"
className={`text-[9px] ms-1.5 px-1 ${isActive ? "bg-white/20 text-white" : ""}`}
>
מותאם
</Badge>
)}
</button>
);
})}
</div>
{/* "חל על:" explainer band — gold-wash (mockup 13) */}
{current && CHECKLIST_APPLIES[current.key] && (
<div className="flex items-baseline gap-2 rounded-lg border border-rule bg-gold-wash px-4 py-2.5 text-[0.84rem] text-ink-soft">
<b className="text-gold-deep font-semibold whitespace-nowrap">חל על:</b>
<span>{CHECKLIST_APPLIES[current.key]}</span>
</div>
)}
{/* Editor — framed card with header chip + parchment editor + footer */}
{current && (
<Card className="border-rule shadow-sm overflow-hidden p-0 gap-0">
<div className="flex items-center gap-2.5 px-[18px] py-3.5 border-b border-rule-soft">
<h2 className="text-[0.95rem] font-semibold text-navy m-0">
צ׳קליסט תוכן {current.label}
</h2>
<span
className={`ms-auto rounded-full text-xs font-semibold px-2.5 py-0.5 ${
current.isOverride
? "bg-gold-wash text-gold-deep border border-rule"
: "bg-info-bg text-info"
}`}
>
{current.isOverride ? "מותאם" : "ידני"}
</span>
<Button
size="sm"
variant="ghost"
onClick={() => setPreview(!preview)}
className="text-xs shrink-0 h-7"
>
{preview ? (
<EyeOff className="w-3.5 h-3.5 ms-1" />
) : (
<Eye className="w-3.5 h-3.5 ms-1" />
)}
{preview ? "עריכה" : "תצוגה מקדימה"}
</Button>
</div>
{preview ? (
<div className="p-[18px] bg-parchment max-h-[500px] overflow-y-auto border-b border-rule-soft">
<Markdown content={current.draft} />
</div>
) : (
<Textarea
value={current.draft}
onChange={(e) => updateDraft(e.target.value)}
className="min-h-[340px] rounded-none border-0 border-b border-rule-soft bg-parchment font-mono text-[0.84rem] leading-[1.95] text-ink-soft focus-visible:ring-0 resize-y"
dir="rtl"
/>
)}
<div className="flex items-center gap-2.5 px-[18px] py-3.5">
<Button
disabled={!current.dirty || update.isPending}
onClick={handleSave}
className="bg-gold text-white hover:bg-gold-deep"
>
{update.isPending ? (
<Loader2 className="w-3.5 h-3.5 animate-spin ms-1" />
) : (
<Save className="w-3.5 h-3.5 ms-1" />
)}
שמור
</Button>
{current.isOverride && (
<Button
variant="outline"
disabled={reset.isPending}
onClick={handleReset}
className="border-rule text-navy"
>
<RotateCcw className="w-3.5 h-3.5 ms-1" />
אפס לברירת-מחדל
</Button>
)}
<span className="ms-auto text-[0.78rem] text-ink-muted tabular-nums">
{itemCount} פריטים
</span>
</div>
</Card>
)}
</div>
);
}