Adds accounting-service PATCH/archive, fiscal helpers, COA templates and setup status, plus SuperApp Accounting UI (DS, scoreboard, masters, vouchers, ledger, ops modules) with session refresh and HTTPS public API URLs. Co-authored-by: Cursor <cursoragent@cursor.com>
144 lines
3.8 KiB
TypeScript
144 lines
3.8 KiB
TypeScript
"use client";
|
||
|
||
import { cn } from "@/lib/utils";
|
||
|
||
export function Tabs({
|
||
items,
|
||
value,
|
||
onChange,
|
||
className,
|
||
}: {
|
||
items: { id: string; label: string }[];
|
||
value: string;
|
||
onChange: (id: string) => void;
|
||
className?: string;
|
||
}) {
|
||
return (
|
||
<div
|
||
role="tablist"
|
||
className={cn(
|
||
"flex gap-1 overflow-x-auto rounded-xl border border-[var(--border)] bg-[var(--surface-muted)] p-1",
|
||
className
|
||
)}
|
||
>
|
||
{items.map((item) => {
|
||
const active = item.id === value;
|
||
return (
|
||
<button
|
||
key={item.id}
|
||
type="button"
|
||
role="tab"
|
||
aria-selected={active}
|
||
onClick={() => onChange(item.id)}
|
||
className={cn(
|
||
"whitespace-nowrap rounded-lg px-3 py-1.5 text-sm font-medium transition-colors",
|
||
active
|
||
? "bg-[var(--surface)] text-secondary shadow-sm"
|
||
: "text-[var(--muted)] hover:text-secondary"
|
||
)}
|
||
>
|
||
{item.label}
|
||
</button>
|
||
);
|
||
})}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function TableToolbar({
|
||
search,
|
||
onSearchChange,
|
||
searchPlaceholder = "جستجو…",
|
||
actions,
|
||
className,
|
||
}: {
|
||
search?: string;
|
||
onSearchChange?: (v: string) => void;
|
||
searchPlaceholder?: string;
|
||
actions?: React.ReactNode;
|
||
className?: string;
|
||
}) {
|
||
return (
|
||
<div className={cn("mb-4 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between", className)}>
|
||
{onSearchChange ? (
|
||
<input
|
||
value={search ?? ""}
|
||
onChange={(e) => onSearchChange(e.target.value)}
|
||
placeholder={searchPlaceholder}
|
||
className="h-10 w-full max-w-sm rounded-xl border border-[var(--border)] bg-[var(--surface)] px-3 text-sm"
|
||
/>
|
||
) : (
|
||
<div />
|
||
)}
|
||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function Breadcrumbs({
|
||
items,
|
||
}: {
|
||
items: { label: string; href?: string }[];
|
||
}) {
|
||
return (
|
||
<nav aria-label="مسیر" className="mb-3 flex flex-wrap items-center gap-1 text-xs text-[var(--muted)]">
|
||
{items.map((item, i) => (
|
||
<span key={`${item.label}-${i}`} className="flex items-center gap-1">
|
||
{i > 0 ? <span>/</span> : null}
|
||
{item.href ? (
|
||
<a href={item.href} className="hover:text-secondary">
|
||
{item.label}
|
||
</a>
|
||
) : (
|
||
<span className="text-secondary">{item.label}</span>
|
||
)}
|
||
</span>
|
||
))}
|
||
</nav>
|
||
);
|
||
}
|
||
|
||
export function Skeleton({ className }: { className?: string }) {
|
||
return <div className={cn("animate-pulse rounded-xl bg-[var(--surface-muted)]", className)} />;
|
||
}
|
||
|
||
export function Pagination({
|
||
page,
|
||
pageSize,
|
||
total,
|
||
onPageChange,
|
||
}: {
|
||
page: number;
|
||
pageSize: number;
|
||
total: number;
|
||
onPageChange: (page: number) => void;
|
||
}) {
|
||
const pages = Math.max(1, Math.ceil(total / pageSize));
|
||
if (pages <= 1) return null;
|
||
return (
|
||
<div className="mt-4 flex items-center justify-between text-sm">
|
||
<span className="text-[var(--muted)]">
|
||
صفحه {page} از {pages}
|
||
</span>
|
||
<div className="flex gap-2">
|
||
<button
|
||
type="button"
|
||
disabled={page <= 1}
|
||
className="rounded-lg border border-[var(--border)] px-3 py-1.5 disabled:opacity-40"
|
||
onClick={() => onPageChange(page - 1)}
|
||
>
|
||
قبلی
|
||
</button>
|
||
<button
|
||
type="button"
|
||
disabled={page >= pages}
|
||
className="rounded-lg border border-[var(--border)] px-3 py-1.5 disabled:opacity-40"
|
||
onClick={() => onPageChange(page + 1)}
|
||
>
|
||
بعدی
|
||
</button>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|