All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
תיקון הגישה: יישום מלא ונאמן של עיצוב-המוקאפים המאושרים (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>
241 lines
8.1 KiB
TypeScript
241 lines
8.1 KiB
TypeScript
"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>
|
||
);
|
||
}
|