All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 37s
- דף /feedback חדש: מאגד את כל הערות chair_feedback מכל התיקים, סינון טרם-יושמו/הכל + לפי קטגוריה, כפתור "סמן כיושם" לכל הערה - מרכז אישורים: כרטיס "הערות יו"ר" קישר ל-/ (חסר תועלת) → עכשיו /feedback - מרכז אישורים: כרטיס "תיקים שנכשלו ב-QA" — כל תיק במדגם קליקבילי לדף התיק, והכרטיס מקשר ישירות לתיק כשיש רק אחד - ApprovalSample.href אופציונלי; פריטי מדגם נהפכים ל-Link כשיש href - ניווט: הוספת "הערות יו"ר" לקבוצת work ב-app-shell Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
"use client";
|
||
|
||
import type { ReactNode } from "react";
|
||
import Link from "next/link";
|
||
import { usePathname } from "next/navigation";
|
||
import { ChevronDown, Settings } from "lucide-react";
|
||
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuLabel,
|
||
DropdownMenuSeparator,
|
||
DropdownMenuTrigger,
|
||
} from "@/components/ui/dropdown-menu";
|
||
import { GlobalSearch } from "@/components/global-search";
|
||
import { headerSubtitle } from "@/components/header-context";
|
||
import { useMissingPrecedentsOpenCount } from "@/lib/api/missing-precedents";
|
||
import { usePendingApprovals } from "@/lib/api/chair";
|
||
|
||
/**
|
||
* Ezer Mishpati navigation shell — two-row header.
|
||
*
|
||
* Row 1 (brand): logo + dynamic context subtitle · global search · agent boards
|
||
* Row 2 (nav): work group · knowledge group · admin dropdown
|
||
*
|
||
* Editorial/judicial aesthetic preserved:
|
||
* - Navy background with a gold hairline rule (border-b-3)
|
||
* - Parchment text, gold accents on hover/active
|
||
* - Hebrew RTL throughout (set on <html> in layout.tsx)
|
||
* - Active item gets `aria-current="page"` and a gold underline anchored
|
||
* to the bottom border, so screen readers announce the section and
|
||
* sighted users see where they are.
|
||
*/
|
||
|
||
type NavItem = { href: string; label: string };
|
||
type NavGroup = { id: string; items: NavItem[] };
|
||
|
||
const NAV_GROUPS: NavGroup[] = [
|
||
{
|
||
id: "work",
|
||
items: [
|
||
{ href: "/", label: "בית" },
|
||
{ href: "/approvals", label: "מרכז אישורים" },
|
||
{ href: "/feedback", label: "הערות יו״ר" },
|
||
{ href: "/archive", label: "ארכיון" },
|
||
],
|
||
},
|
||
{
|
||
id: "knowledge",
|
||
items: [
|
||
{ href: "/precedents", label: "ספריית פסיקה" },
|
||
{ href: "/missing-precedents", label: "פסיקה חסרה" },
|
||
{ href: "/training", label: "אימון סגנון" },
|
||
{ href: "/methodology", label: "מתודולוגיה" },
|
||
],
|
||
},
|
||
];
|
||
|
||
const ADMIN_ITEMS: NavItem[] = [
|
||
{ href: "/skills", label: "מיומנויות" },
|
||
{ href: "/diagnostics", label: "אבחון" },
|
||
{ href: "/settings", label: "הגדרות" },
|
||
];
|
||
|
||
type AgentBoard = { prefix: string; label: string; hint: string };
|
||
|
||
const AGENT_BOARDS: AgentBoard[] = [
|
||
{ prefix: "CMP", label: "רישוי ובניה", hint: "תיקי 1xxx" },
|
||
{ prefix: "CMPA", label: "היטלי השבחה", hint: "תיקי 8xxx / 9xxx" },
|
||
];
|
||
|
||
const PAPERCLIP_BASE = "https://pc.nautilus.marcusgroup.org";
|
||
|
||
function isActive(pathname: string, href: string): boolean {
|
||
if (href === "/") return pathname === "/";
|
||
return pathname === href || pathname.startsWith(`${href}/`);
|
||
}
|
||
|
||
export function AppShell({ children }: { children: ReactNode }) {
|
||
const pathname = usePathname();
|
||
const subtitle = headerSubtitle(pathname);
|
||
const adminActive = ADMIN_ITEMS.some((i) => isActive(pathname, i.href));
|
||
|
||
return (
|
||
<>
|
||
<header
|
||
className="
|
||
relative z-10 flex flex-col
|
||
bg-navy text-parchment
|
||
border-b-[3px] border-gold
|
||
shadow-md
|
||
"
|
||
>
|
||
{/* ─── Row 1 — brand bar (3-column grid) ─── */}
|
||
{/* Side columns flex 1fr each so the search column stays centered on
|
||
the viewport regardless of how wide the brand or agent labels grow. */}
|
||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(280px,460px)_minmax(0,1fr)] items-center gap-4 px-10 pt-[14px] pb-2">
|
||
<Link
|
||
href="/"
|
||
className="flex items-baseline gap-3 hover:text-parchment min-w-0 justify-self-start"
|
||
>
|
||
<span className="font-display text-[1.45rem] font-bold tracking-[0.02em] text-parchment whitespace-nowrap">
|
||
עוזר משפטי
|
||
</span>
|
||
<span
|
||
className="text-gold-soft text-sm font-medium truncate"
|
||
aria-live="polite"
|
||
>
|
||
{subtitle}
|
||
</span>
|
||
</Link>
|
||
|
||
<div className="w-full justify-self-center">
|
||
<GlobalSearch />
|
||
</div>
|
||
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger
|
||
className="
|
||
justify-self-end flex items-baseline gap-2 px-3 py-1.5 rounded
|
||
transition-colors outline-none
|
||
text-parchment/80 hover:text-parchment hover:bg-navy-soft/60
|
||
focus-visible:ring-2 focus-visible:ring-gold/60
|
||
data-[state=open]:bg-navy-soft/80 data-[state=open]:text-parchment
|
||
"
|
||
aria-label="ניהול סוכנים — בחר ועדה"
|
||
>
|
||
<span className="font-display text-[1.45rem] font-bold tracking-[0.02em] text-parchment whitespace-nowrap">
|
||
ניהול סוכנים
|
||
</span>
|
||
<ChevronDown className="size-4 self-center text-gold-soft" aria-hidden="true" />
|
||
</DropdownMenuTrigger>
|
||
|
||
<DropdownMenuContent align="end" sideOffset={10} className="min-w-[240px]">
|
||
<DropdownMenuLabel className="text-xs text-muted-foreground text-center">
|
||
Paperclip פתח דאשבורד
|
||
</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
{AGENT_BOARDS.map((board) => (
|
||
<DropdownMenuItem key={board.prefix} asChild>
|
||
<a
|
||
href={`${PAPERCLIP_BASE}/${board.prefix}/dashboard`}
|
||
target="_blank"
|
||
rel="noreferrer noopener"
|
||
className="flex flex-col gap-0.5 cursor-pointer py-1.5"
|
||
>
|
||
<span className="font-medium whitespace-nowrap">{board.label}</span>
|
||
<span className="text-xs text-muted-foreground tracking-wide whitespace-nowrap">
|
||
<span className="font-mono">{board.prefix}</span> · {board.hint}
|
||
</span>
|
||
</a>
|
||
</DropdownMenuItem>
|
||
))}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
|
||
{/* ─── Row 2 — section nav ─── */}
|
||
<div className="flex items-center gap-3 px-10 pt-1 pb-[18px]">
|
||
<nav className="flex items-center gap-3" aria-label="ניווט ראשי">
|
||
{NAV_GROUPS.map((group, idx) => (
|
||
<div key={group.id} className="flex items-center">
|
||
{idx > 0 && (
|
||
<span
|
||
className="mx-2 h-4 w-px bg-parchment/20"
|
||
aria-hidden="true"
|
||
/>
|
||
)}
|
||
<div className="flex items-center gap-1">
|
||
{group.items.map((item) => (
|
||
<NavLink key={item.href} item={item} active={isActive(pathname, item.href)} />
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</nav>
|
||
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger
|
||
className={`
|
||
relative ms-auto shrink-0 flex items-center gap-1.5
|
||
px-3 py-1.5 rounded text-sm transition-colors outline-none
|
||
focus-visible:ring-2 focus-visible:ring-gold/60
|
||
${adminActive
|
||
? "text-parchment font-semibold bg-navy-soft/80"
|
||
: "text-parchment/80 hover:text-parchment hover:bg-navy-soft/60"}
|
||
data-[state=open]:bg-navy-soft/80 data-[state=open]:text-parchment
|
||
`}
|
||
aria-label="הגדרות מערכת"
|
||
>
|
||
<Settings className="size-4" aria-hidden="true" />
|
||
<ChevronDown className="size-3" aria-hidden="true" />
|
||
{adminActive && (
|
||
<span
|
||
className="absolute -bottom-[19px] inset-x-2 h-[2px] bg-gold"
|
||
aria-hidden="true"
|
||
/>
|
||
)}
|
||
</DropdownMenuTrigger>
|
||
|
||
<DropdownMenuContent align="end" sideOffset={10} className="min-w-[180px]">
|
||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||
מערכת
|
||
</DropdownMenuLabel>
|
||
<DropdownMenuSeparator />
|
||
{ADMIN_ITEMS.map((item) => {
|
||
const active = isActive(pathname, item.href);
|
||
return (
|
||
<DropdownMenuItem key={item.href} asChild>
|
||
<Link
|
||
href={item.href}
|
||
aria-current={active ? "page" : undefined}
|
||
className={`cursor-pointer ${active ? "font-semibold" : ""}`}
|
||
>
|
||
{item.label}
|
||
</Link>
|
||
</DropdownMenuItem>
|
||
);
|
||
})}
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
</div>
|
||
</header>
|
||
|
||
<main
|
||
id="main"
|
||
className="flex-1 w-full max-w-[1400px] mx-auto px-10 py-10"
|
||
>
|
||
{children}
|
||
</main>
|
||
</>
|
||
);
|
||
}
|
||
|
||
function NavLink({ item, active }: { item: NavItem; active: boolean }) {
|
||
return (
|
||
<Link
|
||
href={item.href}
|
||
aria-current={active ? "page" : undefined}
|
||
className={`
|
||
relative px-3 py-1.5 rounded text-sm transition-colors
|
||
${active
|
||
? "text-parchment font-semibold bg-navy-soft/80"
|
||
: "text-parchment/80 hover:text-parchment hover:bg-navy-soft/60"}
|
||
`}
|
||
>
|
||
<span>{item.label}</span>
|
||
{item.href === "/missing-precedents" ? <MissingPrecedentsBadge /> : null}
|
||
{item.href === "/approvals" ? <ApprovalsBadge /> : null}
|
||
{active && (
|
||
<span
|
||
className="absolute -bottom-[19px] inset-x-2 h-[2px] bg-gold"
|
||
aria-hidden="true"
|
||
/>
|
||
)}
|
||
</Link>
|
||
);
|
||
}
|
||
|
||
/* Total pending-approvals badge next to "מרכז אישורים" — Dafna's outstanding
|
||
* human-gate items (halachot + missing precedents + feedback + qa_failed).
|
||
* Renders only when >0 so the nav stays quiet when everything is cleared. */
|
||
function ApprovalsBadge() {
|
||
const { data } = usePendingApprovals();
|
||
const total = data?.total_pending ?? 0;
|
||
if (!total) return null;
|
||
return (
|
||
<span
|
||
className="ms-1 inline-flex items-center justify-center min-w-[1.25rem] h-4 px-1 rounded-full bg-gold text-navy text-[0.65rem] font-semibold"
|
||
aria-label={`${total} פריטים ממתינים לאישורך`}
|
||
>
|
||
{total}
|
||
</span>
|
||
);
|
||
}
|
||
|
||
/* Small open-count badge next to "פסיקה חסרה" — only renders when >0
|
||
* so the nav stays quiet in normal operation. */
|
||
function MissingPrecedentsBadge() {
|
||
const { data: openCount } = useMissingPrecedentsOpenCount();
|
||
if (!openCount) return null;
|
||
return (
|
||
<span
|
||
className="ms-1 inline-flex items-center justify-center min-w-[1.25rem] h-4 px-1 rounded-full bg-gold text-navy text-[0.65rem] font-semibold"
|
||
aria-label={`${openCount} פסיקות חסרות פתוחות`}
|
||
>
|
||
{openCount}
|
||
</span>
|
||
);
|
||
}
|