From 2a4cf1d20bca3f67093d8f511ca6ffe28728d4b4 Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 11 Jun 2026 23:10:38 +0330 Subject: [PATCH] feat(dashboard): Jalali date pickers + mobile/tablet responsive shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Full Persian calendar: - New JalaliDateField — Shamsi popover picker (Saturday-first weeks, Persian digits, امروز shortcut); wire format stays ISO Gregorian YYYY-MM-DD. Falls back to the native input for the en locale. - Replaces all 5 native type="date" inputs (Gregorian-only pickers): reservations, expenses from/to, reports from/to. - Reservations list date now renders Jalali instead of the raw ISO string; branches purge timestamp now formats with fa-IR. Responsive shell (mobile + tablet): - New MobileNav: hamburger in the topbar (< md) opening an RTL-aware slide-over drawer with all nav destinations, permission-filtered, Escape/backdrop close and body scroll lock. - Desktop sidebar hidden below md; header center cluster (clock/plan) hidden below md; language switcher hidden below sm. - Main content padding scales p-3 → p-4 → p-6. - Verified at 375px and 768px: no horizontal overflow, drawer and Jalali picker fully functional. Co-Authored-By: Claude Fable 5 --- .../src/app/[locale]/(dashboard)/layout.tsx | 2 +- .../components/branches/branches-screen.tsx | 2 +- .../components/expenses/expenses-screen.tsx | 22 +- .../layout/header-center-cluster.tsx | 2 +- .../src/components/layout/mobile-nav.tsx | 187 +++++++++++++++ .../src/components/layout/sidebar.tsx | 3 +- .../src/components/layout/topbar.tsx | 14 +- .../src/components/reports/reports-screen.tsx | 24 +- .../reservations/reservations-screen.tsx | 15 +- .../src/components/ui/jalali-date-field.tsx | 221 ++++++++++++++++++ 10 files changed, 439 insertions(+), 53 deletions(-) create mode 100644 web/dashboard/src/components/layout/mobile-nav.tsx create mode 100644 web/dashboard/src/components/ui/jalali-date-field.tsx diff --git a/web/dashboard/src/app/[locale]/(dashboard)/layout.tsx b/web/dashboard/src/app/[locale]/(dashboard)/layout.tsx index c327a3c..c232527 100644 --- a/web/dashboard/src/app/[locale]/(dashboard)/layout.tsx +++ b/web/dashboard/src/app/[locale]/(dashboard)/layout.tsx @@ -37,7 +37,7 @@ export default function DashboardLayout({ const mainColumn = (
-
+
{children}
diff --git a/web/dashboard/src/components/branches/branches-screen.tsx b/web/dashboard/src/components/branches/branches-screen.tsx index 0e4e5ce..ea9f764 100644 --- a/web/dashboard/src/components/branches/branches-screen.tsx +++ b/web/dashboard/src/components/branches/branches-screen.tsx @@ -191,7 +191,7 @@ export function BranchesScreen() {

{b.scheduledPermanentDeleteAt ? (

- {new Date(b.scheduledPermanentDeleteAt).toLocaleString()} + {new Date(b.scheduledPermanentDeleteAt).toLocaleString("fa-IR")}

) : null} diff --git a/web/dashboard/src/components/expenses/expenses-screen.tsx b/web/dashboard/src/components/expenses/expenses-screen.tsx index 871a0ee..8d45e7e 100644 --- a/web/dashboard/src/components/expenses/expenses-screen.tsx +++ b/web/dashboard/src/components/expenses/expenses-screen.tsx @@ -11,6 +11,7 @@ import { isoTodayTehran } from "@/lib/reports/analytics"; import { PageHeader } from "@/components/layout/page-header"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { JalaliDateField } from "@/components/ui/jalali-date-field"; import { LabeledField } from "@/components/ui/labeled-field"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; type Branch = { id: string; name: string }; @@ -172,27 +173,10 @@ export function ExpensesScreen() { - setFrom(e.target.value)} - /> + - setTo(e.target.value)} - /> +

diff --git a/web/dashboard/src/components/layout/header-center-cluster.tsx b/web/dashboard/src/components/layout/header-center-cluster.tsx index ca5b158..cf76757 100644 --- a/web/dashboard/src/components/layout/header-center-cluster.tsx +++ b/web/dashboard/src/components/layout/header-center-cluster.tsx @@ -50,7 +50,7 @@ export function HeaderCenterCluster() { return (

void; +}) { + const Icon = item.icon; + return ( + + + {label} + + ); +} + +export function MobileNav() { + const t = useTranslations("nav"); + const tGroups = useTranslations("nav.groups"); + const tBrand = useTranslations("brand"); + const locale = useLocale(); + const pathname = usePathname(); + const user = useAuthStore((s) => s.user); + const role = user?.role; + const branchId = user?.branchId ?? null; + const permissions = useMemo(() => permissionsOf(user), [user]); + const [open, setOpen] = useState(false); + const [mounted, setMounted] = useState(false); + + useEffect(() => setMounted(true), []); + + // Close on Escape; lock body scroll while open. + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") setOpen(false); + }; + document.addEventListener("keydown", onKey); + const prevOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.removeEventListener("keydown", onKey); + document.body.style.overflow = prevOverflow; + }; + }, [open]); + + const close = () => setOpen(false); + const isRtl = locale !== "en"; + + const visible = (items: NavItemDef[]) => + items.filter((item) => canSeeNavItem(item.key, role, branchId, permissions)); + + const isActive = (item: NavItemDef) => + item.href === "/" + ? pathname === "/" + : pathname === item.href || pathname.startsWith(`${item.href}/`); + + return ( + <> + + + {mounted && + open && + createPortal( +
+ {/* Backdrop */} + +
+ + + + {user && ( +
+
+ + {(user.actor ?? user.role).charAt(0).toUpperCase()} + +
+
+

{user.actor ?? user.userId}

+

{user.role}

+
+
+ )} +
+
, + document.body + )} + + ); +} diff --git a/web/dashboard/src/components/layout/sidebar.tsx b/web/dashboard/src/components/layout/sidebar.tsx index ea08798..e2652e3 100644 --- a/web/dashboard/src/components/layout/sidebar.tsx +++ b/web/dashboard/src/components/layout/sidebar.tsx @@ -283,7 +283,8 @@ export function Sidebar({ side }: { side: "left" | "right" }) { return (