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>
140 lines
4.3 KiB
TypeScript
140 lines
4.3 KiB
TypeScript
"use client";
|
||
|
||
import { cn } from "@/lib/utils";
|
||
import { Inbox, Loader2, AlertCircle } from "lucide-react";
|
||
import { Button } from "./Button";
|
||
|
||
export function PageHeader({
|
||
title,
|
||
description,
|
||
actions,
|
||
}: {
|
||
title: string;
|
||
description?: string;
|
||
actions?: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<div className="mb-6 flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||
<div>
|
||
<h1 className="text-2xl font-bold tracking-tight text-secondary">{title}</h1>
|
||
{description ? <p className="mt-1 text-sm text-[var(--muted)]">{description}</p> : null}
|
||
</div>
|
||
{actions ? <div className="flex flex-wrap items-center gap-2">{actions}</div> : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function EmptyState({
|
||
title = "موردی یافت نشد",
|
||
description = "هنوز دادهای ثبت نشده است.",
|
||
action,
|
||
}: {
|
||
title?: string;
|
||
description?: string;
|
||
action?: React.ReactNode;
|
||
}) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center gap-3 rounded-2xl border border-dashed border-[var(--border)] bg-[var(--surface)] px-6 py-16 text-center">
|
||
<div className="rounded-full bg-[var(--surface-muted)] p-3">
|
||
<Inbox className="h-6 w-6 text-[var(--muted)]" />
|
||
</div>
|
||
<div>
|
||
<p className="font-medium text-secondary">{title}</p>
|
||
<p className="mt-1 text-sm text-[var(--muted)]">{description}</p>
|
||
</div>
|
||
{action}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function LoadingState({ label = "در حال بارگذاری…" }: { label?: string }) {
|
||
return (
|
||
<div className="flex items-center justify-center gap-2 py-16 text-sm text-[var(--muted)]">
|
||
<Loader2 className="h-5 w-5 animate-spin" />
|
||
{label}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function ErrorState({
|
||
message,
|
||
onRetry,
|
||
}: {
|
||
message: string;
|
||
onRetry?: () => void;
|
||
}) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center gap-3 rounded-2xl border border-red-200 bg-red-50 px-6 py-12 text-center dark:border-red-900 dark:bg-red-950/30">
|
||
<AlertCircle className="h-6 w-6 text-red-600" />
|
||
<p className="text-sm text-red-700 dark:text-red-300">{message}</p>
|
||
{onRetry ? (
|
||
<Button type="button" variant="outline" size="sm" onClick={onRetry}>
|
||
تلاش مجدد
|
||
</Button>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function DataTable({
|
||
columns,
|
||
rows,
|
||
empty,
|
||
}: {
|
||
columns: { key: string; header: string; className?: string; render?: (row: Record<string, unknown>) => React.ReactNode }[];
|
||
rows: Record<string, unknown>[];
|
||
empty?: React.ReactNode;
|
||
}) {
|
||
if (!rows.length) return <>{empty}</>;
|
||
return (
|
||
<div className="overflow-x-auto rounded-2xl border border-[var(--border)] bg-[var(--surface)]">
|
||
<table className="w-full min-w-[640px] text-sm">
|
||
<thead className="bg-[var(--surface-muted)] text-right">
|
||
<tr>
|
||
{columns.map((c) => (
|
||
<th
|
||
key={c.key}
|
||
className={cn(
|
||
"px-4 py-3 font-medium text-[var(--muted)] first:rounded-tr-2xl last:rounded-tl-2xl",
|
||
c.className
|
||
)}
|
||
>
|
||
{c.header}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{rows.map((row, i) => (
|
||
<tr key={String(row.id ?? i)} className="border-t border-[var(--border)] hover:bg-[var(--surface-muted)]/60">
|
||
{columns.map((c) => (
|
||
<td key={c.key} className={cn("px-4 py-3 text-secondary", c.className)}>
|
||
{c.render ? c.render(row) : String(row[c.key] ?? "—")}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export function StatCard({
|
||
label,
|
||
value,
|
||
hint,
|
||
}: {
|
||
label: string;
|
||
value: string | number;
|
||
hint?: string;
|
||
}) {
|
||
return (
|
||
<div className="rounded-2xl border border-[var(--border)] bg-[var(--surface)] p-5 shadow-[var(--shadow-sm)]">
|
||
<p className="text-sm text-[var(--muted)]">{label}</p>
|
||
<p className="mt-2 text-2xl font-bold text-secondary">{value}</p>
|
||
{hint ? <p className="mt-1 text-xs text-[var(--muted)]">{hint}</p> : null}
|
||
</div>
|
||
);
|
||
}
|