feat: mobile UI optimizations
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { Link, useLocation, useSearchParams } from "react-router-dom";
|
||||
import { Plus, Warehouse as WarehouseIcon } from "lucide-react";
|
||||
import { Plus, Warehouse as WarehouseIcon, SlidersHorizontal, ArrowUpDown, X } from "lucide-react";
|
||||
import { Header, PageLayout } from "../../components/layout/Header";
|
||||
import { EmptyState } from "../../components/EmptyState";
|
||||
import { Badge } from "../../components/ui/badge";
|
||||
@@ -12,6 +12,7 @@ import { useInventoryData, brandName, catName, materialName, unitName, whName }
|
||||
import type { InventoryRow, InventoryTransaction, TransactionType } from "../../lib/types";
|
||||
import { faNum } from "../../lib/formatters";
|
||||
import { isoToJalaliStr } from "../../lib/jalali";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
// رنگبندی معنایی: سبز زمردی = ورود/افزایش، رز = خروج/کاهش، آبی = خنثی/جابهجایی
|
||||
const TX_META: Record<TransactionType, { label: string; color: string; icon: string }> = {
|
||||
@@ -47,25 +48,23 @@ function specsHash(specs: Record<string, string> | null | undefined): string {
|
||||
return keys.map((k) => `${k}=${s[k]}`).join("|");
|
||||
}
|
||||
|
||||
// داشبورد انبار — پورت کامل نسخهی ارسالی (گروهبندی + خط زمانی + فیلترها)
|
||||
export function InventoryPage() {
|
||||
const location = useLocation();
|
||||
const user = useAuthStore((s) => s.user);
|
||||
const { warehouses, inventory, transactions, lookup, loading } = useInventoryData();
|
||||
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager"; // مدیر تدارکات = ادمین دفتر
|
||||
const isAdmin = user?.role === "admin" || user?.role === "procurement_manager";
|
||||
const isPM = user?.role === "pm";
|
||||
// پرش عمیق از «انبارها»: /inventory?warehouse=<id> → داشبورد همان انبار از پیش فیلتر شده
|
||||
const [params] = useSearchParams();
|
||||
|
||||
const [filters, setFilters] = useState<{ category: string; warehouse: string; material: string; project: string }>({
|
||||
category: "", warehouse: params.get("warehouse") || "", material: "", project: "",
|
||||
});
|
||||
const [sort, setSort] = useState<"asc" | "desc" | "">("");
|
||||
const [showMobileFilters, setShowMobileFilters] = useState(false);
|
||||
const [expandedKey, setExpandedKey] = useState<string | null>(null);
|
||||
const [selTx, setSelTx] = useState<string | null>(null);
|
||||
const [expBrand, setExpBrand] = useState<string | null>(null);
|
||||
|
||||
// ادمین: انبارهای همه پروژهها یا پروژهی انتخابی
|
||||
const visibleWarehouses = useMemo(() => {
|
||||
if (isAdmin && filters.project) return warehouses.filter((w) => w.project_id === filters.project);
|
||||
if (isAdmin) return warehouses;
|
||||
@@ -120,7 +119,7 @@ export function InventoryPage() {
|
||||
.slice()
|
||||
.sort((a, b) => new Date(a.transaction_date).getTime() - new Date(b.transaction_date).getTime());
|
||||
|
||||
const hasFilter = !!(filters.category || filters.warehouse || filters.material || (isAdmin && filters.project));
|
||||
const hasFilter = !!(filters.category || filters.warehouse || (isAdmin && filters.project));
|
||||
|
||||
if (loading) return <div className="min-h-screen" />;
|
||||
if (!user || (!isAdmin && !isPM)) {
|
||||
@@ -135,119 +134,171 @@ export function InventoryPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<div className="min-h-screen pb-20 md:pb-6">
|
||||
<Header
|
||||
title="سامانه مدیریت انبار"
|
||||
sub={inventorySubheader(user)}
|
||||
icon={<WarehouseIcon className="h-4.5 w-4.5" strokeWidth={2} />}
|
||||
menu={<AppSwitcherMenu activeModule="warehouse" canSwitchToProcurement={isAdmin} />}
|
||||
actions={
|
||||
<>
|
||||
<InventoryTopNav pathname={location.pathname} variant={isAdmin ? "admin" : "pm"} />
|
||||
<div className="flex items-center gap-2">
|
||||
{/* تبهای تکراری در موبایل مخفی و فقط در دسکتاپ نمایش داده میشوند */}
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<InventoryTopNav pathname={location.pathname} variant={isAdmin ? "admin" : "pm"} />
|
||||
</div>
|
||||
{isPM && (
|
||||
<Link
|
||||
to="/inventory/change"
|
||||
className="btn-line-file no-underline bg-accent text-white hover:bg-accent-dark"
|
||||
className="btn-line-file no-underline bg-accent text-white hover:bg-accent-dark h-8 px-2.5 text-xs flex items-center gap-1"
|
||||
>
|
||||
<Plus className="h-3.5 w-3.5" />
|
||||
تغییر موجودی
|
||||
<span>تغییر موجودی</span>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<PageLayout className="flex flex-col gap-3">
|
||||
{/* فیلترها */}
|
||||
<div className="flex flex-wrap items-end gap-2 rounded-xl border border-line bg-panel p-3">
|
||||
<div className="field">
|
||||
<label>دستهبندی</label>
|
||||
<select
|
||||
className="h-8 rounded-lg border border-line bg-panel px-2 text-[12.5px]"
|
||||
value={filters.category}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, category: e.target.value, material: "" }))}
|
||||
<PageLayout className="flex flex-col gap-3 px-3 md:px-6">
|
||||
{/* نوار فیلتر و جستجوی واکنشگرا */}
|
||||
<div className="rounded-xl border border-line bg-panel p-2.5 sm:p-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative flex-1">
|
||||
<input
|
||||
className="h-9 w-full rounded-lg border border-line bg-bg/50 px-3 text-xs focus:border-accent focus:bg-panel focus:outline-none"
|
||||
placeholder="جستجوی نام متریال…"
|
||||
value={filters.material}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, material: e.target.value }))}
|
||||
/>
|
||||
{filters.material && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setFilters((f) => ({ ...f, material: "" }))}
|
||||
className="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted hover:text-ink"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowMobileFilters((s) => !s)}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center gap-1.5 rounded-lg border px-2.5 text-xs font-medium transition-colors md:hidden",
|
||||
hasFilter ? "border-accent bg-accent/10 text-accent font-bold" : "border-line bg-panel text-ink hover:bg-bg"
|
||||
)}
|
||||
>
|
||||
<option value="">همه دستهبندیها</option>
|
||||
{lookup.categories.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<SlidersHorizontal className="h-3.5 w-3.5" />
|
||||
<span>فیلترها</span>
|
||||
{hasFilter && <span className="h-1.5 w-1.5 rounded-full bg-accent" />}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center gap-1 rounded-lg border px-2.5 text-xs transition-colors",
|
||||
sort ? "border-accent bg-accent/5 text-accent font-semibold" : "border-line bg-panel text-ink hover:bg-bg"
|
||||
)}
|
||||
onClick={() => setSort((s) => (s === "" ? "asc" : s === "asc" ? "desc" : ""))}
|
||||
title="مرتبسازی تعداد"
|
||||
>
|
||||
<ArrowUpDown className="h-3.5 w-3.5" />
|
||||
<span className="hidden sm:inline">
|
||||
{sort === "asc" ? "تعداد: صعودی" : sort === "desc" ? "تعداد: نزولی" : "مرتبسازی"}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<Badge variant="outline" className="hidden sm:inline-flex h-9 items-center px-2.5 text-xs">
|
||||
{faNum(groups.length)} قلم
|
||||
</Badge>
|
||||
</div>
|
||||
{isAdmin && (
|
||||
<div className="field">
|
||||
<label>پروژه</label>
|
||||
|
||||
{/* گزینههای فیلتر پیشرفته: در موبایل با دکمه باز میشود و در دسکتاپ همیشه باز است */}
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3 grid grid-cols-1 gap-2.5 border-t border-line/60 pt-3 sm:grid-cols-2 md:flex md:flex-wrap md:items-end",
|
||||
!showMobileFilters && "hidden md:flex"
|
||||
)}
|
||||
>
|
||||
<div className="field flex-1 min-w-[140px]">
|
||||
<label className="text-[11px] font-semibold text-muted">دستهبندی</label>
|
||||
<select
|
||||
className="h-8 rounded-lg border border-line bg-panel px-2 text-[12.5px]"
|
||||
value={filters.project}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, project: e.target.value, warehouse: "", material: "" }))}
|
||||
className="h-8 w-full rounded-lg border border-line bg-panel px-2 text-xs"
|
||||
value={filters.category}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, category: e.target.value, material: "" }))}
|
||||
>
|
||||
<option value="">همه پروژهها</option>
|
||||
{lookup.projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
<option value="">همه دستهبندیها</option>
|
||||
{lookup.categories.map((c) => (
|
||||
<option key={c.id} value={c.id}>{c.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
<div className="field">
|
||||
<label>انبار</label>
|
||||
<select
|
||||
className="h-8 rounded-lg border border-line bg-panel px-2 text-[12.5px]"
|
||||
value={filters.warehouse}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, warehouse: e.target.value, material: "" }))}
|
||||
>
|
||||
<option value="">همه انبارها</option>
|
||||
{visibleWarehouses.map((w) => (
|
||||
<option key={w.id} value={w.id}>
|
||||
{w.name}{isAdmin ? ` — ${lookup.projects.find((p) => p.id === w.project_id)?.name || ""}` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{isAdmin && (
|
||||
<div className="field flex-1 min-w-[140px]">
|
||||
<label className="text-[11px] font-semibold text-muted">پروژه</label>
|
||||
<select
|
||||
className="h-8 w-full rounded-lg border border-line bg-panel px-2 text-xs"
|
||||
value={filters.project}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, project: e.target.value, warehouse: "", material: "" }))}
|
||||
>
|
||||
<option value="">همه پروژهها</option>
|
||||
{lookup.projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>{p.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="field flex-1 min-w-[140px]">
|
||||
<label className="text-[11px] font-semibold text-muted">انبار</label>
|
||||
<select
|
||||
className="h-8 w-full rounded-lg border border-line bg-panel px-2 text-xs"
|
||||
value={filters.warehouse}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, warehouse: e.target.value, material: "" }))}
|
||||
>
|
||||
<option value="">همه انبارها</option>
|
||||
{visibleWarehouses.map((w) => (
|
||||
<option key={w.id} value={w.id}>
|
||||
{w.name}{isAdmin ? ` — ${lookup.projects.find((p) => p.id === w.project_id)?.name || ""}` : ""}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{hasFilter && (
|
||||
<button
|
||||
type="button"
|
||||
className="h-8 rounded-lg border border-red/30 bg-red/5 px-3 text-xs text-red hover:bg-red/10 transition-colors"
|
||||
onClick={() => setFilters({ category: "", warehouse: "", material: "", project: "" })}
|
||||
>
|
||||
✕ پاک کردن فیلترها
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="text-[11px] text-muted ms-auto sm:hidden">
|
||||
نمایش {faNum(groups.length)} قلم کالا
|
||||
</div>
|
||||
</div>
|
||||
<div className="field">
|
||||
<label>متریال</label>
|
||||
<input
|
||||
className="h-8 w-44 rounded-lg border border-line bg-panel px-2 text-[12.5px]"
|
||||
placeholder="تایپ نام متریال…"
|
||||
value={filters.material}
|
||||
onChange={(e) => setFilters((f) => ({ ...f, material: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
className="h-8 rounded-lg border border-line bg-panel px-3 text-[12px]"
|
||||
onClick={() => setSort((s) => (s === "" ? "asc" : s === "asc" ? "desc" : ""))}
|
||||
>
|
||||
{sort === "asc" ? "تعداد: صعودی ▲" : sort === "desc" ? "تعداد: نزولی ▼" : "مرتبسازی تعداد"}
|
||||
</button>
|
||||
{hasFilter && (
|
||||
<button
|
||||
className="h-8 rounded-lg border border-red/30 bg-red/5 px-3 text-[12px] text-red"
|
||||
onClick={() => setFilters({ category: "", warehouse: "", material: "", project: "" })}
|
||||
>
|
||||
✕ پاک کردن فیلترها
|
||||
</button>
|
||||
)}
|
||||
{filters.warehouse && (
|
||||
<Badge variant="offered">🏬 {whName(warehouses, filters.warehouse)}</Badge>
|
||||
)}
|
||||
<Badge variant="outline">{faNum(groups.length)} قلم کالا</Badge>
|
||||
</div>
|
||||
|
||||
{/* جدول گروهبندیشده */}
|
||||
<div className="overflow-hidden rounded-xl border border-line bg-panel">
|
||||
{/* جدول موجودی انبار با اسکرول افقی محافظتشده */}
|
||||
<div className="overflow-x-auto rounded-xl border border-line bg-panel shadow-2xs [scrollbar-width:thin]">
|
||||
{!groups.length ? (
|
||||
<EmptyState emoji="📋" text="هیچ رکورد موجودیای مطابق فیلترهای انتخابی یافت نشد." />
|
||||
) : (
|
||||
<table className="w-full text-[12.5px]">
|
||||
<table className="w-full min-w-[640px] text-[12.5px]">
|
||||
<thead>
|
||||
<tr className="border-b border-line bg-panel text-right text-muted">
|
||||
<th className="w-8 py-2" />
|
||||
<th className="py-2 pe-3">متریال</th>
|
||||
<th className="py-2 pe-3">دستهبندی</th>
|
||||
<th className="py-2 pe-3">انبار</th>
|
||||
<th className="py-2 pe-3">مشخصه فنی</th>
|
||||
<th className="py-2 pe-3">تعداد</th>
|
||||
<th className="py-2 pe-3">واحد</th>
|
||||
<th className="py-2 pe-3">آخرین بهروزرسانی</th>
|
||||
<th className="w-8 py-2" />
|
||||
<tr className="border-b border-line bg-bg/40 text-right text-muted">
|
||||
<th className="w-8 py-2.5" />
|
||||
<th className="py-2.5 pe-3 font-semibold">متریال</th>
|
||||
<th className="py-2.5 pe-3 font-semibold">دستهبندی</th>
|
||||
<th className="py-2.5 pe-3 font-semibold">انبار</th>
|
||||
<th className="py-2.5 pe-3 font-semibold">مشخصه فنی</th>
|
||||
<th className="py-2.5 pe-3 font-semibold">تعداد</th>
|
||||
<th className="py-2.5 pe-3 font-semibold">واحد</th>
|
||||
<th className="py-2.5 pe-3 font-semibold">آخرین بهروزرسانی</th>
|
||||
<th className="w-8 py-2.5" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -295,7 +346,9 @@ export function InventoryPage() {
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted">
|
||||
|
||||
{/* متن راهنما فقط در دسکتاپ نمایش داده میشود */}
|
||||
<div className="hidden md:block text-[11px] text-muted">
|
||||
۱۰ نوع تراکنش استاندارد • سبز زمردی = ورود به انبار · رز = خروج/کاهش · آبی = انتقال و شمارش • کلیک روی هر سطر: خط زمانی با «مانده» لحظهبهلحظه و تفکیک برندها
|
||||
</div>
|
||||
</PageLayout>
|
||||
@@ -317,21 +370,21 @@ function GroupRow({ g, isExp, isAdmin, matName, matCatName, wh, projName, unit,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<tr className={`border-b border-line/60 cursor-pointer ${isExp ? "bg-accent/5" : "hover:bg-accent/5"}`} onClick={onToggle}>
|
||||
<td className="py-2 text-center text-muted">{isExp ? "▾" : "▸"}</td>
|
||||
<td className="py-2 pe-3 font-semibold">{matName}</td>
|
||||
<td className="py-2 pe-3">{matCatName}</td>
|
||||
<td className="py-2 pe-3">
|
||||
{wh}
|
||||
{isAdmin && <small className="block text-muted">{projName}</small>}
|
||||
<tr className={`border-b border-line/60 cursor-pointer transition-colors ${isExp ? "bg-accent/5" : "hover:bg-accent/5"}`} onClick={onToggle}>
|
||||
<td className="py-2.5 text-center text-muted">{isExp ? "▾" : "▸"}</td>
|
||||
<td className="py-2.5 pe-3 font-bold text-ink">{matName}</td>
|
||||
<td className="py-2.5 pe-3 text-muted">{matCatName}</td>
|
||||
<td className="py-2.5 pe-3">
|
||||
<div>{wh}</div>
|
||||
{isAdmin && <span className="block text-[10.5px] text-muted">{projName}</span>}
|
||||
</td>
|
||||
<td className="py-2 pe-3 text-muted">
|
||||
<td className="py-2.5 pe-3 text-muted">
|
||||
{Object.entries(g.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"}
|
||||
</td>
|
||||
<td className="py-2 pe-3 font-bold">{faNum(g.qty)}</td>
|
||||
<td className="py-2 pe-3">{unit}</td>
|
||||
<td className="py-2 pe-3 text-muted">{isoToJalaliStr(g.updatedAt)}</td>
|
||||
<td className="py-2 text-muted">🕘</td>
|
||||
<td className="py-2.5 pe-3 font-extrabold text-ink tabular-nums">{faNum(g.qty)}</td>
|
||||
<td className="py-2.5 pe-3">{unit}</td>
|
||||
<td className="py-2.5 pe-3 text-muted tabular-nums">{isoToJalaliStr(g.updatedAt)}</td>
|
||||
<td className="py-2.5 text-muted">🕘</td>
|
||||
</tr>
|
||||
{isExp && (
|
||||
<tr className="border-b border-line/40 bg-accent-bg/20">
|
||||
@@ -367,14 +420,14 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB
|
||||
key={t.id}
|
||||
type="button"
|
||||
onClick={() => setSelTx(sel ? null : t.id)}
|
||||
className={`flex min-w-[120px] flex-col gap-0.5 rounded-lg border p-2 text-start ${sel ? "border-accent bg-accent/10" : "border-line bg-panel"}`}
|
||||
className={`flex min-w-[120px] flex-col gap-0.5 rounded-lg border p-2 text-start transition-all ${sel ? "border-accent bg-accent/10 shadow-xs" : "border-line bg-panel hover:border-accent/40"}`}
|
||||
style={{ borderTop: `3px solid ${meta.color}` }}
|
||||
>
|
||||
<span className="text-[11px] font-bold" style={{ color: meta.color }}>{meta.icon} {meta.label}</span>
|
||||
<span className="text-[12px] font-bold" style={{ color: meta.color }}>
|
||||
<span className="text-[12px] font-bold tabular-nums" style={{ color: meta.color }}>
|
||||
{faNum(Math.abs(t.quantity_number))} {unit}
|
||||
</span>
|
||||
<span className="text-[10.5px] text-muted">مانده: <b>{faNum(bal)}</b> {unit}</span>
|
||||
<span className="text-[10.5px] text-muted">مانده: <b className="tabular-nums">{faNum(bal)}</b> {unit}</span>
|
||||
</button>
|
||||
);
|
||||
});
|
||||
@@ -385,7 +438,7 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB
|
||||
const brandSec = expBrand ? (
|
||||
<div className="mt-2">
|
||||
<div className="mb-1 text-[12px] font-semibold">جریان برند: {brandNameFn(expBrand)} ({faNum(brandTxs(g, expBrand).length)} تراکنش)</div>
|
||||
<div className="flex gap-1.5 overflow-x-auto">{track(brandTxs(g, expBrand))}</div>
|
||||
<div className="flex gap-1.5 overflow-x-auto pb-1 [scrollbar-width:thin]">{track(brandTxs(g, expBrand))}</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
@@ -395,21 +448,21 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB
|
||||
const meta = TX_META[t.transaction_type] || { label: t.transaction_type, color: "#64748b", icon: "•" };
|
||||
const pairTx = t.transfer_pair_id ? allTxs.find((x) => x.transfer_pair_id === t.transfer_pair_id && x.id !== t.id) : null;
|
||||
return (
|
||||
<div className="mt-2 rounded-lg border p-2 text-[12px]" style={{ borderTop: `3px solid ${meta.color}` }}>
|
||||
<div className="mt-2 rounded-lg border p-2.5 text-[12px] bg-panel shadow-2xs" style={{ borderTop: `3px solid ${meta.color}` }}>
|
||||
<b style={{ color: meta.color }}>{meta.label}</b>
|
||||
<div className="mt-1 flex flex-wrap gap-x-4 gap-y-1 text-muted">
|
||||
<span>تاریخ و ساعت: <b>{isoToJalaliStr(t.transaction_date)}</b></span>
|
||||
<span>تاریخ و ساعت: <b className="text-ink">{isoToJalaliStr(t.transaction_date)}</b></span>
|
||||
{t.transaction_type === "TRANSFER_OUT" || t.transaction_type === "TRANSFER_IN" ? (
|
||||
<>
|
||||
<span>از انبار: <b>{t.transaction_type === "TRANSFER_OUT" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)}</b></span>
|
||||
<span>به انبار: <b>{t.transaction_type === "TRANSFER_IN" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)}</b></span>
|
||||
<span>از انبار: <b className="text-ink">{t.transaction_type === "TRANSFER_OUT" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)}</b></span>
|
||||
<span>به انبار: <b className="text-ink">{t.transaction_type === "TRANSFER_IN" ? whNameFn(t.warehouse_id) : whNameFn(pairTx?.warehouse_id)}</b></span>
|
||||
</>
|
||||
) : (
|
||||
<span>انبار: <b>{whNameFn(t.warehouse_id)}</b></span>
|
||||
<span>انبار: <b className="text-ink">{whNameFn(t.warehouse_id)}</b></span>
|
||||
)}
|
||||
{t.brand?.name && <span>برند: <b>{t.brand.name}</b></span>}
|
||||
{t.performed_by && <span>انجامدهنده: <b>{t.performed_by}</b></span>}
|
||||
{t.note && <span>یادداشت: <b>{t.note}</b></span>}
|
||||
{t.brand?.name && <span>برند: <b className="text-ink">{t.brand.name}</b></span>}
|
||||
{t.performed_by && <span>انجامدهنده: <b className="text-ink">{t.performed_by}</b></span>}
|
||||
{t.note && <span>یادداشت: <b className="text-ink">{t.note}</b></span>}
|
||||
{t.transaction_type === "STOCK_COUNT" && t.recorded_qty != null && t.physical_qty != null && (
|
||||
<span>
|
||||
ثبتشده: {faNum(t.recorded_qty)} • شمارششده: {faNum(t.physical_qty)} • اختلاف: {faNum(t.physical_qty - t.recorded_qty)}
|
||||
@@ -437,16 +490,16 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB
|
||||
key={bid}
|
||||
type="button"
|
||||
onClick={() => setExpBrand(expBrand === bid ? null : bid)}
|
||||
className={`rounded-lg border px-2 py-1 text-[11.5px] ${expBrand === bid ? "border-accent bg-accent/10 text-accent" : "border-line bg-panel"}`}
|
||||
className={`rounded-lg border px-2 py-1 text-[11.5px] transition-colors ${expBrand === bid ? "border-accent bg-accent/10 text-accent font-bold" : "border-line bg-panel hover:bg-bg"}`}
|
||||
>
|
||||
{brandNameFn(bid)}: <b>{faNum(g.brands[bid])}</b> {unit}{expBrand === bid ? " ✕" : ""}
|
||||
{brandNameFn(bid)}: <b className="tabular-nums">{faNum(g.brands[bid])}</b> {unit}{expBrand === bid ? " ✕" : ""}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{brandSec}
|
||||
<div className="mb-1 mt-2 text-[12px] font-semibold">جریان کلی رکورد — ترکیب همه برندها ({faNum(txs.length)} تراکنش)</div>
|
||||
<div className="flex gap-1.5 overflow-x-auto pb-1">{track(txs)}</div>
|
||||
<div className="flex gap-1.5 overflow-x-auto pb-1 [scrollbar-width:thin]">{track(txs)}</div>
|
||||
{selDetail}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user