feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
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>
This commit is contained in:
2026-06-11 23:00:25 +00:00
parent c53ef9a7c4
commit f3b075d282
32 changed files with 2925 additions and 1799 deletions

View File

@@ -26,7 +26,8 @@ import {
TableRow,
} from "@/components/ui/table";
import { useCases, useRestoreCase, type Case } from "@/lib/api/cases";
import { APPEAL_SUBTYPE_LABELS } from "@/lib/practice-area";
import { subtypeOf } from "@/components/cases/appeal-type-bars";
import { APPEAL_SUBTYPE_LABELS, type AppealSubtype } from "@/lib/practice-area";
function formatDate(iso?: string | null) {
if (!iso) return "—";
@@ -41,6 +42,20 @@ function formatDate(iso?: string | null) {
}
}
// type chip styling per mockup 05 (.t-lic / .t-bet / .t-comp)
const TYPE_CHIP: Record<string, string> = {
building_permit: "bg-info-bg text-info",
betterment_levy: "bg-gold-wash text-gold-deep border border-rule",
compensation_197: "bg-rule-soft text-ink-soft",
};
// outcome chip styling per mockup 05 (.r-acc / .r-rej / .r-part)
const OUTCOME_CHIP: Record<string, { label: string; cls: string }> = {
full_acceptance: { label: "התקבל", cls: "bg-success-bg text-success" },
partial_acceptance: { label: "חלקי", cls: "bg-warn-bg text-warn" },
rejection: { label: "נדחה", cls: "bg-danger-bg text-danger" },
};
function RestoreButton({ caseNumber }: { caseNumber: string }) {
const restore = useRestoreCase(caseNumber);
return (
@@ -77,7 +92,7 @@ function RestoreButton({ caseNumber }: { caseNumber: string }) {
const columns: ColumnDef<Case>[] = [
{
accessorKey: "case_number",
header: "מס׳ ערר",
header: "מספר ערר",
cell: ({ row }) => (
<Link
href={`/cases/${row.original.case_number}`}
@@ -91,20 +106,42 @@ const columns: ColumnDef<Case>[] = [
accessorKey: "title",
header: "כותרת",
cell: ({ row }) => (
<div className="text-ink max-w-[420px] truncate" title={row.original.title}>
<div className="text-ink-soft max-w-[420px] truncate" title={row.original.title}>
{row.original.title}
</div>
),
},
{
accessorKey: "appeal_subtype",
header: "תחום",
header: "סוג",
cell: ({ row }) => {
const s = row.original.appeal_subtype;
const s = subtypeOf(row.original);
if (!s || s === "unknown")
return <span className="text-ink-muted"></span>;
return (
<span className="text-ink-soft text-sm">{APPEAL_SUBTYPE_LABELS[s]}</span>
<span
className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold whitespace-nowrap ${
TYPE_CHIP[s] ?? "bg-rule-soft text-ink-soft"
}`}
>
{APPEAL_SUBTYPE_LABELS[s as AppealSubtype]}
</span>
);
},
},
{
accessorKey: "expected_outcome",
header: "תוצאה",
cell: ({ row }) => {
const o = row.original.expected_outcome;
const chip = o ? OUTCOME_CHIP[o] : undefined;
if (!chip) return <span className="text-ink-muted"></span>;
return (
<span
className={`inline-block rounded-full px-2.5 py-0.5 text-[0.72rem] font-semibold whitespace-nowrap ${chip.cls}`}
>
{chip.label}
</span>
);
},
},
@@ -112,7 +149,7 @@ const columns: ColumnDef<Case>[] = [
accessorKey: "archived_at",
header: "תאריך ארכוב",
cell: ({ row }) => (
<span className="text-ink-muted text-sm tabular-nums">
<span className="text-ink-muted text-sm tabular-nums whitespace-nowrap">
{formatDate(row.original.archived_at)}
</span>
),
@@ -130,6 +167,7 @@ export default function ArchivePage() {
{ id: "archived_at", desc: true },
]);
const [globalFilter, setGlobalFilter] = useState("");
const [typeFilter, setTypeFilter] = useState<string>("all");
const rows = useMemo(() => data ?? [], [data]);
@@ -152,126 +190,141 @@ export default function ArchivePage() {
},
});
// domain filter applied client-side (subtypeOf collapses בל"מ variants)
const filteredRows = useMemo(() => {
const all = table.getFilteredRowModel().rows;
if (typeFilter === "all") return all;
return all.filter((r) => subtypeOf(r.original) === typeFilter);
}, [table, typeFilter, globalFilter, sorting, rows]);
const total = rows.length;
const shown = filteredRows.length;
return (
<AppShell>
<section className="space-y-8">
<section className="space-y-6">
<header className="space-y-1.5">
<nav className="text-[0.78rem] text-ink-muted mb-1">
<Link href="/" className="hover:text-gold-deep">בית</Link>
<span aria-hidden> · </span>
<span className="text-navy">ארכיון</span>
</nav>
<div className="flex items-end justify-between gap-4 flex-wrap">
<div className="space-y-1">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
ארכיון תיקי ערר
</div>
<h1 className="text-navy mb-0">תיקים סגורים</h1>
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
תיקים שסגרו את הטיפול בהם. שחזור מחזיר את התיק לרשימה הראשית
ופותח מחדש את הפרויקט המקביל ב-Paperclip.
</p>
</div>
<div className="inline-flex items-baseline gap-2 rounded-lg border border-rule bg-gold-wash px-4 py-2.5">
<span className="text-2xl font-semibold text-gold-deep leading-none tabular-nums">
{table.getFilteredRowModel().rows.length}
</span>
<span className="text-[0.85rem] text-ink-soft">תיקים בארכיון</span>
<div className="space-y-1">
<div className="text-[0.75rem] uppercase tracking-[0.12em] text-gold-deep">
ארכיון תיקי ערר
</div>
<h1 className="text-navy mb-0">ארכיון</h1>
<p className="text-ink-muted text-base max-w-2xl leading-relaxed">
כל ההחלטות שהושלמו לחיפוש, סינון ועיון. {total} תיקים סגורים.
שחזור מחזיר את התיק לרשימה הראשית ופותח מחדש את הפרויקט המקביל ב-Paperclip.
</p>
</div>
</header>
<div className="h-[2px] bg-gradient-to-l from-transparent via-gold to-transparent" />
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5 space-y-4">
<div className="flex items-center gap-3">
<Input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="חיפוש לפי מס׳ ערר או כותרת…"
className="max-w-sm bg-surface"
dir="rtl"
/>
</div>
{/* filter bar — search + domain select + count aligned to end (mockup 05 .filters) */}
<div className="flex items-center gap-3 flex-wrap">
<Input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="חיפוש לפי מספר ערר, כותרת או צד…"
className="flex-1 min-w-[220px] bg-surface"
dir="rtl"
/>
<select
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
className="cursor-pointer text-[0.84rem] text-ink-soft bg-surface border border-rule rounded-lg px-3.5 py-2"
>
<option value="all">כל סוגי הערר</option>
<option value="building_permit">רישוי ובנייה</option>
<option value="betterment_levy">היטל השבחה</option>
<option value="compensation_197">פיצויים (ס׳ 197)</option>
</select>
<span className="ms-auto text-[0.82rem] text-ink-muted tabular-nums">
מציג {shown} מתוך {total}
</span>
</div>
<div className="rounded-lg border border-rule bg-surface shadow-sm overflow-hidden">
<Table>
<TableHeader className="bg-rule-soft/60">
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id} className="border-rule">
{hg.headers.map((header) => (
<TableHead
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className="text-navy font-semibold cursor-pointer select-none text-right"
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{{ asc: " ▲", desc: " ▼" }[
header.column.getIsSorted() as string
] ?? ""}
</TableHead>
{/* clean bordered table — parchment header, gold-wash hover (mockup 05 .card table) */}
<Card className="bg-surface border-rule shadow-sm overflow-hidden p-0">
<CardContent className="p-0">
<Table>
<TableHeader className="bg-parchment">
{table.getHeaderGroups().map((hg) => (
<TableRow key={hg.id} className="border-rule">
{hg.headers.map((header) => (
<TableHead
key={header.id}
onClick={header.column.getToggleSortingHandler()}
className="text-ink-muted font-medium text-[0.78rem] cursor-pointer select-none text-start"
>
{flexRender(
header.column.columnDef.header,
header.getContext(),
)}
{{ asc: " ▲", desc: "" }[
header.column.getIsSorted() as string
] ?? ""}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isPending ? (
Array.from({ length: 3 }).map((_, i) => (
<TableRow key={i} className="border-rule-soft">
{columns.map((_c, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-24" />
</TableCell>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{isPending ? (
Array.from({ length: 3 }).map((_, i) => (
<TableRow key={i} className="border-rule">
{columns.map((_c, j) => (
<TableCell key={j}>
<Skeleton className="h-4 w-24" />
</TableCell>
))}
</TableRow>
))
) : error ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center text-danger py-8"
>
שגיאה בטעינת ארכיון: {error.message}
</TableCell>
))
) : error ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center text-danger py-8"
>
שגיאה בטעינת ארכיון: {error.message}
</TableCell>
</TableRow>
) : filteredRows.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center text-ink-muted py-12"
>
<div className="text-gold text-2xl mb-2" aria-hidden>
</div>
{globalFilter || typeFilter !== "all"
? "אין תיקים תואמים לחיפוש"
: "אין תיקים בארכיון"}
</TableCell>
</TableRow>
) : (
filteredRows.map((row) => (
<TableRow
key={row.id}
className="border-rule-soft hover:bg-gold-wash transition-colors"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-3">
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
) : table.getRowModel().rows.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="text-center text-ink-muted py-12"
>
<div className="text-gold text-2xl mb-2" aria-hidden>
</div>
{globalFilter
? "אין תיקים תואמים לחיפוש"
: "אין תיקים בארכיון"}
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
className="border-rule hover:bg-gold-wash/40 transition-colors"
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="py-3">
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
</section>