diff --git a/frontend/src/components/ui/button.tsx b/frontend/src/components/ui/button.tsx index a34246f..1cc6c66 100644 --- a/frontend/src/components/ui/button.tsx +++ b/frontend/src/components/ui/button.tsx @@ -4,17 +4,17 @@ import { cva, type VariantProps } from "class-variance-authority"; import { cn } from "../../lib/utils"; const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent/40 disabled:pointer-events-none disabled:opacity-50 active:scale-[0.98] [&_svg]:pointer-events-none [&_svg]:shrink-0", { variants: { variant: { default: "bg-accent text-white shadow-sm hover:bg-accent-dark", primary: "bg-accent text-white shadow-sm hover:bg-accent-dark", - slate: "bg-slate-bg text-ink hover:bg-line", - line: "border border-line bg-panel text-ink hover:bg-slate-bg", + slate: "bg-slate-bg text-ink border border-transparent hover:brightness-95", + line: "border border-line bg-panel text-ink shadow-sm hover:bg-slate-bg", ghost: "hover:bg-slate-bg text-ink", - green: "bg-green text-white hover:brightness-95", - danger: "bg-red-bg text-red hover:bg-red hover:text-white", + green: "bg-green text-white shadow-sm hover:brightness-95", + danger: "bg-red-bg text-red shadow-sm hover:bg-red hover:text-white", }, size: { default: "h-9 px-4 py-2", @@ -44,4 +44,4 @@ const Button = React.forwardRef( ); Button.displayName = "Button"; -export { Button, buttonVariants }; +export { Button, buttonVariants }; \ No newline at end of file diff --git a/frontend/src/pages/Dashboard/DashboardPage.tsx b/frontend/src/pages/Dashboard/DashboardPage.tsx index eededbd..0e1dcd7 100644 --- a/frontend/src/pages/Dashboard/DashboardPage.tsx +++ b/frontend/src/pages/Dashboard/DashboardPage.tsx @@ -1,6 +1,7 @@ import { useMemo, useState } from "react"; import { Link } from "react-router-dom"; import { Users, FolderKanban, Ruler, Plus } from "lucide-react"; +import { Button } from "../../components/ui/button"; import { useMaterials, useProjects, @@ -80,22 +81,33 @@ export function DashboardPage() { menu={} actions={ <> - - - تأمین‌کننده‌ها - - - - پروژه‌ها - - - - ضرایب تبدیل - - - - افزودن پیشنهاد جدید - + + + + + + + } /> @@ -128,4 +140,4 @@ export function DashboardPage() { setEditId(null)} /> ); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Inventory/InventoryPage.tsx b/frontend/src/pages/Inventory/InventoryPage.tsx index 5b62036..a89413c 100644 --- a/frontend/src/pages/Inventory/InventoryPage.tsx +++ b/frontend/src/pages/Inventory/InventoryPage.tsx @@ -1,9 +1,10 @@ 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, ArrowUpDown, ArrowUp, ArrowDown, X } from "lucide-react"; import { Header, PageLayout } from "../../components/layout/Header"; import { EmptyState } from "../../components/EmptyState"; import { Badge } from "../../components/ui/badge"; +import { Button } from "../../components/ui/button"; import { useAuthStore } from "../../store/auth"; import { AppSwitcherMenu } from "../../components/AppSwitcherMenu"; import { inventorySubheader } from "./panelMenu"; @@ -52,20 +53,21 @@ 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= → داشبورد همان انبار از پیش فیلتر شده const [params] = useSearchParams(); const [filters, setFilters] = useState<{ category: string; warehouse: string; material: string; project: string }>({ - category: "", warehouse: params.get("warehouse") || "", material: "", project: "", + category: "", + warehouse: params.get("warehouse") || "", + material: "", + project: "", }); const [sort, setSort] = useState<"asc" | "desc" | "">(""); const [expandedKey, setExpandedKey] = useState(null); const [selTx, setSelTx] = useState(null); const [expBrand, setExpBrand] = useState(null); - // ادمین: انبارهای همه پروژه‌ها یا پروژه‌ی انتخابی const visibleWarehouses = useMemo(() => { if (isAdmin && filters.project) return warehouses.filter((w) => w.project_id === filters.project); if (isAdmin) return warehouses; @@ -86,7 +88,18 @@ export function InventoryPage() { const gk = `${whId}|${r.material.id}|${specsHash(r.specs)}`; let g = gmap.get(gk); if (!g) { - g = { key: gk, whId, matId: r.material.id, specs: r.specs || {}, unitId: r.unit?.id ?? null, qty: 0, updatedAt: r.updated_at, recs: [], brands: {}, brandOrder: [] }; + g = { + key: gk, + whId, + matId: r.material.id, + specs: r.specs || {}, + unitId: r.unit?.id ?? null, + qty: 0, + updatedAt: r.updated_at, + recs: [], + brands: {}, + brandOrder: [], + }; gmap.set(gk, g); order.push(gk); } @@ -145,24 +158,23 @@ export function InventoryPage() { <> {isPM && ( - - - تغییر موجودی - + )} } /> - - {/* فیلترها */} -
-
- + + {/* نوار فیلترها و ابزارها */} +
+
+
+ {isAdmin && ( -
- +
+
)} -
- + +
+
-
- + +
+ setFilters((f) => ({ ...f, material: e.target.value }))} />
- + {sort === "asc" ? ( + <> + + تعداد: صعودی + + ) : sort === "desc" ? ( + <> + + تعداد: نزولی + + ) : ( + <> + + مرتب‌سازی تعداد + + )} + + {hasFilter && ( - + + پاک کردن فیلترها + )} - {filters.warehouse && ( - 🏬 {whName(warehouses, filters.warehouse)} - )} - {faNum(groups.length)} قلم کالا + +
+ {filters.warehouse && ( + + 🏬 {whName(warehouses, filters.warehouse)} + + )} + + {faNum(groups.length)} قلم کالا + +
- {/* جدول گروه‌بندی‌شده */} -
- {!groups.length ? ( + {/* بخش جدول یا وضعیت خالی */} + {!groups.length ? ( +
- ) : ( +
+ ) : ( +
- - - - - - - - - + + + + + + + + @@ -293,8 +337,9 @@ export function InventoryPage() { })}
- متریالدسته‌بندیانبارمشخصه فنیتعدادواحدآخرین به‌روزرسانی +
+ متریالدسته‌بندیانبارمشخصه فنیتعدادواحدآخرین به‌روزرسانی
- )} -
+
+ )} +
۱۰ نوع تراکنش استاندارد • سبز زمردی = ورود به انبار · رز = خروج/کاهش · آبی = انتقال و شمارش • کلیک روی هر سطر: خط زمانی با «مانده» لحظه‌به‌لحظه و تفکیک برندها
@@ -317,25 +362,25 @@ function GroupRow({ g, isExp, isAdmin, matName, matCatName, wh, projName, unit, }) { return ( <> - - {isExp ? "▾" : "▸"} - {matName} - {matCatName} - + + {isExp ? "▾" : "▸"} + {matName} + {matCatName} + {wh} {isAdmin && {projName}} - + {Object.entries(g.specs || {}).map(([k, v]) => `${k}: ${v}`).join(" · ") || "—"} - {faNum(g.qty)} - {unit} - {isoToJalaliStr(g.updatedAt)} - 🕘 + {faNum(g.qty)} + {unit} + {isoToJalaliStr(g.updatedAt)} + 🕘 {isExp && ( - - {timeline} + + {timeline} )} @@ -367,7 +412,9 @@ 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-[125px] flex-col gap-1 rounded-md border p-2.5 text-start transition-all duration-150 active:scale-[0.98] ${ + sel ? "border-accent bg-accent/10 shadow-xs" : "border-line bg-panel hover:border-slate-300" + }`} style={{ borderTop: `3px solid ${meta.color}` }} > {meta.icon} {meta.label} @@ -383,9 +430,9 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB if (!txs.length) return
تراکنشی ثبت نشده است.
; const brandSec = expBrand ? ( -
-
جریان برند: {brandNameFn(expBrand)} ({faNum(brandTxs(g, expBrand).length)} تراکنش)
-
{track(brandTxs(g, expBrand))}
+
+
جریان برند: {brandNameFn(expBrand)} ({faNum(brandTxs(g, expBrand).length)} تراکنش)
+
{track(brandTxs(g, expBrand))}
) : null; @@ -395,7 +442,7 @@ 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 ( -
+
{meta.label}
تاریخ و ساعت: {isoToJalaliStr(t.transaction_date)} @@ -422,14 +469,14 @@ function Timeline({ g, txs, allTxs, brandTxs, selTx, setSelTx, expBrand, setExpB return (
-
+
خط زمانی — {matName} {Object.entries(g.specs || {}).length > 0 && ( - + {Object.entries(g.specs).map(([k, v]) => `${k}: ${v}`).join(" · ")} )} - {faNum(g.brandOrder.length)} برند + {faNum(g.brandOrder.length)} برند
{g.brandOrder.map((bid) => ( @@ -437,16 +484,18 @@ 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-md border px-2.5 py-1 text-[11.5px] transition-all duration-150 active:scale-[0.98] ${ + expBrand === bid ? "border-accent bg-accent/10 font-semibold text-accent" : "border-line bg-panel hover:bg-slate-bg" + }`} > {brandNameFn(bid)}: {faNum(g.brands[bid])} {unit}{expBrand === bid ? " ✕" : ""} ))}
{brandSec} -
جریان کلی رکورد — ترکیب همه برندها ({faNum(txs.length)} تراکنش)
-
{track(txs)}
+
جریان کلی رکورد — ترکیب همه برندها ({faNum(txs.length)} تراکنش)
+
{track(txs)}
{selDetail}
); -} +} \ No newline at end of file diff --git a/frontend/src/pages/Inventory/InventoryTopNav.tsx b/frontend/src/pages/Inventory/InventoryTopNav.tsx index 297d459..c89ee70 100644 --- a/frontend/src/pages/Inventory/InventoryTopNav.tsx +++ b/frontend/src/pages/Inventory/InventoryTopNav.tsx @@ -1,13 +1,7 @@ import { Link } from "react-router-dom"; -import { cn } from "../../lib/utils"; +import { Button } from "../../components/ui/button"; -// ناوبری بالای صفحه در پنل انبار — فقط روی «داشبورد انبار» (هاب) دیده می‌شود؛ -// خودِ «داشبورد انبار» دکمه ندارد (ریشه/هاب است) و زیرصفحه‌ها تب ندارند و -// فقط با «بازگشت به داشبورد» به هاب برمی‌گردند. -// -// - variant="pm" (مدیر پروژه): کنترل روزانه، انبارها، کالاهای در راه — -// ورود به «تغییر موجودی» از دکمه‌ی بالای هاب است. -// - variant="admin" (مدیر تدارکات/ادمین): کنترل روزانه، کالاهای در راه، سازمان‌دهی. +// ناوبری بالای صفحه در پنل انبار — هماهنگ با دیزاین‌سیستم دکمه‌ها export function InventoryTopNav({ pathname, variant = "pm", @@ -27,20 +21,20 @@ export function InventoryTopNav({ { label: "انبارها", to: "/inventory/warehouses", active: pathname.startsWith("/inventory/warehouses") }, { label: "کالاهای در راه", to: "/inventory/arrivals", active: pathname.startsWith("/inventory/arrivals") }, ]; + return ( <> {items.map((it) => ( - - {it.label} - + + {it.label} + + ))} ); -} +} \ No newline at end of file