diff --git a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx index 5884779..f6890df 100644 --- a/web-ui/src/app/cases/[caseNumber]/compose/page.tsx +++ b/web-ui/src/app/cases/[caseNumber]/compose/page.tsx @@ -7,13 +7,20 @@ import { AppShell } from "@/components/app-shell"; import { Card, CardContent } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { SubsectionCard } from "@/components/compose/subsection-card"; import { PrecedentsSection } from "@/components/compose/precedents-section"; import { CitationVerificationPanel } from "@/components/compose/citation-verification-panel"; import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel"; +import { StatusBadge } from "@/components/cases/status-badge"; import { Markdown } from "@/components/ui/markdown"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { useCase, type CaseStatus } from "@/lib/api/cases"; +import { useCase } from "@/lib/api/cases"; import { useCaseLearningStatus, type CaseLearningStatus, @@ -23,34 +30,6 @@ import { useCasePrecedents } from "@/lib/api/precedents"; import { APPEAL_SUBTYPES } from "@/lib/practice-area"; import { DOC_TYPE_LABELS, type DocType } from "@/lib/doc-types"; -// ── Case-status → Hebrew label + tone (mockup 03 status chip) ──────────────── -const STATUS_CHIP: Record = { - new: { label: "חדש", cls: "bg-rule-soft text-ink-muted border-rule" }, - processing: { label: "בעיבוד", cls: "bg-info-bg text-info border-info/30" }, - documents_ready: { label: "מסמכים מוכנים", cls: "bg-info-bg text-info border-info/30" }, - outcome_set: { label: "תוצאה נקבעה", cls: "bg-info-bg text-info border-info/30" }, - direction_approved: { label: "כיוון אושר", cls: "bg-info-bg text-info border-info/30" }, - qa_review: { label: "בדיקת-איכות", cls: "bg-gold-wash text-gold-deep border-gold/40" }, - drafted: { label: "טיוטה", cls: "bg-gold-wash text-gold-deep border-gold/40" }, - exported: { label: "יוצא", cls: "bg-success-bg text-success border-success/40" }, - reviewed: { label: "נסקר", cls: "bg-success-bg text-success border-success/40" }, - final: { label: "סופי", cls: "bg-success-bg text-success border-success/40" }, -}; - -function StatusChip({ status }: { status?: CaseStatus }) { - const c = (status && STATUS_CHIP[status]) || { - label: "בעריכה", - cls: "bg-info-bg text-info border-info/30", - }; - return ( - - {c.label} - - ); -} - function subtypeLabel(subtype?: string | null): string | null { if (!subtype) return null; return APPEAL_SUBTYPES.find((s) => s.value === subtype)?.label ?? null; @@ -289,7 +268,9 @@ export default function ComposePage({

ערר {caseNumber}

- + {/* read-only status chip — reuses the shared StatusBadge (G2: no + parallel status map). Status is changed only in the overview hero. */} + {caseQuery.data?.status && } {subtype && ( {subtype} @@ -313,193 +294,208 @@ export default function ComposePage({ ) : ( - /* ── Two-column workspace: tabbed main editor + 320px side rail ──── */ -
- {/* MAIN — block editor (default) + chair positions, as tabs (mockup 03) */} -
- - - עורך הבלוקים - עמדות וטענות - אימות פסיקה - + /* ── Full-width workspace (mockup 03b): no side rail; the case-docs + table is gone (DocumentsPanel in the overview is the sole owner), + relocated here as a collapsible doc-strip inside the positions tab. ── */ + + + עורך הבלוקים + עמדות וטענות + אימות פסיקה + - {/* Tab 1 — the 12-block decision editor (reused DecisionBlocksPanel) */} - - - + {/* Tab 1 — the 12-block decision editor (reused DecisionBlocksPanel) */} + + + - {/* Tab 3 — citation verification: per-argument support + verify gate (#154) */} - - - + {/* Tab 3 — citation verification: per-argument support + verify gate (#154) */} + + + - {/* Tab 2 — chair positions on the analyst's threshold-claims + issues */} - - {analysis.isPending ? ( - - - - - - - - ) : isNotFound ? ( - - -
-

- טרם בוצע ניתוח משפטי לתיק זה -

-

- לאחר שקובץ analysis-and-research.md ייווצר, תוכלי - לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה. -

-
-
- ) : analysis.error ? ( - - -

{analysis.error.message}

-
-
- ) : analysis.data ? ( -
- {/* Threshold claims */} - {analysis.data.threshold_claims && - analysis.data.threshold_claims.length > 0 && ( -
-
-

טענות סף

- - {analysis.data.threshold_claims.length} + {/* Tab 2 — chair positions on the analyst's threshold-claims + issues */} + + {/* ★ collapsible doc-strip at the top, closed by default (mockup 03b). + Read-only pointer to the docs owned by the overview tab. */} + + + + + + מסמכי התיק + + {documents.length} מסמכים -
-
- {analysis.data.threshold_claims.map((tc) => ( - - ))} -
-
- )} - - {/* Issues */} - {analysis.data.issues && analysis.data.issues.length > 0 && ( -
-
-

סוגיות להכרעה

- - {analysis.data.issues.length} -
-
- {analysis.data.issues.map((iss) => ( - - ))} -
-
- )} + + + {documents.length === 0 ? ( +

אין מסמכים מצורפים

+ ) : ( +
+ {documents.map((d) => ( + + + {d.title || "מסמך"} + + {DOC_TYPE_LABELS[d.doc_type as DocType] ?? d.doc_type} + + + ))} +
+ )} +
+ + - {!analysis.data.threshold_claims?.length && - !analysis.data.issues?.length && ( - - - לא נמצאו טענות סף או סוגיות בניתוח זה. - - - )} +

+ סוגיות-המחלוקת ועמדות הצדדים. עורך עמדת-היו״ר נשמר אוטומטית ומזין את בלוק י׳ (דיון והכרעה). +

- {/* Background prose — supporting context after the decision points */} - - -

רקע לניתוח

- - - - -
-
- - {analysis.data.conclusions?.trim() && ( - + {analysis.isPending ? ( + -

מסקנות

- + + +
- )} + ) : isNotFound ? ( + + +
+

+ טרם בוצע ניתוח משפטי לתיק זה +

+

+ לאחר שקובץ analysis-and-research.md ייווצר, תוכלי + לערוך כאן את עמדת הוועדה לכל טענת סף וסוגיה. +

+
+
+ ) : analysis.error ? ( + + +

{analysis.error.message}

+
+
+ ) : analysis.data ? ( +
+ {/* Threshold claims — laid out 2-up across the full width (mockup 03b) */} + {analysis.data.threshold_claims && + analysis.data.threshold_claims.length > 0 && ( +
+
+

טענות סף

+ + {analysis.data.threshold_claims.length} + +
+
+ {analysis.data.threshold_claims.map((tc) => ( + + ))} +
+
+ )} + + {/* Issues — 2-up grid */} + {analysis.data.issues && analysis.data.issues.length > 0 && ( +
+
+

סוגיות להכרעה

+ + {analysis.data.issues.length} + +
+
+ {analysis.data.issues.map((iss) => ( + + ))} +
- ) : null} - - -
- - {/* SIDE RAIL — documents · attached precedents · finish-and-transfer */} - -
+ {/* Background prose — supporting context after the decision points */} + + +

רקע לניתוח

+ + + + +
+
+ + {analysis.data.conclusions?.trim() && ( + + +

מסקנות

+ +
+
+ )} +
+ ) : null} + + {/* ── case-level פסיקה מצורפת + finish/export gates — relocated from + the removed side rail; kept inline (INV-IA3: gates not removed). ── */} +
+ {/* פסיקה מצורפת (case-level) — stays inline */} + + +

פסיקה מצורפת

+

+ ציטוטים התומכים בעמדה באופן רוחבי — ישולבו בפתיחת בלוק י (דיון). +

+ +
+
+ + {/* השלמה והעברה — export/upload gates, relocated not removed */} + analysis.refetch()} + /> +
+ + )} ); diff --git a/web-ui/src/app/cases/[caseNumber]/page.tsx b/web-ui/src/app/cases/[caseNumber]/page.tsx index ef92085..9436628 100644 --- a/web-ui/src/app/cases/[caseNumber]/page.tsx +++ b/web-ui/src/app/cases/[caseNumber]/page.tsx @@ -9,26 +9,18 @@ import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { CaseHeader } from "@/components/cases/case-header"; import { CaseEditDialog } from "@/components/cases/case-edit-dialog"; -import { WorkflowTimeline } from "@/components/cases/workflow-timeline"; -import { StatusGuide } from "@/components/cases/status-guide"; -import { StatusChanger } from "@/components/cases/status-changer"; +import { StatusHero } from "@/components/cases/status-hero"; import { DocumentsPanel } from "@/components/cases/documents-panel"; import { DraftsPanel } from "@/components/cases/drafts-panel"; import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel"; import { LegalArgumentsPanel } from "@/components/cases/legal-arguments-panel"; import { AgentActivityFeed } from "@/components/cases/agent-activity-feed"; import { AgentActivityPreview } from "@/components/cases/agent-activity-preview"; -import { AgentStatusWidget } from "@/components/cases/agent-status-widget"; import { UploadSheet } from "@/components/documents/upload-sheet"; -import { expectedOutcomes } from "@/lib/schemas/case"; import { useCase, useStartWorkflow } from "@/lib/api/cases"; import { toast } from "sonner"; import { Play, Loader2 } from "lucide-react"; -const EXPECTED_OUTCOME_LABELS: Record = Object.fromEntries( - expectedOutcomes.map((o) => [o.value, o.label]), -); - /* * Next 16 breaking change: route params are now a Promise. * The `use()` hook unwraps them inside a client component. @@ -42,9 +34,6 @@ export default function CaseDetailPage({ const { data, isPending, error, refetch } = useCase(caseNumber); const startWorkflow = useStartWorkflow(caseNumber); const canStartWorkflow = data?.status === "new" || data?.status === "documents_ready"; - const expectedOutcomeLabel = data?.expected_outcome - ? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome - : null; // Only take over the whole page when there is NO data to show. A transient // 5xx on the 5s background refetch must not blow away an already-loaded page. @@ -138,73 +127,52 @@ export default function CaseDetailPage({ )} - {/* two-column wrap — main tab content (1fr) + rail (340px) */} -
-
- + {/* full-width tab content — status lives ONLY in the overview hero + (mockup 18b); the other tabs lose the status rail entirely (INV-IA1). */} +
+ + {/* ★ STATUS HERO — the single home for case status (INV-IA1/INV-UI7) */} + + + {/* documents + agents two-column below the hero (mockup 18b) */} +
- - {/* decision-editor CTA moved to the band actions (visible on all tabs) */} - +
+ {/* decision-editor CTA moved to the band actions (visible on all tabs) */} +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - {/* rail — status timeline + status controls (mockup .rail) */} -
- -
- סטטוס התיק -
- - - - {expectedOutcomeLabel && ( -
-
-
תוצאה צפויה
-
- {expectedOutcomeLabel} -
-
-
- )} - - + + + + -
+
+ + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/web-ui/src/components/cases/legal-arguments-panel.tsx b/web-ui/src/components/cases/legal-arguments-panel.tsx index 7297333..eb9ff9c 100644 --- a/web-ui/src/components/cases/legal-arguments-panel.tsx +++ b/web-ui/src/components/cases/legal-arguments-panel.tsx @@ -36,6 +36,23 @@ const PRIORITY_BADGE_TONE: Record = { relief: "bg-emerald-50 text-emerald-900 border-emerald-200", }; +// Per-party colour mark + one-line role description (mockup 25b party trigger). +const PARTY_MARK_TONE: Record = { + appellant: "bg-info", + respondent: "bg-gold-deep", + committee: "bg-success", + permit_applicant: "bg-warn", + unknown: "bg-ink-muted", +}; + +const PARTY_SUB_HE: Record = { + appellant: "הצד היוזם של הערר", + respondent: "הצד המשיב לערר", + committee: "עמדת הגורם המאשר", + permit_applicant: "צד שלישי שזכותו עשויה להיפגע", + unknown: "צד שלא זוהה אוטומטית", +}; + function groupByPriority( args: LegalArgument[], ): Record { @@ -55,22 +72,18 @@ function groupByPriority( } type PartySectionProps = { - party: LegalArgumentParty; args: LegalArgument[]; }; -function PartySection({ party, args }: PartySectionProps) { +/** + * Inner body of a single party — the priority-grouping + per-argument inner + * accordion. The party header (name / sub / count) now lives on the enclosing + * party-accordion trigger (mockup 25b), so this renders the groups only. + */ +function PartySection({ args }: PartySectionProps) { const grouped = useMemo(() => groupByPriority(args), [args]); return ( -
-
-

- {PARTY_LABELS_HE[party] ?? party} -

- - {args.length} טיעונים - -
+
{PRIORITY_ORDER.map((priority) => { const list = grouped[priority]; if (!list?.length) return null; @@ -253,15 +266,47 @@ export function LegalArgumentsPanel({ caseNumber }: LegalArgumentsPanelProps) { החישוב למנתח המשפטי.

) : ( -
- {parties.map((party) => ( - - ))} -
+ // Top-level party accordion — each party is its own collapsible section, + // closed by default (empty defaultValue). The count badge on every + // trigger shows the scope without expanding (mockup 25b). + + {parties.map((party) => { + const args = data.by_party[party] ?? []; + return ( + + + + + + + {PARTY_LABELS_HE[party] ?? party} + + + {PARTY_SUB_HE[party]} + + + + {args.length} טיעונים + + + + + + + + ); + })} + )}
); diff --git a/web-ui/src/components/cases/status-changer.tsx b/web-ui/src/components/cases/status-changer.tsx index bf432e7..a318764 100644 --- a/web-ui/src/components/cases/status-changer.tsx +++ b/web-ui/src/components/cases/status-changer.tsx @@ -21,9 +21,13 @@ const ALL_STATUSES: readonly CaseStatus[] = CASE_STATUSES; export function StatusChanger({ caseNumber, currentStatus, + inline = false, }: { caseNumber: string; currentStatus?: CaseStatus; + /** Compact horizontal layout for the status hero (mockup 18b hero-controls); + * default is the stacked rail layout. */ + inline?: boolean; }) { // `null` = untouched → the dropdown tracks the live `currentStatus` (which // arrives async and changes on the 5s poll / external updates). Only an @@ -45,8 +49,10 @@ export function StatusChanger({ }; return ( -
- +
+ {!inline && ( + + )}