From e849285806c36253aca9adc62a47b6e83c1d358b Mon Sep 17 00:00:00 2001 From: Chaim Date: Sat, 2 May 2026 15:44:41 +0000 Subject: [PATCH] home: split cases table by appeal type + add appeal-type chart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (cases listing) - /api/cases: also return updated_at, created_at, practice_area, appeal_subtype, subject. The detail-mode response was previously dropping these even though db.list_cases reads them, leaving the UI's "תחום" and "עודכן" columns blank. Frontend - Split the home table into two: רישוי (1xxx) and היטל השבחה ופיצויים (8xxx + 9xxx), bucketing on appeal_subtype with a case-number-prefix fallback. The "תחום" column is now redundant and removed. - New AppealTypeBars chart in the right rail next to the existing status donut. - Donut: switch to a vertical layout (donut on top, legend below in a 3-col grid) so labels like "חדש / בעיבוד" no longer wrap inside the 320px sidebar; counts now align in a tabular column. - CasesTable accepts emptyText/searchPlaceholder so each split table has its own copy. Co-Authored-By: Claude Opus 4.7 (1M context) --- web-ui/src/app/page.tsx | 97 +++++++++++++++---- .../src/components/cases/appeal-type-bars.tsx | 55 +++++++++++ web-ui/src/components/cases/cases-table.tsx | 18 ++-- web-ui/src/components/cases/status-donut.tsx | 23 +++-- web/app.py | 8 ++ 5 files changed, 160 insertions(+), 41 deletions(-) create mode 100644 web-ui/src/components/cases/appeal-type-bars.tsx diff --git a/web-ui/src/app/page.tsx b/web-ui/src/app/page.tsx index 607232b..eba80d7 100644 --- a/web-ui/src/app/page.tsx +++ b/web-ui/src/app/page.tsx @@ -1,17 +1,31 @@ "use client"; +import { useMemo } from "react"; import Link from "next/link"; import { AppShell } from "@/components/app-shell"; import { KPICards } from "@/components/cases/kpi-cards"; import { StatusDonut } from "@/components/cases/status-donut"; +import { AppealTypeBars, subtypeOf } from "@/components/cases/appeal-type-bars"; import { CasesTable } from "@/components/cases/cases-table"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; -import { useCases } from "@/lib/api/cases"; +import { useCases, type Case } from "@/lib/api/cases"; export default function HomePage() { const { data, isPending, error } = useCases(true); + const { permits, levies } = useMemo(() => { + const permits: Case[] = []; + const levies: Case[] = []; + (data ?? []).forEach((c) => { + const s = subtypeOf(c); + if (s === "building_permit") permits.push(c); + else if (s === "betterment_levy" || s === "compensation_197") levies.push(c); + else permits.push(c); // fallback bucket — keep visible + }); + return { permits, levies }; + }, [data]); + return (
@@ -35,25 +49,70 @@ export default function HomePage() { -
- - -
-

רשימת תיקים

- - מעודכן חי - -
- -
-
+
+
+ + +
+
+

רישוי ובנייה

+ + עררים 1xxx + +
+ + מעודכן חי + +
+ +
+
- - -

פיזור סטטוסים

- -
-
+ + +
+
+

היטל השבחה ופיצויים

+ + עררים 8xxx · 9xxx + +
+ + מעודכן חי + +
+ +
+
+
+ +
diff --git a/web-ui/src/components/cases/appeal-type-bars.tsx b/web-ui/src/components/cases/appeal-type-bars.tsx new file mode 100644 index 0000000..5cf724b --- /dev/null +++ b/web-ui/src/components/cases/appeal-type-bars.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { deriveSubtype } from "@/lib/practice-area"; +import type { AppealSubtype } from "@/lib/practice-area"; +import type { Case } from "@/lib/api/cases"; + +type Bucket = { key: AppealSubtype; label: string; color: string }; + +const BUCKETS: Bucket[] = [ + { key: "building_permit", label: "רישוי ובנייה", color: "var(--color-info)" }, + { key: "betterment_levy", label: "היטל השבחה", color: "var(--color-gold)" }, + { key: "compensation_197", label: "פיצויים (ס׳ 197)", color: "var(--color-warn)" }, +]; + +export function subtypeOf(c: Case): AppealSubtype { + return c.appeal_subtype && c.appeal_subtype !== "unknown" + ? c.appeal_subtype + : deriveSubtype(c.case_number); +} + +export function AppealTypeBars({ cases }: { cases?: Case[] }) { + const counts: Record = { + building_permit: 0, + betterment_levy: 0, + compensation_197: 0, + unknown: 0, + }; + (cases ?? []).forEach((c) => { + counts[subtypeOf(c)] += 1; + }); + const max = Math.max(1, ...BUCKETS.map((b) => counts[b.key])); + + return ( +
    + {BUCKETS.map((b) => { + const n = counts[b.key]; + const widthPct = (n / max) * 100; + return ( +
  • +
    + {b.label} + {n} +
    +
    +
    +
    +
  • + ); + })} +
+ ); +} diff --git a/web-ui/src/components/cases/cases-table.tsx b/web-ui/src/components/cases/cases-table.tsx index ddc4bf6..61bbf12 100644 --- a/web-ui/src/components/cases/cases-table.tsx +++ b/web-ui/src/components/cases/cases-table.tsx @@ -17,7 +17,6 @@ import { import { Input } from "@/components/ui/input"; import { Skeleton } from "@/components/ui/skeleton"; import { StatusBadge } from "@/components/cases/status-badge"; -import { APPEAL_SUBTYPE_LABELS } from "@/lib/practice-area"; import type { Case } from "@/lib/api/cases"; function formatDate(iso?: string) { @@ -60,15 +59,6 @@ const columns: ColumnDef[] = [ header: "סטטוס", cell: ({ row }) => , }, - { - accessorKey: "appeal_subtype", - header: "תחום", - cell: ({ row }) => { - const s = row.original.appeal_subtype; - if (!s || s === "unknown") return ; - return {APPEAL_SUBTYPE_LABELS[s]}; - }, - }, { accessorKey: "document_count", header: "מסמכים", @@ -91,10 +81,14 @@ export function CasesTable({ cases, loading, error, + emptyText = "עדיין אין תיקי ערר", + searchPlaceholder = "חיפוש לפי מס׳ ערר או כותרת…", }: { cases?: Case[]; loading?: boolean; error?: Error | null; + emptyText?: string; + searchPlaceholder?: string; }) { const [sorting, setSorting] = useState([ { id: "updated_at", desc: true }, @@ -128,7 +122,7 @@ export function CasesTable({ setGlobalFilter(e.target.value)} - placeholder="חיפוש לפי מס׳ ערר או כותרת…" + placeholder={searchPlaceholder} className="max-w-sm bg-surface" dir="rtl" /> @@ -176,7 +170,7 @@ export function CasesTable({
- {globalFilter ? "אין תיקים תואמים לחיפוש" : "עדיין אין תיקי ערר"} + {globalFilter ? "אין תיקים תואמים לחיפוש" : emptyText}
) : ( diff --git a/web-ui/src/components/cases/status-donut.tsx b/web-ui/src/components/cases/status-donut.tsx index 5e40ac1..0077f5e 100644 --- a/web-ui/src/components/cases/status-donut.tsx +++ b/web-ui/src/components/cases/status-donut.tsx @@ -55,29 +55,32 @@ export function StatusDonut({ cases }: { cases?: Case[] }) { .join(", ")})`; return ( -
+
-
- +
+ {total} - תיקים + תיקים
-
    +
      {(Object.keys(GROUP_META) as GroupKey[]).map((k) => ( -
    • +
    • - {GROUP_META[k].label} - + {GROUP_META[k].label} + {counts[k]}
    • diff --git a/web/app.py b/web/app.py index 8417db7..26a6d01 100644 --- a/web/app.py +++ b/web/app.py @@ -1080,6 +1080,9 @@ async def list_cases( "title": c["title"], "status": c["status"], "archived_at": c["archived_at"].isoformat() if c.get("archived_at") else None, + "updated_at": c["updated_at"].isoformat() if c.get("updated_at") else None, + "practice_area": c.get("practice_area"), + "appeal_subtype": c.get("appeal_subtype"), } for c in cases ] @@ -1100,10 +1103,15 @@ async def list_cases( "case_number": c["case_number"], "title": c["title"], "status": c["status"], + "subject": c.get("subject", "") or "", "expected_outcome": c.get("expected_outcome", ""), "committee_type": c.get("committee_type", ""), "hearing_date": str(c["hearing_date"]) if c.get("hearing_date") else "", "archived_at": c["archived_at"].isoformat() if c.get("archived_at") else None, + "created_at": c["created_at"].isoformat() if c.get("created_at") else None, + "updated_at": c["updated_at"].isoformat() if c.get("updated_at") else None, + "practice_area": c.get("practice_area"), + "appeal_subtype": c.get("appeal_subtype"), "document_count": doc_count, "processing_count": processing_count, "gitea_url": f"https://gitea.nautilus.marcusgroup.org/cases/{c['case_number']}",