feat(ui): IA redesign → production · יישום נאמן של 16 הדפים הנותרים למוקאפים
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 6s
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user