Merge pull request 'feat(case-ui): banner-A status rail + V2 segmented tabs (X17 #2)' (#373) from worktree-case-banner-v2 into main
This commit was merged in pull request #373.
This commit is contained in:
@@ -9,7 +9,6 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import { CaseHeader } from "@/components/cases/case-header";
|
import { CaseHeader } from "@/components/cases/case-header";
|
||||||
import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
|
import { CaseEditDialog } from "@/components/cases/case-edit-dialog";
|
||||||
import { StatusHero } from "@/components/cases/status-hero";
|
|
||||||
import { DocumentsPanel } from "@/components/cases/documents-panel";
|
import { DocumentsPanel } from "@/components/cases/documents-panel";
|
||||||
import { DraftsPanel } from "@/components/cases/drafts-panel";
|
import { DraftsPanel } from "@/components/cases/drafts-panel";
|
||||||
import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel";
|
import { DecisionBlocksPanel } from "@/components/cases/decision-blocks-panel";
|
||||||
@@ -19,7 +18,10 @@ import { AgentActivityPreview } from "@/components/cases/agent-activity-preview"
|
|||||||
import { UploadSheet } from "@/components/documents/upload-sheet";
|
import { UploadSheet } from "@/components/documents/upload-sheet";
|
||||||
import { useCase, useStartWorkflow } from "@/lib/api/cases";
|
import { useCase, useStartWorkflow } from "@/lib/api/cases";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Play, Loader2 } from "lucide-react";
|
import {
|
||||||
|
Play, Loader2, LayoutGrid, Scale, FileText, MessageSquare, Users,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Next 16 breaking change: route params are now a Promise.
|
* Next 16 breaking change: route params are now a Promise.
|
||||||
@@ -60,23 +62,27 @@ export default function CaseDetailPage({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabDefs: [string, string, LucideIcon][] = [
|
||||||
|
["overview", "סקירה", LayoutGrid],
|
||||||
|
["arguments", "טיעונים", Scale],
|
||||||
|
["decision", "ההחלטה", FileText],
|
||||||
|
["drafts", "טיוטות והערות", MessageSquare],
|
||||||
|
["agents", "סוכנים", Users],
|
||||||
|
];
|
||||||
|
|
||||||
|
// V2 "segmented" tab strip (X17): each tab a pill, active = raised white card.
|
||||||
const tabsList = (
|
const tabsList = (
|
||||||
<TabsList
|
<TabsList
|
||||||
variant="line"
|
variant="line"
|
||||||
className="gap-6 h-auto p-0 rounded-none -mb-px"
|
className="!h-auto gap-1.5 p-0 flex-wrap justify-start"
|
||||||
>
|
>
|
||||||
{[
|
{tabDefs.map(([value, label, Icon]) => (
|
||||||
["overview", "סקירה"],
|
|
||||||
["arguments", "טיעונים"],
|
|
||||||
["decision", "ההחלטה"],
|
|
||||||
["drafts", "טיוטות והערות"],
|
|
||||||
["agents", "סוכנים"],
|
|
||||||
].map(([value, label]) => (
|
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={value}
|
key={value}
|
||||||
value={value}
|
value={value}
|
||||||
className="flex-none rounded-none px-0 pb-3.5 pt-0 text-[0.92rem] font-medium text-ink-muted data-active:text-navy data-active:font-semibold data-active:after:bg-gold data-active:after:bottom-0"
|
className="flex-none gap-1.5 rounded-lg border border-transparent px-3.5 py-1.5 text-[0.9rem] font-medium text-ink-soft after:hidden hover:bg-parchment/60 data-active:bg-surface data-active:text-navy data-active:font-semibold data-active:border-rule data-active:shadow-sm [&_svg]:text-ink-light data-active:[&_svg]:text-gold-deep"
|
||||||
>
|
>
|
||||||
|
<Icon className="w-4 h-4" />
|
||||||
{label}
|
{label}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
))}
|
))}
|
||||||
@@ -124,22 +130,18 @@ export default function CaseDetailPage({
|
|||||||
<Skeleton className="h-6 w-96" />
|
<Skeleton className="h-6 w-96" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<CaseHeader data={data} actions={bandActions} tabs={tabsList} />
|
<CaseHeader caseNumber={caseNumber} data={data} actions={bandActions} tabs={tabsList} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* full-width tab content — status lives ONLY in the overview hero
|
{/* full-width tab content — status now lives in the always-visible band
|
||||||
(mockup 18b); the other tabs lose the status rail entirely (INV-IA1). */}
|
StatusRail (banner A, X17), so every tab keeps the status context. */}
|
||||||
<div className="min-w-0 mt-6">
|
<div className="min-w-0 mt-6">
|
||||||
<TabsContent value="overview" className="mt-0 space-y-6">
|
<TabsContent value="overview" className="mt-0">
|
||||||
{/* ★ STATUS HERO — the single home for case status (INV-IA1/INV-UI7) */}
|
{/* documents + agents two-column (mockup 18j) */}
|
||||||
<StatusHero caseNumber={caseNumber} data={data} />
|
|
||||||
|
|
||||||
{/* documents + agents two-column below the hero (mockup 18b) */}
|
|
||||||
<div className="grid gap-6 lg:grid-cols-2 items-start">
|
<div className="grid gap-6 lg:grid-cols-2 items-start">
|
||||||
<DocumentsPanel data={data} />
|
<DocumentsPanel data={data} />
|
||||||
<AgentActivityPreview caseNumber={caseNumber} />
|
<AgentActivityPreview caseNumber={caseNumber} />
|
||||||
</div>
|
</div>
|
||||||
{/* decision-editor CTA moved to the band actions (visible on all tabs) */}
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="arguments" className="mt-0">
|
<TabsContent value="arguments" className="mt-0">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { ReactNode } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { StatusBadge } from "@/components/cases/status-badge";
|
import { StatusBadge } from "@/components/cases/status-badge";
|
||||||
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
import { StatusRail } from "@/components/cases/status-rail";
|
||||||
import { CaseArchiveAction } from "@/components/cases/case-archive-action";
|
import { CaseArchiveAction } from "@/components/cases/case-archive-action";
|
||||||
import { CreateRepoButton } from "@/components/cases/create-repo-button";
|
import { CreateRepoButton } from "@/components/cases/create-repo-button";
|
||||||
import {
|
import {
|
||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
APPEAL_SUBTYPE_LABELS,
|
APPEAL_SUBTYPE_LABELS,
|
||||||
isBlamSubtype,
|
isBlamSubtype,
|
||||||
} from "@/lib/practice-area";
|
} from "@/lib/practice-area";
|
||||||
import { formatDate } from "@/lib/format-date";
|
|
||||||
import type { CaseDetail } from "@/lib/api/cases";
|
import type { CaseDetail } from "@/lib/api/cases";
|
||||||
|
|
||||||
function partiesLine(data?: CaseDetail): string | null {
|
function partiesLine(data?: CaseDetail): string | null {
|
||||||
@@ -29,10 +28,12 @@ function partiesLine(data?: CaseDetail): string | null {
|
|||||||
* the tab strip inside the band, anchored to its bottom edge.
|
* the tab strip inside the band, anchored to its bottom edge.
|
||||||
*/
|
*/
|
||||||
export function CaseHeader({
|
export function CaseHeader({
|
||||||
|
caseNumber,
|
||||||
data,
|
data,
|
||||||
actions,
|
actions,
|
||||||
tabs,
|
tabs,
|
||||||
}: {
|
}: {
|
||||||
|
caseNumber: string;
|
||||||
data?: CaseDetail;
|
data?: CaseDetail;
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
tabs?: ReactNode;
|
tabs?: ReactNode;
|
||||||
@@ -123,25 +124,13 @@ export function CaseHeader({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* metadata strip — hearing date / updated / sync */}
|
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm shrink-0">
|
|
||||||
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
|
||||||
תאריך דיון
|
|
||||||
</dt>
|
|
||||||
<dd className="text-ink-soft tabular-nums">{formatDate(data?.hearing_date)}</dd>
|
|
||||||
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
|
||||||
עודכן
|
|
||||||
</dt>
|
|
||||||
<dd className="text-ink-soft tabular-nums">{formatDate(data?.updated_at)}</dd>
|
|
||||||
<dt className="text-ink-muted text-[0.72rem] uppercase tracking-wider">
|
|
||||||
סנכרון
|
|
||||||
</dt>
|
|
||||||
<dd><SyncIndicator caseNumber={data?.case_number} /></dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* tab strip anchored to band bottom (mockup .tabs) */}
|
{/* ★ integrated status rail — visible on every tab (banner A, X17) */}
|
||||||
{tabs ? <div className="mt-5">{tabs}</div> : null}
|
<StatusRail caseNumber={caseNumber} data={data} />
|
||||||
|
|
||||||
|
{/* tab strip anchored to band bottom */}
|
||||||
|
{tabs ? <div className="mt-4">{tabs}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { PHASES, phaseIndexOf, type CaseStatus } from "@/lib/api/case-status";
|
|
||||||
import { statusLabel } from "@/lib/api/case-status";
|
|
||||||
import { StatusChanger } from "@/components/cases/status-changer";
|
|
||||||
import { StatusGuide } from "@/components/cases/status-guide";
|
|
||||||
import { AgentStatusWidget } from "@/components/cases/agent-status-widget";
|
|
||||||
import { expectedOutcomes } from "@/lib/schemas/case";
|
|
||||||
import { formatDate } from "@/lib/format-date";
|
|
||||||
import type { CaseDetail } from "@/lib/api/cases";
|
|
||||||
|
|
||||||
/*
|
|
||||||
* StatusHero — the single, prominent home for case status (mockup 18b, INV-IA1).
|
|
||||||
* Renders the horizontal pipeline stepper, the current-stage chip, the manual
|
|
||||||
* status changer, and a meta strip. This is the ONLY surface in the app that
|
|
||||||
* owns the status changer / pipeline timeline (status was removed from the
|
|
||||||
* compose editor and the other case tabs). Reuses the shared StatusChanger,
|
|
||||||
* StatusGuide and AgentStatusWidget so no parallel status path is created (G2).
|
|
||||||
*/
|
|
||||||
|
|
||||||
const EXPECTED_OUTCOME_LABELS: Record<string, string> = Object.fromEntries(
|
|
||||||
expectedOutcomes.map((o) => [o.value, o.label]),
|
|
||||||
);
|
|
||||||
|
|
||||||
/** Horizontal RTL stepper over the 5 lifecycle phases (mockup 18b .stepper). */
|
|
||||||
function HorizontalStepper({ status }: { status?: CaseStatus }) {
|
|
||||||
const currentIdx = phaseIndexOf(status);
|
|
||||||
return (
|
|
||||||
<ol className="flex items-start gap-0">
|
|
||||||
{PHASES.map((phase, i) => {
|
|
||||||
const state =
|
|
||||||
currentIdx === -1 ? "pending"
|
|
||||||
: i < currentIdx ? "done"
|
|
||||||
: i === currentIdx ? "now"
|
|
||||||
: "pending";
|
|
||||||
|
|
||||||
const isLast = i === PHASES.length - 1;
|
|
||||||
|
|
||||||
const knobTone =
|
|
||||||
state === "done"
|
|
||||||
? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
|
|
||||||
: state === "now"
|
|
||||||
? "bg-gold [box-shadow:0_0_0_2px_var(--color-gold-wash),0_0_0_3px_var(--color-gold)]"
|
|
||||||
: "bg-rule [box-shadow:0_0_0_1px_var(--color-rule)]";
|
|
||||||
|
|
||||||
const labelTone =
|
|
||||||
state === "done"
|
|
||||||
? "text-ink-soft"
|
|
||||||
: state === "now"
|
|
||||||
? "text-navy font-bold"
|
|
||||||
: "text-ink-muted";
|
|
||||||
|
|
||||||
// connector runs toward the next (earlier-completed) phase; in RTL the
|
|
||||||
// line sits to the start side. Coloured when this phase is done.
|
|
||||||
const segTone = state === "done" ? "bg-success" : "bg-rule";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li
|
|
||||||
key={phase.key}
|
|
||||||
className="relative flex-1 pt-7 text-center"
|
|
||||||
>
|
|
||||||
{!isLast && (
|
|
||||||
<span
|
|
||||||
className={`absolute top-[9px] h-0.5 w-full ${segTone}`}
|
|
||||||
style={{ insetInlineStart: "50%" }}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<span
|
|
||||||
className={`absolute top-[3px] h-4 w-4 -translate-x-1/2 rounded-full border-[3px] border-surface ${knobTone}`}
|
|
||||||
style={{ insetInlineStart: "50%", transform: "translateX(50%)" }}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<span className={`block text-[0.78rem] ${labelTone}`}>
|
|
||||||
{phase.label}
|
|
||||||
<span className="block text-[0.68rem] text-ink-muted mt-0.5 tabular-nums">
|
|
||||||
{state === "now" ? "כעת" : state === "done" ? "✓" : "—"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ol>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StatusHero({
|
|
||||||
caseNumber,
|
|
||||||
data,
|
|
||||||
}: {
|
|
||||||
caseNumber: string;
|
|
||||||
data?: CaseDetail;
|
|
||||||
}) {
|
|
||||||
const status = data?.status;
|
|
||||||
const expectedOutcomeLabel = data?.expected_outcome
|
|
||||||
? EXPECTED_OUTCOME_LABELS[data.expected_outcome] ?? data.expected_outcome
|
|
||||||
: null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="rounded-xl border border-rule bg-surface shadow-sm px-6 py-5">
|
|
||||||
{/* hero top — title + current stage chip + manual changer */}
|
|
||||||
<div className="flex items-start justify-between flex-wrap gap-4 mb-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<h2 className="text-navy text-base font-bold">סטטוס התיק</h2>
|
|
||||||
{status && (
|
|
||||||
<span className="rounded-full bg-warn-bg text-warn text-[0.8rem] font-bold px-3.5 py-1">
|
|
||||||
כעת: {statusLabel(status)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{/* manual status changer — the SINGLE manual-status surface (INV-IA1) */}
|
|
||||||
<StatusChanger caseNumber={caseNumber} currentStatus={status} inline />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* horizontal pipeline */}
|
|
||||||
<HorizontalStepper status={status} />
|
|
||||||
|
|
||||||
{/* meta strip below the stepper (mockup 18b .hero-meta) */}
|
|
||||||
<div className="flex flex-wrap gap-x-8 gap-y-3 mt-5 pt-4 border-t border-rule-soft">
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.72rem] text-ink-muted">תוצאה צפויה</span>
|
|
||||||
<span className="text-sm font-semibold mt-0.5">
|
|
||||||
{expectedOutcomeLabel ? (
|
|
||||||
<span className="inline-block rounded-full bg-warn-bg text-warn text-[0.78rem] font-semibold px-2.5 py-0.5">
|
|
||||||
{expectedOutcomeLabel}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-ink-soft">לא נקבע</span>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.72rem] text-ink-muted">תאריך דיון</span>
|
|
||||||
<span className="text-sm font-semibold text-ink-soft mt-0.5 tabular-nums">
|
|
||||||
{formatDate(data?.hearing_date)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-[0.72rem] text-ink-muted">עודכן לאחרונה</span>
|
|
||||||
<span className="text-sm font-semibold text-ink-soft mt-0.5 tabular-nums">
|
|
||||||
{formatDate(data?.updated_at)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* agents activity + status-map progressive disclosure (INV-IA6) */}
|
|
||||||
<div className="mt-4 pt-4 border-t border-rule-soft space-y-3">
|
|
||||||
<AgentStatusWidget caseNumber={caseNumber} />
|
|
||||||
<StatusGuide />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
195
web-ui/src/components/cases/status-rail.tsx
Normal file
195
web-ui/src/components/cases/status-rail.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
PHASES,
|
||||||
|
phaseIndexOf,
|
||||||
|
phaseOf,
|
||||||
|
type CaseStatus,
|
||||||
|
} from "@/lib/api/case-status";
|
||||||
|
import { StatusChanger } from "@/components/cases/status-changer";
|
||||||
|
import { StatusGuide } from "@/components/cases/status-guide";
|
||||||
|
import { AgentStatusWidget } from "@/components/cases/agent-status-widget";
|
||||||
|
import { SyncIndicator } from "@/components/cases/sync-indicator";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
|
import { expectedOutcomes } from "@/lib/schemas/case";
|
||||||
|
import { formatDate } from "@/lib/format-date";
|
||||||
|
import { Settings2, Users } from "lucide-react";
|
||||||
|
import type { CaseDetail } from "@/lib/api/cases";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* StatusRail — the integrated case-status strip that lives inside the parchment
|
||||||
|
* band (CaseHeader), so it is visible on EVERY tab (banner "A", X17 case-detail
|
||||||
|
* redesign — supersedes the overview-only StatusHero). One thin row of chips
|
||||||
|
* (current phase · expected outcome · meta · agents popover · manual changer)
|
||||||
|
* over the 5-phase pipeline stepper. Reuses the shared StatusChanger / StatusGuide
|
||||||
|
* / AgentStatusWidget so no parallel status path is created (G2).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const EXPECTED_OUTCOME_LABELS: Record<string, string> = Object.fromEntries(
|
||||||
|
expectedOutcomes.map((o) => [o.value, o.label]),
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Semantic colour per expected-outcome value (rejection/partial/full/none). */
|
||||||
|
const OUTCOME_TONE: Record<string, string> = {
|
||||||
|
rejection: "bg-danger-bg text-danger",
|
||||||
|
partial_acceptance: "bg-warn-bg text-warn",
|
||||||
|
full_acceptance: "bg-success-bg text-success",
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Horizontal RTL stepper over the 5 lifecycle phases (mockup A .stepper). */
|
||||||
|
function HorizontalStepper({ status }: { status?: CaseStatus }) {
|
||||||
|
const currentIdx = phaseIndexOf(status);
|
||||||
|
return (
|
||||||
|
<ol className="flex items-start gap-0">
|
||||||
|
{PHASES.map((phase, i) => {
|
||||||
|
const state =
|
||||||
|
currentIdx === -1 ? "pending"
|
||||||
|
: i < currentIdx ? "done"
|
||||||
|
: i === currentIdx ? "now"
|
||||||
|
: "pending";
|
||||||
|
|
||||||
|
const isLast = i === PHASES.length - 1;
|
||||||
|
|
||||||
|
const knobTone =
|
||||||
|
state === "done"
|
||||||
|
? "bg-success [box-shadow:0_0_0_1px_var(--color-success)]"
|
||||||
|
: state === "now"
|
||||||
|
? "bg-gold [box-shadow:0_0_0_2px_var(--color-gold-wash),0_0_0_3px_var(--color-gold)]"
|
||||||
|
: "bg-rule [box-shadow:0_0_0_1px_var(--color-rule)]";
|
||||||
|
|
||||||
|
const labelTone =
|
||||||
|
state === "done"
|
||||||
|
? "text-ink-soft"
|
||||||
|
: state === "now"
|
||||||
|
? "text-navy font-bold"
|
||||||
|
: "text-ink-muted";
|
||||||
|
|
||||||
|
const segTone = state === "done" ? "bg-success" : "bg-rule";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li key={phase.key} className="relative flex-1 pt-6 text-center">
|
||||||
|
{!isLast && (
|
||||||
|
<span
|
||||||
|
className={`absolute top-[7px] h-0.5 w-full ${segTone}`}
|
||||||
|
style={{ insetInlineStart: "50%" }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span
|
||||||
|
className={`absolute top-[2px] h-3.5 w-3.5 rounded-full border-[2.5px] border-surface ${knobTone}`}
|
||||||
|
style={{ insetInlineStart: "50%", transform: "translateX(50%)" }}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span className={`block text-[0.74rem] ${labelTone}`}>
|
||||||
|
{phase.label}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ol>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const PILL =
|
||||||
|
"inline-flex items-center gap-1.5 rounded-full border border-rule bg-surface px-3 py-1 text-[0.72rem] font-semibold text-ink-soft hover:bg-parchment transition-colors";
|
||||||
|
|
||||||
|
export function StatusRail({
|
||||||
|
caseNumber,
|
||||||
|
data,
|
||||||
|
}: {
|
||||||
|
caseNumber: string;
|
||||||
|
data?: CaseDetail;
|
||||||
|
}) {
|
||||||
|
const status = data?.status;
|
||||||
|
const phaseKey = phaseOf(status);
|
||||||
|
const phaseLabel = phaseKey
|
||||||
|
? PHASES.find((p) => p.key === phaseKey)?.label
|
||||||
|
: null;
|
||||||
|
const outcome = data?.expected_outcome ?? "";
|
||||||
|
const outcomeLabel = outcome
|
||||||
|
? EXPECTED_OUTCOME_LABELS[outcome] ?? outcome
|
||||||
|
: null;
|
||||||
|
const outcomeTone = outcome
|
||||||
|
? OUTCOME_TONE[outcome] ?? "bg-warn-bg text-warn"
|
||||||
|
: "bg-rule-soft text-ink-muted";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-3 border-t border-rule-soft pt-3">
|
||||||
|
{/* chip row */}
|
||||||
|
<div className="flex items-center gap-x-3 gap-y-2 flex-wrap mb-3">
|
||||||
|
<span className="text-navy text-[0.8rem] font-bold me-1">סטטוס</span>
|
||||||
|
{phaseLabel && (
|
||||||
|
<span className="rounded-full bg-warn-bg text-warn text-[0.78rem] font-bold px-3 py-1">
|
||||||
|
כעת: {phaseLabel}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<span className="text-[0.72rem] text-ink-muted">תוצאה צפויה:</span>
|
||||||
|
<span
|
||||||
|
className={`inline-block rounded-full text-[0.74rem] font-semibold px-2.5 py-0.5 ${outcomeTone}`}
|
||||||
|
>
|
||||||
|
{outcomeLabel ?? "טרם נקבעה"}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span className="flex-1" />
|
||||||
|
|
||||||
|
<span className="flex items-center gap-3 text-[0.72rem] text-ink-muted">
|
||||||
|
<span>
|
||||||
|
דיון{" "}
|
||||||
|
<b className="text-ink-soft font-semibold tabular-nums">
|
||||||
|
{formatDate(data?.hearing_date)}
|
||||||
|
</b>
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
עודכן{" "}
|
||||||
|
<b className="text-ink-soft font-semibold tabular-nums">
|
||||||
|
{formatDate(data?.updated_at)}
|
||||||
|
</b>
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex items-center gap-1">
|
||||||
|
סנכרון <SyncIndicator caseNumber={data?.case_number} />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* agents popover */}
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button type="button" className={PILL} title="סוכני התיק">
|
||||||
|
<span className="h-1.5 w-1.5 rounded-full bg-success" />
|
||||||
|
<Users className="w-3.5 h-3.5" />
|
||||||
|
סוכנים
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-80 p-3">
|
||||||
|
<AgentStatusWidget caseNumber={caseNumber} />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
{/* manual status changer + status guide popover */}
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<button type="button" className={PILL} title="שינוי סטטוס ידני">
|
||||||
|
<Settings2 className="w-3.5 h-3.5" />
|
||||||
|
שינוי
|
||||||
|
</button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent align="end" className="w-72 p-3 space-y-3">
|
||||||
|
<div className="text-[0.72rem] font-semibold text-ink-muted">
|
||||||
|
שינוי סטטוס ידני
|
||||||
|
</div>
|
||||||
|
<StatusChanger caseNumber={caseNumber} currentStatus={status} inline />
|
||||||
|
<StatusGuide />
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* pipeline */}
|
||||||
|
<HorizontalStepper status={status} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user