All changes are gated behind max-md:/md:hidden so the >=768px desktop layout is untouched (verified with lint + build + live checks at 375px). - globals.css: one mobile block — 16px inputs (no iOS auto-zoom), units sidebar stacks above content, wizard item rows become 2-col (text fields full-width, delete button full-width), dd-panels / flow diagram / conv-form stack, larger touch targets - ui/dialog.tsx: dialogs become full-width bottom sheets on mobile (all modals inherit it); desktop keeps the centered dialog - Header: sticky on mobile only + PageLayout bottom clearance - New MobileTabBar: role-aware bottom tabs (office admin/manager, inventory PM, inventory admin); hidden on /wizard - Wizard step 4: TouchSensor (150ms delay) so touch drag works without stealing scroll - JalaliDatePicker: dropdown capped to viewport; bigger day cells and time inputs on mobile - PWA: manifest.webmanifest + 192/512 icons (scripts/gen-pwa-icons.ps1) + theme-color/manifest links in index.html - New docs/mobile-responsive.md documenting the full change set
101 lines
3.7 KiB
TypeScript
101 lines
3.7 KiB
TypeScript
import { NavLink, useLocation } from "react-router-dom";
|
|
import {
|
|
ArrowLeftRight,
|
|
Boxes,
|
|
Building2,
|
|
CalendarDays,
|
|
LayoutDashboard,
|
|
PackageSearch,
|
|
Scale,
|
|
Truck,
|
|
UsersRound,
|
|
type LucideIcon,
|
|
} from "lucide-react";
|
|
import { cn } from "../../lib/utils";
|
|
import { useAuthStore } from "../../store/auth";
|
|
|
|
interface TabDef {
|
|
to: string;
|
|
label: string;
|
|
icon: LucideIcon;
|
|
end?: boolean;
|
|
}
|
|
|
|
// تبهای دفتر تدارکات (ادمین / مدیر تدارکات)
|
|
const PROC_TABS: TabDef[] = [
|
|
{ to: "/", label: "داشبورد", icon: LayoutDashboard, end: true },
|
|
{ to: "/suppliers", label: "تأمینکنندهها", icon: Truck },
|
|
{ to: "/projects", label: "پروژهها", icon: Building2 },
|
|
{ to: "/units", label: "ضرایب", icon: Scale },
|
|
];
|
|
|
|
// تبهای پنل انبار برای مدیر پروژه
|
|
const PM_INV_TABS: TabDef[] = [
|
|
{ to: "/inventory", label: "انبار", icon: Boxes, end: true },
|
|
{ to: "/inventory/change", label: "تغییر موجودی", icon: ArrowLeftRight },
|
|
{ to: "/inventory/daily", label: "کنترل روزانه", icon: CalendarDays },
|
|
{ to: "/inventory/arrivals", label: "کالاهای در راه", icon: PackageSearch },
|
|
];
|
|
|
|
// تبهای پنل انبار برای ادمین / مدیر تدارکات (آینهی ناوبری بالای دسکتاپ)
|
|
const ADMIN_INV_TABS: TabDef[] = [
|
|
{ to: "/inventory", label: "انبار", icon: Boxes, end: true },
|
|
{ to: "/inventory/daily", label: "کنترل روزانه", icon: CalendarDays },
|
|
{ to: "/inventory/arrivals", label: "کالاهای در راه", icon: PackageSearch },
|
|
{ to: "/inventory/org", label: "سازماندهی", icon: UsersRound },
|
|
];
|
|
|
|
function TabBar({ tabs }: { tabs: TabDef[] }) {
|
|
return (
|
|
<nav
|
|
className="fixed inset-x-0 bottom-0 z-40 border-t border-line bg-panel md:hidden"
|
|
style={{ paddingBottom: "env(safe-area-inset-bottom)" }}
|
|
>
|
|
<div className="grid grid-cols-4">
|
|
{tabs.map((t) => (
|
|
<NavLink
|
|
key={t.to}
|
|
to={t.to}
|
|
end={t.end}
|
|
className={({ isActive }) =>
|
|
cn(
|
|
"flex min-h-[56px] flex-col items-center justify-center gap-0.5 whitespace-nowrap px-1 text-[10.5px] font-medium transition-colors",
|
|
isActive ? "text-accent" : "text-muted",
|
|
)
|
|
}
|
|
>
|
|
<t.icon className="h-5 w-5" strokeWidth={1.8} />
|
|
<span>{t.label}</span>
|
|
</NavLink>
|
|
))}
|
|
</div>
|
|
</nav>
|
|
);
|
|
}
|
|
|
|
// نوار تب پایینِ موبایل — فقط زیر 768px (md:hidden) و فقط در صفحاتِ نقش کاربر:
|
|
// مدیر پروژه → تبهای انبار؛ ادمین/مدیر تدارکات → تبهای دفتر یا انبار (بر اساس مسیر).
|
|
// در ویزارد نمایش داده نمیشود تا با جابهجایی تصادفی، کارِ کاربر از بین نرود.
|
|
export function MobileTabBar() {
|
|
const role = useAuthStore((s) => s.user?.role);
|
|
const { pathname } = useLocation();
|
|
|
|
if (pathname === "/login" || pathname === "/admin") return null;
|
|
const isPM = role === "pm";
|
|
const isOffice = role === "admin" || role === "procurement_manager";
|
|
|
|
if (pathname.startsWith("/inventory")) {
|
|
if (isPM) return <TabBar tabs={PM_INV_TABS} />;
|
|
if (isOffice) return <TabBar tabs={ADMIN_INV_TABS} />;
|
|
return null;
|
|
}
|
|
if (pathname === "/wizard") return null;
|
|
const onOfficePage =
|
|
pathname === "/" ||
|
|
pathname === "/projects" ||
|
|
pathname === "/units" ||
|
|
pathname.startsWith("/suppliers");
|
|
if (onOfficePage && isOffice) return <TabBar tabs={PROC_TABS} />;
|
|
return null;
|
|
}
|