chore: restore MVP dashboard components from backup

This commit is contained in:
fatemeh sane
2026-10-08 11:16:15 +03:30
parent d38cf38bd9
commit b0776da061
13 changed files with 1628 additions and 0 deletions
@@ -0,0 +1,169 @@
// ============================================================
// ویجت ۱ — ماتریس Drill-Down (کل سازمان → پروژه → انبار)
// نمودار ستونی تعاملی: کلیک روی هر ستون پروژه، به انبارهای همان
// پروژه رفته و مسیر (breadcrumb) به‌روز می‌شود — سبک Power BI.
// مقادیر هرگز بین واحدهای ناسازگار مخلوط نمی‌شوند: واحد فعال با
// چیپ‌های بالای نمودار انتخاب می‌شود.
// ============================================================
import { useMemo, useState } from "react";
import {
Bar,
BarChart,
CartesianGrid,
Cell,
ResponsiveContainer,
Tooltip,
XAxis,
YAxis,
} from "recharts";
import { ChevronLeft } from "lucide-react";
import { useDrilldown } from "../../hooks/useDashboardData";
import { faNum } from "../../lib/formatters";
import {
CHART_COLORS,
ChartCard,
ChartEmpty,
ChartLoading,
ChartTooltip,
UnitPicker,
faAxisTick,
} from "./chartShared";
interface Slice {
id: string | null;
name: string;
value: number;
rows: number;
}
export function DrilldownMatrix() {
const query = useDrilldown();
const [unit, setUnit] = useState<string | null>(null);
const [project, setProject] = useState<{ id: string; name: string } | null>(null);
const data = query.data;
// واحد فعال: انتخاب کاربر یا بزرگ‌ترین مجموع (پایدار بین رندرها)
const activeUnit = useMemo(() => {
if (unit && data?.units?.[unit]) return unit;
const entries = Object.entries(data?.total?.qty_by_unit ?? {});
if (!entries.length) return null;
return entries.sort((a, b) => b[1] - a[1])[0][0];
}, [unit, data]);
const slices: Slice[] = useMemo(() => {
if (!data) return [];
const pick = (q: Record<string, number>) => (activeUnit ? q[activeUnit] ?? 0 : 0);
if (project) {
const p = data.projects.find((x) => x.id === project.id);
return (p?.warehouses ?? []).map((w) => ({ id: w.id, name: w.name, value: pick(w.qty_by_unit), rows: w.rows }));
}
return data.projects.map((p) => ({ id: p.id, name: p.name, value: pick(p.qty_by_unit), rows: p.rows }));
}, [data, project, activeUnit]);
const unitLabel = activeUnit ? data?.units?.[activeUnit] || activeUnit : "";
const levelTotal = slices.reduce((s, x) => s + x.value, 0);
return (
<ChartCard
title="ماتریس توزیع موجودی — کجا ذخیره شده؟"
sub={
project
? `انبارهای پروژه «${project.name}» — برای بازگشت روی مسیر کلیک کنید`
: "سهم هر پروژه از کل سازمان — روی هر ستون کلیک کنید تا انبارهای آن پروژه را ببینید"
}
actions={
data ? <UnitPicker units={data.units} value={activeUnit ?? ""} onChange={setUnit} /> : null
}
>
{/* مسیر Drill-Down */}
<nav dir="rtl" className="mb-2 flex flex-wrap items-center gap-1 text-[11.5px]">
<button
type="button"
onClick={() => setProject(null)}
className={`rounded-md px-2 py-0.5 font-bold transition-colors ${
project ? "text-muted hover:bg-slate-bg hover:text-ink" : "bg-accent-bg text-accent"
}`}
>
کل سازمان
</button>
{project && (
<>
<ChevronLeft className="h-3.5 w-3.5 text-muted" />
<span className="rounded-md bg-accent-bg px-2 py-0.5 font-bold text-accent">
{project.name}
</span>
<span className="text-muted">← انبار</span>
</>
)}
<span className="ms-auto text-[11px] tabular-nums text-muted">
جمع سطح فعال: {faNum(levelTotal.toLocaleString("en-US"))} {unitLabel}
</span>
</nav>
{query.isLoading ? (
<ChartLoading />
) : !slices.length || levelTotal === 0 ? (
<ChartEmpty message="موجودی‌ای با این فیلترها یافت نشد" />
) : (
<div dir="ltr" className="h-64 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart data={slices} margin={{ top: 24, right: 8, left: 8, bottom: 4 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#e5e9f0" vertical={false} />
<XAxis
dataKey="name"
tick={{ fontSize: 11, fill: "#64748b", fontFamily: "Vazirmatn" }}
tickLine={false}
axisLine={{ stroke: "#e5e9f0" }}
interval={0}
height={44}
angle={slices.length > 4 ? -18 : 0}
textAnchor={slices.length > 4 ? "end" : "middle"}
/>
<YAxis
tick={{ fontSize: 11, fill: "#64748b" }}
tickFormatter={faAxisTick}
tickLine={false}
axisLine={false}
width={56}
/>
<Tooltip
cursor={{ fill: "rgba(79, 70, 229, 0.06)" }}
content={
<ChartTooltip
labelFormatter={(l) => String(l)}
valueFormatter={(v) => `${faNum(v.toLocaleString("en-US"))} ${unitLabel}`}
/>
}
/>
<Bar
dataKey="value"
name="موجودی"
radius={[6, 6, 0, 0]}
maxBarSize={64}
isAnimationActive={false}
onClick={(entry: unknown) => {
// Recharts ورودی ستون را در آرگومان اول می‌دهد
const payload = (entry as { payload?: Slice })?.payload;
if (!project && payload?.id) {
setProject({ id: payload.id, name: payload.name });
}
}}
className={project ? "" : "cursor-pointer"}
>
{slices.map((s) => (
<Cell
key={s.id ?? s.name}
fill={project ? CHART_COLORS.accent : "#818cf8"}
fillOpacity={project ? 1 : 0.9}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
</div>
)}
</ChartCard>
);
}