Browse Source

Replace dashboard revenue with total parts, fix active jobs count, and improve mobile nav drawer (#114)

* remove reveue from dash

* active count

* phone menu

* area label

* area label
Bernt Christian Egeland 6 months ago
parent
commit
64e03fcc8a

+ 1 - 1
messages/de/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Aktive Aufträge",
     "pending": "Ausstehend",
-    "todaysRevenue": "Heutiger Umsatz",
+    "totalParts": "Teile gesamt",
     "totalCustomers": "Kunden gesamt"
   },
   "customize": "Anpassen",

+ 1 - 1
messages/en/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Active Jobs",
     "pending": "Pending",
-    "todaysRevenue": "Today's Revenue",
+    "totalParts": "Total Parts",
     "totalCustomers": "Total Customers"
   },
   "customize": "Customize",

+ 1 - 1
messages/es/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Trabajos activos",
     "pending": "Pendientes",
-    "todaysRevenue": "Ingresos de hoy",
+    "totalParts": "Total de piezas",
     "totalCustomers": "Total de clientes"
   },
   "customize": "Personalizar",

+ 1 - 1
messages/fr/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Travaux en cours",
     "pending": "En attente",
-    "todaysRevenue": "Revenus du jour",
+    "totalParts": "Total des pièces",
     "totalCustomers": "Total des clients"
   },
   "customize": "Personnaliser",

+ 1 - 1
messages/it/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Lavori attivi",
     "pending": "In attesa",
-    "todaysRevenue": "Fatturato odierno",
+    "totalParts": "Ricambi totali",
     "totalCustomers": "Clienti totali"
   },
   "customize": "Personalizza",

+ 1 - 1
messages/nb/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Aktive jobber",
     "pending": "Ventende",
-    "todaysRevenue": "Dagens omsetning",
+    "totalParts": "Totale deler",
     "totalCustomers": "Totalt antall kunder"
   },
   "customize": "Tilpass",

+ 1 - 1
messages/nl/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Actieve opdrachten",
     "pending": "In afwachting",
-    "todaysRevenue": "Omzet vandaag",
+    "totalParts": "Totaal onderdelen",
     "totalCustomers": "Totaal klanten"
   },
   "customize": "Aanpassen",

+ 1 - 1
messages/pl/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Aktywne zlecenia",
     "pending": "Oczekujące",
-    "todaysRevenue": "Dzisiejszy przychód",
+    "totalParts": "Łączna liczba części",
     "totalCustomers": "Łączna liczba klientów"
   },
   "customize": "Dostosuj",

+ 1 - 1
messages/pt-BR/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Trabalhos Ativos",
     "pending": "Pendentes",
-    "todaysRevenue": "Receita de Hoje",
+    "totalParts": "Total de Peças",
     "totalCustomers": "Total de Clientes"
   },
   "customize": "Personalizar",

+ 1 - 1
messages/ru/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Активные работы",
     "pending": "Ожидающие",
-    "todaysRevenue": "Выручка за сегодня",
+    "totalParts": "Всего запчастей",
     "totalCustomers": "Всего клиентов"
   },
   "customize": "Настроить",

+ 1 - 1
messages/tr/dashboard.json

@@ -26,7 +26,7 @@
   "stats": {
     "activeJobs": "Aktif İşler",
     "pending": "Beklemede",
-    "todaysRevenue": "Bugünkü Gelir",
+    "totalParts": "Toplam Parça",
     "totalCustomers": "Toplam Müşteriler"
   },
   "customize": "Özelleştir",

+ 5 - 6
src/app/(authenticated)/dashboard-client.tsx

@@ -27,7 +27,6 @@ import {
   ClipboardCheck,
   ClipboardList,
   Clock,
-  DollarSign,
   EyeOff,
   FileText,
   Gauge,
@@ -71,7 +70,7 @@ interface DashboardStats {
   isAdmin: boolean;
   activeJobs: number;
   pendingJobs: number;
-  todayRevenue: number;
+  totalParts: number;
   totalCustomers: number;
   todaysServices: ServiceItem[];
   recentServices: ServiceItem[];
@@ -289,12 +288,12 @@ export function DashboardClient({
           <p className="text-lg font-bold">{stats.pendingJobs}</p>
         </Link>
         {stats.isAdmin && (
-          <Link href="/reports" className="rounded-lg border border-0 shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
+          <Link href="/inventory" className="rounded-lg border border-0 shadow-sm bg-card px-3 py-2 transition-colors hover:bg-muted/50">
             <div className="flex items-center justify-between">
-              <span className="text-[11px] text-muted-foreground">{t("stats.todaysRevenue")}</span>
-              <DollarSign className="h-3.5 w-3.5 text-muted-foreground/50" />
+              <span className="text-[11px] text-muted-foreground">{t("stats.totalParts")}</span>
+              <Settings className="h-3.5 w-3.5 text-muted-foreground/50" />
             </div>
-            <p className="text-lg font-bold">{formatCurrency(stats.todayRevenue, currencyCode)}</p>
+            <p className="text-lg font-bold">{stats.totalParts}</p>
           </Link>
         )}
         {stats.isAdmin && (

+ 1 - 1
src/app/(authenticated)/layout.tsx

@@ -130,7 +130,6 @@ export default async function DashboardLayout({
         <SidebarInset>
           <div className="pb-14 md:pb-0">{children}</div>
         </SidebarInset>
-        <MobileBottomNav />
         <SearchCommand />
         {isOwnerOrAdmin && <NotificationInitializer />}
         <OnlineTracker />
@@ -138,6 +137,7 @@ export default async function DashboardLayout({
       </ConfirmProvider>
       </DateSettingsProvider>
     </SidebarProvider>
+    <MobileBottomNav isSuperAdmin={data.isSuperAdmin} />
     </WhiteLabelCtaProvider>
     </LicenseExpiryProvider>
     </ServiceTypeProvider>

+ 98 - 30
src/components/mobile-bottom-nav.tsx

@@ -5,6 +5,7 @@ import Link from 'next/link'
 import { usePathname, useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
 import {
+  BarChart3,
   Bell,
   CalendarDays,
   Car,
@@ -22,6 +23,7 @@ import {
   Receipt,
   ScanBarcode,
   Settings,
+  ShieldCheck,
   Users,
 } from 'lucide-react'
 import {
@@ -48,19 +50,24 @@ const primaryItems = [
   { href: '/inventory', icon: Package, labelKey: 'inventory' },
 ] as const
 
-const drawerItems = [
+const workshopItems = [
   { href: '/calendar', icon: CalendarDays, labelKey: 'calendar' },
   { href: '/work-board', icon: Columns3, labelKey: 'workBoard' },
-  { href: '/quotes', icon: FileText, labelKey: 'quotes' },
   { href: '/inspections', icon: ClipboardCheck, labelKey: 'inspections' },
   { href: '/reminders', icon: Bell, labelKey: 'reminders' },
+] as const
+
+const businessItems = [
+  { href: '/quotes', icon: FileText, labelKey: 'quotes' },
   { href: '/billing', icon: Receipt, labelKey: 'billing' },
   { href: '/labor-presets', icon: Layers, labelKey: 'laborPresets' },
+  { href: '/reports', icon: BarChart3, labelKey: 'reports' },
   { href: '/audit-log', icon: History, labelKey: 'auditLog' },
-  { href: '/settings', icon: Settings, labelKey: 'settings' },
 ] as const
 
-export function MobileBottomNav() {
+const allDrawerItems = [...workshopItems, ...businessItems, { href: '/settings', icon: Settings, labelKey: 'settings' }, { href: '/admin', icon: ShieldCheck, labelKey: 'adminPanel' }] as const
+
+export function MobileBottomNav({ isSuperAdmin }: { isSuperAdmin?: boolean }) {
   const pathname = usePathname()
   const router = useRouter()
   const t = useTranslations('navigation.sidebar')
@@ -82,7 +89,7 @@ export function MobileBottomNav() {
   const [addQty, setAddQty] = useState(1)
   const [addingStock, setAddingStock] = useState(false)
 
-  const isMoreActive = drawerItems.some(
+  const isMoreActive = allDrawerItems.some(
     (item) => pathname === item.href || pathname.startsWith(`${item.href}/`)
   )
 
@@ -120,7 +127,7 @@ export function MobileBottomNav() {
           })}
           <button
             type="button"
-            onClick={() => setDrawerOpen(true)}
+            onClick={(e) => { e.currentTarget.blur(); setDrawerOpen(true) }}
             className={cn(
               'flex flex-col items-center gap-0.5 py-2 text-[10px]',
               isMoreActive ? 'text-primary' : 'text-muted-foreground'
@@ -133,11 +140,11 @@ export function MobileBottomNav() {
       </nav>
 
       <Drawer open={drawerOpen} onOpenChange={setDrawerOpen}>
-        <DrawerContent>
+        <DrawerContent aria-describedby={undefined}>
           <DrawerHeader className="sr-only">
             <DrawerTitle>{t('more')}</DrawerTitle>
           </DrawerHeader>
-          <div className="p-4">
+          <div className="px-4 pb-4 space-y-4">
             {/* Quick action */}
             <button
               type="button"
@@ -151,29 +158,90 @@ export function MobileBottomNav() {
               {tScan('scanParts')}
             </button>
 
-            <Separator className="my-4" />
+            {/* Workshop */}
+            <div>
+              <p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">{t('workshop')}</p>
+              <nav className="grid grid-cols-4 gap-2">
+                {workshopItems.map(({ href, icon: Icon, labelKey }) => {
+                  const isActive = pathname === href || pathname.startsWith(`${href}/`)
+                  return (
+                    <Link
+                      key={href}
+                      href={href}
+                      onClick={() => setDrawerOpen(false)}
+                      className={cn(
+                        'flex flex-col items-center gap-1.5 rounded-lg py-3 text-xs transition-colors',
+                        isActive
+                          ? 'bg-primary/10 text-primary'
+                          : 'text-muted-foreground hover:bg-accent'
+                      )}
+                    >
+                      <Icon className="h-5 w-5" />
+                      <span>{t(labelKey)}</span>
+                    </Link>
+                  )
+                })}
+              </nav>
+            </div>
+
+            {/* Business */}
+            <div>
+              <p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wider mb-2 px-1">{t('business')}</p>
+              <nav className="grid grid-cols-4 gap-2">
+                {businessItems.map(({ href, icon: Icon, labelKey }) => {
+                  const isActive = pathname === href || pathname.startsWith(`${href}/`)
+                  return (
+                    <Link
+                      key={href}
+                      href={href}
+                      onClick={() => setDrawerOpen(false)}
+                      className={cn(
+                        'flex flex-col items-center gap-1.5 rounded-lg py-3 text-xs transition-colors',
+                        isActive
+                          ? 'bg-primary/10 text-primary'
+                          : 'text-muted-foreground hover:bg-accent'
+                      )}
+                    >
+                      <Icon className="h-5 w-5" />
+                      <span>{t(labelKey)}</span>
+                    </Link>
+                  )
+                })}
+              </nav>
+            </div>
 
-            {/* Navigation grid */}
+            <Separator />
+
+            {/* Settings & Admin */}
             <nav className="grid grid-cols-4 gap-2">
-              {drawerItems.map(({ href, icon: Icon, labelKey }) => {
-                const isActive = pathname === href || pathname.startsWith(`${href}/`)
-                return (
-                  <Link
-                    key={href}
-                    href={href}
-                    onClick={() => setDrawerOpen(false)}
-                    className={cn(
-                      'flex flex-col items-center gap-1.5 rounded-lg py-3 text-xs transition-colors',
-                      isActive
-                        ? 'bg-primary/10 text-primary'
-                        : 'text-muted-foreground hover:bg-accent'
-                    )}
-                  >
-                    <Icon className="h-5 w-5" />
-                    <span>{t(labelKey)}</span>
-                  </Link>
-                )
-              })}
+              <Link
+                href="/settings"
+                onClick={() => setDrawerOpen(false)}
+                className={cn(
+                  'flex flex-col items-center gap-1.5 rounded-lg py-3 text-xs transition-colors',
+                  pathname.startsWith('/settings')
+                    ? 'bg-primary/10 text-primary'
+                    : 'text-muted-foreground hover:bg-accent'
+                )}
+              >
+                <Settings className="h-5 w-5" />
+                <span>{t('settings')}</span>
+              </Link>
+              {isSuperAdmin && (
+                <Link
+                  href="/admin"
+                  onClick={() => setDrawerOpen(false)}
+                  className={cn(
+                    'flex flex-col items-center gap-1.5 rounded-lg py-3 text-xs transition-colors',
+                    pathname.startsWith('/admin')
+                      ? 'bg-primary/10 text-primary'
+                      : 'text-muted-foreground hover:bg-accent'
+                  )}
+                >
+                  <ShieldCheck className="h-5 w-5" />
+                  <span>{t('adminPanel')}</span>
+                </Link>
+              )}
             </nav>
           </div>
         </DrawerContent>
@@ -189,7 +257,7 @@ export function MobileBottomNav() {
         setShowScanActions(open)
         if (!open) router.refresh()
       }}>
-        <DrawerContent>
+        <DrawerContent aria-describedby={undefined}>
           <DrawerHeader>
             <DrawerTitle>{tScan('title')}</DrawerTitle>
           </DrawerHeader>

+ 5 - 14
src/features/vehicles/Actions/dashboardActions.ts

@@ -7,32 +7,23 @@ import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 export async function getDashboardStats() {
   return withAuth(async ({ organizationId, role }) => {
     const isAdmin = role === "owner" || role === "admin" || role === "super_admin";
-    const today = new Date();
-    today.setHours(0, 0, 0, 0);
-    const tomorrow = new Date(today);
-    tomorrow.setDate(tomorrow.getDate() + 1);
 
     const [
       activeJobs,
       pendingJobs,
-      todayCompletedRecords,
+      totalParts,
       totalCustomers,
       todaysServices,
       recentServices,
     ] = await Promise.all([
       db.serviceRecord.count({
-        where: { vehicle: { organizationId }, status: "in-progress" },
+        where: { vehicle: { organizationId }, status: { not: "completed" } },
       }),
       db.serviceRecord.count({
         where: { vehicle: { organizationId }, status: "pending" },
       }),
-      db.serviceRecord.aggregate({
-        where: {
-          vehicle: { organizationId },
-          status: "completed",
-          startDateTime: { gte: today, lt: tomorrow },
-        },
-        _sum: { totalAmount: true },
+      db.inventoryPart.count({
+        where: { organizationId, isArchived: false },
       }),
       db.customer.count({ where: { organizationId } }),
       db.serviceRecord.findMany({
@@ -81,7 +72,7 @@ export async function getDashboardStats() {
       isAdmin,
       activeJobs,
       pendingJobs,
-      todayRevenue: todayCompletedRecords._sum.totalAmount || 0,
+      totalParts,
       totalCustomers,
       todaysServices,
       recentServices,