Browse Source

Dashboard, workboard, and UX improvements (#96)

* service units

* units

* button

* set selected day for work order

* dashboard card mobile

* tech dropdown

* new tech link to user

* translations

* translations

* migration

* migration

* drag drop

* work

* docs links

* docs links

* app icon at public pages

* terms

* dashboard cards

* dashboar cards

* test
Bernt Christian Egeland 6 months ago
parent
commit
4a7b611481

+ 2 - 2
src/__tests__/features/inspections/create-inspection.test.ts

@@ -102,11 +102,11 @@ describe("createInspection — technician lookup", () => {
 
     await createInspection({ vehicleId: VEHICLE_ID, templateId: TEMPLATE_ID });
 
-    // Technician must be looked up by memberId, not used directly as technicianId
+    // Technician must be looked up by userId, not used directly as technicianId
     expect(vi.mocked(db.technician.findFirst)).toHaveBeenCalledWith(
       expect.objectContaining({
         where: expect.objectContaining({
-          memberId: USER_ID,
+          userId: USER_ID,
           organizationId: ORG,
           isActive: true,
         }),

+ 30 - 39
src/app/(authenticated)/dashboard-client.tsx

@@ -2,6 +2,7 @@
 
 import { useState, useTransition } from "react";
 import { useRouter } from "next/navigation";
+import Link from "next/link";
 import { useTranslations } from "next-intl";
 import { useFormatDate } from "@/lib/use-format-date";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -271,48 +272,38 @@ export function DashboardClient({
   return (
     <div className="space-y-4">
       {/* Quick stats row */}
-      <div className={`grid grid-cols-2 gap-3 ${stats.isAdmin ? "sm:grid-cols-4" : "sm:grid-cols-2"}`}>
-        <Card className="border-0 shadow-sm">
-          <CardContent className="p-4">
-            <div className="flex items-center gap-2 text-muted-foreground">
-              <ClipboardList className="h-4 w-4" />
-              <span className="text-xs font-medium">{t("stats.activeJobs")}</span>
-            </div>
-            <p className="mt-1 text-2xl font-bold">{stats.activeJobs}</p>
-          </CardContent>
-        </Card>
-        <Card className="border-0 shadow-sm">
-          <CardContent className="p-4">
-            <div className="flex items-center gap-2 text-muted-foreground">
-              <Clock className="h-4 w-4" />
-              <span className="text-xs font-medium">{t("stats.pending")}</span>
-            </div>
-            <p className="mt-1 text-2xl font-bold">{stats.pendingJobs}</p>
-          </CardContent>
-        </Card>
+      <div className={`grid grid-cols-2 gap-2 ${stats.isAdmin ? "sm:grid-cols-4" : "sm:grid-cols-2"}`}>
+        <Link href="/work-orders?status=active" 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.activeJobs")}</span>
+            <ClipboardList className="h-3.5 w-3.5 text-muted-foreground/50" />
+          </div>
+          <p className="text-lg font-bold">{stats.activeJobs}</p>
+        </Link>
+        <Link href="/work-orders?status=pending" 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.pending")}</span>
+            <Clock className="h-3.5 w-3.5 text-muted-foreground/50" />
+          </div>
+          <p className="text-lg font-bold">{stats.pendingJobs}</p>
+        </Link>
         {stats.isAdmin && (
-          <Card className="border-0 shadow-sm">
-            <CardContent className="p-4">
-              <div className="flex items-center gap-2 text-muted-foreground">
-                <DollarSign className="h-4 w-4" />
-                <span className="text-xs font-medium">{t("stats.todaysRevenue")}</span>
-              </div>
-              <p className="mt-1 text-2xl font-bold">
-                {formatCurrency(stats.todayRevenue, currencyCode)}
-              </p>
-            </CardContent>
-          </Card>
+          <Link href="/reports" 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" />
+            </div>
+            <p className="text-lg font-bold">{formatCurrency(stats.todayRevenue, currencyCode)}</p>
+          </Link>
         )}
         {stats.isAdmin && (
-          <Card className="border-0 shadow-sm">
-            <CardContent className="p-4">
-              <div className="flex items-center gap-2 text-muted-foreground">
-                <Users className="h-4 w-4" />
-                <span className="text-xs font-medium">{t("stats.totalCustomers")}</span>
-              </div>
-              <p className="mt-1 text-2xl font-bold">{stats.totalCustomers}</p>
-            </CardContent>
-          </Card>
+          <Link href="/customers" 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.totalCustomers")}</span>
+              <Users className="h-3.5 w-3.5 text-muted-foreground/50" />
+            </div>
+            <p className="text-lg font-bold">{stats.totalCustomers}</p>
+          </Link>
         )}
       </div>
 

+ 11 - 6
src/app/(public)/auth/forgot-password/page.tsx

@@ -2,12 +2,13 @@
 
 import { useState } from 'react'
 import Link from 'next/link'
+import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { authClient } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
-import { Gauge, Loader2, ArrowLeft } from 'lucide-react'
+import { Loader2, ArrowLeft } from 'lucide-react'
 
 export default function ForgotPasswordPage() {
   const t = useTranslations('auth.forgotPassword')
@@ -42,11 +43,15 @@ export default function ForgotPasswordPage() {
 
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
-          <div className="mb-4 inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2">
-            <Gauge className="h-5 w-5 text-primary" />
-            <span className="gradient-text text-sm font-bold tracking-wider uppercase">
-              {tc('brandName')}
-            </span>
+          <div className="mb-4 inline-flex items-center gap-2">
+            <Image
+              src="/torqvoice_app_logo.png"
+              alt={tc('brandName')}
+              width={48}
+              height={44}
+              className="h-11 w-auto"
+              priority
+            />
           </div>
           <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
           <p className="mt-1 text-sm text-muted-foreground">

+ 11 - 6
src/app/(public)/auth/reset-password/page.tsx

@@ -3,12 +3,13 @@
 import { Suspense, useState } from 'react'
 import { useSearchParams } from 'next/navigation'
 import Link from 'next/link'
+import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { authClient } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
-import { Gauge, Loader2, XCircle } from 'lucide-react'
+import { Loader2, XCircle } from 'lucide-react'
 
 function ResetPasswordInner() {
   const t = useTranslations('auth.resetPassword')
@@ -143,11 +144,15 @@ export default function ResetPasswordPage() {
 
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
-          <div className="mb-4 inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2">
-            <Gauge className="h-5 w-5 text-primary" />
-            <span className="gradient-text text-sm font-bold tracking-wider uppercase">
-              {tc('brandName')}
-            </span>
+          <div className="mb-4 inline-flex items-center gap-2">
+            <Image
+              src="/torqvoice_app_logo.png"
+              alt={tc('brandName')}
+              width={48}
+              height={44}
+              className="h-11 w-auto"
+              priority
+            />
           </div>
           <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
           <p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>

+ 11 - 6
src/app/(public)/auth/sign-in/sign-in-form.tsx

@@ -3,13 +3,14 @@
 import { Suspense, useState } from 'react'
 import { useRouter, useSearchParams } from 'next/navigation'
 import Link from 'next/link'
+import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { signIn, authClient } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Separator } from '@/components/ui/separator'
-import { Fingerprint, Gauge, Loader2, XCircle } from 'lucide-react'
+import { Fingerprint, Loader2, XCircle } from 'lucide-react'
 
 function SignInFormInner({ registrationDisabled }: { registrationDisabled: boolean }) {
   const t = useTranslations('auth.signIn')
@@ -69,11 +70,15 @@ function SignInFormInner({ registrationDisabled }: { registrationDisabled: boole
   return (
     <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
       <div className="mb-8 text-center">
-        <div className="mb-4 inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2">
-          <Gauge className="h-5 w-5 text-primary" />
-          <span className="gradient-text text-sm font-bold tracking-wider uppercase">
-            {tc('brandName')}
-          </span>
+        <div className="mb-4 inline-flex items-center gap-2">
+          <Image
+            src="/torqvoice_app_logo.png"
+            alt={tc('brandName')}
+            width={48}
+            height={44}
+            className="h-11 w-auto"
+            priority
+          />
         </div>
         <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
         <p className="mt-1 text-sm text-muted-foreground">{t('description')}</p>

+ 11 - 6
src/app/(public)/auth/sign-up/sign-up-form.tsx

@@ -3,12 +3,13 @@
 import { useState } from 'react'
 import { useRouter } from 'next/navigation'
 import Link from 'next/link'
+import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { signUp } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
-import { Gauge, Loader2, XCircle } from 'lucide-react'
+import { Loader2, XCircle } from 'lucide-react'
 import { acceptInvitation } from '@/features/team/Actions/acceptInvitation'
 
 export function SignUpForm({ inviteToken, emailVerificationRequired, redirectTo }: { inviteToken?: string; emailVerificationRequired?: boolean; redirectTo?: string }) {
@@ -73,11 +74,15 @@ export function SignUpForm({ inviteToken, emailVerificationRequired, redirectTo
 
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
-          <div className="mb-4 inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2">
-            <Gauge className="h-5 w-5 text-primary" />
-            <span className="gradient-text text-sm font-bold tracking-wider uppercase">
-              {tc('brandName')}
-            </span>
+          <div className="mb-4 inline-flex items-center gap-2">
+            <Image
+              src="/torqvoice_app_logo.png"
+              alt={tc('brandName')}
+              width={48}
+              height={44}
+              className="h-11 w-auto"
+              priority
+            />
           </div>
           <h1 className="text-2xl font-bold tracking-tight">{t('title')}</h1>
           <p className="mt-1 text-sm text-muted-foreground">

+ 11 - 6
src/app/(public)/auth/verify-2fa/page.tsx

@@ -2,13 +2,14 @@
 
 import { useState } from 'react'
 import { useRouter } from 'next/navigation'
+import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { authClient } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Checkbox } from '@/components/ui/checkbox'
-import { Gauge, Loader2, Shield, XCircle } from 'lucide-react'
+import { Loader2, Shield, XCircle } from 'lucide-react'
 
 export default function VerifyTwoFactorPage() {
   const t = useTranslations('auth.verify2fa')
@@ -53,11 +54,15 @@ export default function VerifyTwoFactorPage() {
 
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
-          <div className="mb-4 inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2">
-            <Gauge className="h-5 w-5 text-primary" />
-            <span className="gradient-text text-sm font-bold tracking-wider uppercase">
-              {tc('brandName')}
-            </span>
+          <div className="mb-4 inline-flex items-center gap-2">
+            <Image
+              src="/torqvoice_app_logo.png"
+              alt={tc('brandName')}
+              width={48}
+              height={44}
+              className="h-11 w-auto"
+              priority
+            />
           </div>
           <div className="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-primary/10">
             <Shield className="h-6 w-6 text-primary" />

+ 11 - 6
src/app/(public)/auth/verify-email/page.tsx

@@ -1,10 +1,11 @@
 'use client'
 
 import { useState, useCallback, useEffect, useRef } from 'react'
+import Image from 'next/image'
 import { useTranslations } from 'next-intl'
 import { authClient, signOut, useSession } from '@/lib/auth-client'
 import { Button } from '@/components/ui/button'
-import { Gauge, Loader2, Mail, LogOut } from 'lucide-react'
+import { Loader2, Mail, LogOut } from 'lucide-react'
 import { useRouter } from 'next/navigation'
 import { useCooldown } from '@/hooks/use-cooldown'
 
@@ -88,11 +89,15 @@ export default function VerifyEmailPage() {
 
       <div className="glass relative z-10 w-full max-w-md rounded-2xl p-8 shadow-2xl">
         <div className="mb-8 text-center">
-          <div className="mb-4 inline-flex items-center gap-2 rounded-full bg-primary/10 px-4 py-2">
-            <Gauge className="h-5 w-5 text-primary" />
-            <span className="gradient-text text-sm font-bold tracking-wider uppercase">
-              {tc('brandName')}
-            </span>
+          <div className="mb-4 inline-flex items-center gap-2">
+            <Image
+              src="/torqvoice_app_logo.png"
+              alt={tc('brandName')}
+              width={48}
+              height={44}
+              className="h-11 w-auto"
+              priority
+            />
           </div>
           <div className="mx-auto mb-4 flex h-16 w-16 items-center justify-center rounded-full bg-primary/10">
             <Mail className="h-8 w-8 text-primary" />

+ 67 - 8
src/app/(public)/terms/page.tsx

@@ -27,7 +27,7 @@ export default function TermsOfServicePage() {
           </div>
           <h1 className="mt-4 text-2xl font-bold">Terms of Service</h1>
           <p className="mt-1 text-sm text-muted-foreground">
-            Last updated: February 2026
+            Last updated: March 2026
           </p>
         </div>
 
@@ -187,7 +187,66 @@ export default function TermsOfServicePage() {
 
           <section>
             <h2 className="mb-2 text-base font-semibold text-foreground">
-              8. Financial and Legal Disclaimer
+              8. Third-Party Services and AI
+            </h2>
+            <p>
+              The Service may integrate with third-party services, including but
+              not limited to:
+            </p>
+            <ul className="mt-2 list-disc space-y-1 pl-6">
+              <li>
+                <strong>AI Providers</strong> (OpenAI, Anthropic): If you enable
+                AI features, data including service descriptions, vehicle
+                information, part details, and uploaded images may be sent to
+                these providers for processing. You are responsible for
+                reviewing and complying with their respective terms of service
+                and privacy policies. Torqvoice does not control how these
+                providers process, store, or use the data sent to them.
+              </li>
+              <li>
+                <strong>Payment Providers</strong> (Stripe, Vipps, PayPal): If
+                you enable online payments, customer payment information is
+                processed directly by these providers. Torqvoice does not store
+                credit card numbers or bank account details. You are responsible
+                for compliance with PCI-DSS and applicable payment regulations.
+              </li>
+              <li>
+                <strong>Messaging Services</strong> (Telegram, SMS, Email): If
+                you enable messaging integrations, customer contact information
+                and message content may be transmitted through these services.
+              </li>
+            </ul>
+            <p className="mt-2">
+              Torqvoice is not responsible for the practices, content, or
+              availability of any third-party service. Your use of third-party
+              integrations is at your own risk and subject to those
+              providers&apos; terms.
+            </p>
+          </section>
+
+          <section>
+            <h2 className="mb-2 text-base font-semibold text-foreground">
+              9. Uploaded Content and Files
+            </h2>
+            <p>
+              You may upload files, images, documents, and other content to the
+              Service. You retain ownership of all uploaded content. By
+              uploading content, you represent that you have the right to do so
+              and that the content does not violate any third-party rights.
+              Torqvoice is not responsible for the content you upload and does
+              not monitor uploaded files for legality or appropriateness.
+            </p>
+            <p className="mt-2">
+              For self-hosted instances, all uploaded files are stored on your
+              own server infrastructure. For cloud-hosted instances, files are
+              stored on our servers and may be deleted when you delete your
+              account or the associated records.
+            </p>
+          </section>
+
+          <section>
+            <h2 className="mb-2 text-base font-semibold text-foreground">
+              10. Financial and Legal Disclaimer
             </h2>
             <p>
               The Service is a tool to assist with workshop management. It does
@@ -203,7 +262,7 @@ export default function TermsOfServicePage() {
 
           <section>
             <h2 className="mb-2 text-base font-semibold text-foreground">
-              9. Service Availability
+              11. Service Availability
             </h2>
             <p>
               Torqvoice reserves the right to modify, suspend, or discontinue
@@ -215,7 +274,7 @@ export default function TermsOfServicePage() {
 
           <section>
             <h2 className="mb-2 text-base font-semibold text-foreground">
-              10. Account Termination
+              12. Account Termination
             </h2>
             <p>
               We may terminate or suspend your account and access to the Service
@@ -228,7 +287,7 @@ export default function TermsOfServicePage() {
 
           <section>
             <h2 className="mb-2 text-base font-semibold text-foreground">
-              11. Changes to Terms
+              13. Changes to Terms
             </h2>
             <p>
               We reserve the right to modify these Terms at any time. Changes
@@ -241,7 +300,7 @@ export default function TermsOfServicePage() {
 
           <section>
             <h2 className="mb-2 text-base font-semibold text-foreground">
-              12. Governing Law
+              14. Governing Law
             </h2>
             <p>
               These Terms shall be governed by and construed in accordance with
@@ -252,7 +311,7 @@ export default function TermsOfServicePage() {
 
           <section>
             <h2 className="mb-2 text-base font-semibold text-foreground">
-              13. Severability
+              15. Severability
             </h2>
             <p>
               If any provision of these Terms is held to be unenforceable or
@@ -265,7 +324,7 @@ export default function TermsOfServicePage() {
 
           <section>
             <h2 className="mb-2 text-base font-semibold text-foreground">
-              14. Entire Agreement
+              16. Entire Agreement
             </h2>
             <p>
               These Terms constitute the entire agreement between you and

+ 155 - 135
src/features/vehicles/Components/MyActiveJobs.tsx

@@ -1,73 +1,121 @@
-"use client";
+'use client'
 
-import { useRef, useState, useCallback } from "react";
-import { useRouter } from "next/navigation";
-import { useTranslations } from "next-intl";
-import { Camera, ImageIcon, Loader2, Wrench, Clock, Pause, ScanBarcode, Package } from "lucide-react";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { toast } from "sonner";
-import { compressImage } from "@/lib/compress-image";
-import { addServiceAttachment } from "@/features/vehicles/Actions/addServiceAttachment";
-import { addPartToServiceRecord } from "@/features/vehicles/Actions/addPartToServiceRecord";
-import { lookupPartByBarcode } from "@/features/inventory/Actions/lookupPartByBarcode";
-import { BarcodeScannerDialog } from "@/components/barcode-scanner-dialog";
-import { CreatePartDialog } from "./CreatePartDialog";
-import type { MyActiveJob } from "@/features/vehicles/Actions/getMyActiveJobs";
+import { useRef, useState, useCallback } from 'react'
+import { useRouter } from 'next/navigation'
+import { useTranslations } from 'next-intl'
+import {
+  Camera,
+  ImageIcon,
+  Loader2,
+  Wrench,
+  Clock,
+  Pause,
+  ScanBarcode,
+  Package,
+} from 'lucide-react'
+import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { toast } from 'sonner'
+import { compressImage } from '@/lib/compress-image'
+import { addServiceAttachment } from '@/features/vehicles/Actions/addServiceAttachment'
+import { addPartToServiceRecord } from '@/features/vehicles/Actions/addPartToServiceRecord'
+import { lookupPartByBarcode } from '@/features/inventory/Actions/lookupPartByBarcode'
+import { BarcodeScannerDialog } from '@/components/barcode-scanner-dialog'
+import { CreatePartDialog } from './CreatePartDialog'
+import type { MyActiveJob } from '@/features/vehicles/Actions/getMyActiveJobs'
 
 interface MyActiveJobsProps {
-  jobs: MyActiveJob[];
+  jobs: MyActiveJob[]
 }
 
 const STATUS_ICON: Record<string, typeof Wrench> = {
-  "in-progress": Wrench,
+  'in-progress': Wrench,
   pending: Clock,
-  "waiting-parts": Pause,
-};
+  'waiting-parts': Pause,
+}
 
 const STATUS_COLOR: Record<string, string> = {
-  "in-progress": "bg-blue-500/10 text-blue-600",
-  pending: "bg-amber-500/10 text-amber-600",
-  "waiting-parts": "bg-orange-500/10 text-orange-600",
-};
+  'in-progress': 'bg-blue-500/10 text-blue-600',
+  pending: 'bg-amber-500/10 text-amber-600',
+  'waiting-parts': 'bg-orange-500/10 text-orange-600',
+}
 
 export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
-  const t = useTranslations("dashboard.myJobs");
-  const router = useRouter();
-  const [uploading, setUploading] = useState<string | null>(null);
-  const [imageCounts, setImageCounts] = useState<Record<string, number>>(
-    () => Object.fromEntries(jobs.map((j) => [j.id, j.imageCount]))
-  );
-  const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({});
+  const t = useTranslations('dashboard.myJobs')
+  const router = useRouter()
+  const [uploading, setUploading] = useState<string | null>(null)
+  const [imageCounts, setImageCounts] = useState<Record<string, number>>(() =>
+    Object.fromEntries(jobs.map((j) => [j.id, j.imageCount]))
+  )
+  const fileInputRefs = useRef<Record<string, HTMLInputElement | null>>({})
 
   // Barcode scanner state
-  const [scannerOpen, setScannerOpen] = useState(false);
-  const [scanJobId, setScanJobId] = useState<string | null>(null);
-  const [createPartOpen, setCreatePartOpen] = useState(false);
-  const [pendingBarcode, setPendingBarcode] = useState("");
-  const [partCounts, setPartCounts] = useState<Record<string, number>>(
-    () => Object.fromEntries(jobs.map((j) => [j.id, j.partCount]))
-  );
+  const [scannerOpen, setScannerOpen] = useState(false)
+  const [scanJobId, setScanJobId] = useState<string | null>(null)
+  const [createPartOpen, setCreatePartOpen] = useState(false)
+  const [pendingBarcode, setPendingBarcode] = useState('')
+  const [partCounts, setPartCounts] = useState<Record<string, number>>(() =>
+    Object.fromEntries(jobs.map((j) => [j.id, j.partCount]))
+  )
 
-  if (jobs.length === 0) return null;
+  if (jobs.length === 0) return null
 
   const handleCameraClick = (jobId: string) => {
-    fileInputRefs.current[jobId]?.click();
-  };
+    fileInputRefs.current[jobId]?.click()
+  }
 
   const handleScanClick = (jobId: string) => {
-    setScanJobId(jobId);
-    setScannerOpen(true);
-  };
+    setScanJobId(jobId)
+    setScannerOpen(true)
+  }
+
+  const handleBarcodeScan = useCallback(
+    async (barcode: string) => {
+      if (!scanJobId) return
+      setScannerOpen(false)
+
+      const result = await lookupPartByBarcode(barcode)
+      if (result.success && result.data) {
+        const part = result.data
+        const price = part.sellPrice > 0 ? part.sellPrice : part.unitCost
+        const addResult = await addPartToServiceRecord({
+          serviceRecordId: scanJobId,
+          partNumber: part.partNumber || undefined,
+          name: part.name,
+          quantity: 1,
+          unitPrice: price,
+          total: price,
+          unitCost: part.unitCost,
+          inventoryPartId: part.id,
+        })
+        if (addResult.success) {
+          toast.success(t('partAdded', { name: part.name }))
+          setPartCounts((prev) => ({ ...prev, [scanJobId]: (prev[scanJobId] || 0) + 1 }))
+          router.refresh()
+        } else {
+          toast.error(addResult.error || t('partAddFailed'))
+        }
+      } else {
+        // Part not found — offer to create it
+        setPendingBarcode(barcode)
+        setCreatePartOpen(true)
+      }
+    },
+    [scanJobId, t, router]
+  )
 
-  const handleBarcodeScan = useCallback(async (barcode: string) => {
-    if (!scanJobId) return;
-    setScannerOpen(false);
+  const handlePartCreated = useCallback(
+    async (part: {
+      id: string
+      name: string
+      partNumber: string | null
+      sellPrice: number
+      unitCost: number
+    }) => {
+      if (!scanJobId) return
+      setCreatePartOpen(false)
 
-    const result = await lookupPartByBarcode(barcode);
-    if (result.success && result.data) {
-      const part = result.data;
-      const price = part.sellPrice > 0 ? part.sellPrice : part.unitCost;
+      const price = part.sellPrice > 0 ? part.sellPrice : part.unitCost
       const addResult = await addPartToServiceRecord({
         serviceRecordId: scanJobId,
         partNumber: part.partNumber || undefined,
@@ -77,71 +125,44 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
         total: price,
         unitCost: part.unitCost,
         inventoryPartId: part.id,
-      });
+      })
       if (addResult.success) {
-        toast.success(t("partAdded", { name: part.name }));
-        setPartCounts((prev) => ({ ...prev, [scanJobId]: (prev[scanJobId] || 0) + 1 }));
-        router.refresh();
+        toast.success(t('partAdded', { name: part.name }))
+        setPartCounts((prev) => ({ ...prev, [scanJobId]: (prev[scanJobId] || 0) + 1 }))
+        router.refresh()
       } else {
-        toast.error(addResult.error || t("partAddFailed"));
+        toast.error(addResult.error || t('partAddFailed'))
       }
-    } else {
-      // Part not found — offer to create it
-      setPendingBarcode(barcode);
-      setCreatePartOpen(true);
-    }
-  }, [scanJobId, t, router]);
-
-  const handlePartCreated = useCallback(async (part: { id: string; name: string; partNumber: string | null; sellPrice: number; unitCost: number }) => {
-    if (!scanJobId) return;
-    setCreatePartOpen(false);
-
-    const price = part.sellPrice > 0 ? part.sellPrice : part.unitCost;
-    const addResult = await addPartToServiceRecord({
-      serviceRecordId: scanJobId,
-      partNumber: part.partNumber || undefined,
-      name: part.name,
-      quantity: 1,
-      unitPrice: price,
-      total: price,
-      unitCost: part.unitCost,
-      inventoryPartId: part.id,
-    });
-    if (addResult.success) {
-      toast.success(t("partAdded", { name: part.name }));
-      setPartCounts((prev) => ({ ...prev, [scanJobId]: (prev[scanJobId] || 0) + 1 }));
-      router.refresh();
-    } else {
-      toast.error(addResult.error || t("partAddFailed"));
-    }
-  }, [scanJobId, t, router]);
+    },
+    [scanJobId, t, router]
+  )
 
   const handleFileSelect = async (jobId: string, files: FileList | null) => {
-    if (!files || files.length === 0) return;
-    setUploading(jobId);
+    if (!files || files.length === 0) return
+    setUploading(jobId)
 
-    let uploaded = 0;
+    let uploaded = 0
     for (let i = 0; i < files.length; i++) {
-      let file = files[i];
-      if (!file.type.startsWith("image/")) continue;
+      let file = files[i]
+      if (!file.type.startsWith('image/')) continue
 
       try {
-        file = await compressImage(file);
-        const formData = new FormData();
-        formData.append("file", file);
+        file = await compressImage(file)
+        const formData = new FormData()
+        formData.append('file', file)
 
-        const res = await fetch("/api/protected/upload/service-files", {
-          method: "POST",
+        const res = await fetch('/api/protected/upload/service-files', {
+          method: 'POST',
           body: formData,
-        });
+        })
 
         if (!res.ok) {
-          const err = await res.json();
-          toast.error(err.error || t("uploadFailed"));
-          continue;
+          const err = await res.json()
+          toast.error(err.error || t('uploadFailed'))
+          continue
         }
 
-        const data = await res.json();
+        const data = await res.json()
         await addServiceAttachment({
           serviceRecordId: jobId,
           attachment: {
@@ -149,66 +170,63 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
             fileUrl: data.url,
             fileType: data.fileType,
             fileSize: data.fileSize,
-            category: "image",
+            category: 'image',
             includeInInvoice: true,
           },
-        });
-        uploaded++;
+        })
+        uploaded++
       } catch {
-        toast.error(t("uploadFailed"));
+        toast.error(t('uploadFailed'))
       }
     }
 
     if (uploaded > 0) {
-      toast.success(t("uploadSuccess", { count: uploaded }));
+      toast.success(t('uploadSuccess', { count: uploaded }))
       setImageCounts((prev) => ({
         ...prev,
         [jobId]: (prev[jobId] || 0) + uploaded,
-      }));
-      router.refresh();
+      }))
+      router.refresh()
     }
 
-    setUploading(null);
-    const input = fileInputRefs.current[jobId];
-    if (input) input.value = "";
-  };
+    setUploading(null)
+    const input = fileInputRefs.current[jobId]
+    if (input) input.value = ''
+  }
 
   return (
     <>
       <Card className="border-0 shadow-sm">
-        <CardHeader className="pb-3">
+        <CardHeader className="px-4 pb-1 pt-3">
           <CardTitle className="flex items-center gap-2 text-base">
             <Wrench className="h-4 w-4" />
-            {t("title")}
+            {t('title')}
           </CardTitle>
-          <p className="text-xs text-muted-foreground">{t("description")}</p>
+          <p className="text-xs text-muted-foreground">{t('description')}</p>
         </CardHeader>
         <CardContent className="p-0">
           <div className="divide-y">
             {jobs.map((job) => {
-              const StatusIcon = STATUS_ICON[job.status] || Wrench;
-              const statusColor = STATUS_COLOR[job.status] || "bg-muted text-muted-foreground";
-              const isUploading = uploading === job.id;
-              const imgCount = imageCounts[job.id] || 0;
-              const prtCount = partCounts[job.id] || 0;
+              const StatusIcon = STATUS_ICON[job.status] || Wrench
+              const statusColor = STATUS_COLOR[job.status] || 'bg-muted text-muted-foreground'
+              const isUploading = uploading === job.id
+              const imgCount = imageCounts[job.id] || 0
+              const prtCount = partCounts[job.id] || 0
 
               return (
-                <div
-                  key={job.id}
-                  className="px-5 py-3"
-                >
+                <div key={job.id} className="px-4 py-2">
                   <div className="flex items-center justify-between">
                     <div
                       className="flex items-center gap-3 min-w-0 cursor-pointer hover:opacity-80"
                       onClick={() => router.push(`/vehicles/${job.vehicleId}/service/${job.id}`)}
                     >
-                      <div className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${statusColor}`}>
+                      <div
+                        className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${statusColor}`}
+                      >
                         <StatusIcon className="h-4 w-4" />
                       </div>
                       <div className="min-w-0">
-                        <p className="font-medium text-sm truncate">
-                          {job.title}
-                        </p>
+                        <p className="font-medium text-sm truncate">{job.title}</p>
                         <p className="text-xs text-muted-foreground truncate">
                           {job.vehicle.year} {job.vehicle.make} {job.vehicle.model}
                           {job.vehicle.licensePlate && ` · ${job.vehicle.licensePlate}`}
@@ -237,7 +255,7 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                         className="h-9 w-9"
                         disabled={isUploading}
                         onClick={() => handleCameraClick(job.id)}
-                        title={t("takePhoto")}
+                        title={t('takePhoto')}
                       >
                         {isUploading ? (
                           <Loader2 className="h-4 w-4 animate-spin" />
@@ -250,7 +268,7 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                         size="icon"
                         className="h-9 w-9"
                         onClick={() => handleScanClick(job.id)}
-                        title={t("scanPart")}
+                        title={t('scanPart')}
                       >
                         <ScanBarcode className="h-4 w-4" />
                       </Button>
@@ -285,7 +303,7 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                       ) : (
                         <Camera className="mr-1.5 h-4 w-4" />
                       )}
-                      {t("takePhoto")}
+                      {t('takePhoto')}
                     </Button>
                     <Button
                       variant="outline"
@@ -294,11 +312,13 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                       onClick={() => handleScanClick(job.id)}
                     >
                       <ScanBarcode className="mr-1.5 h-4 w-4" />
-                      {t("scanPart")}
+                      {t('scanPart')}
                     </Button>
                   </div>
                   <input
-                    ref={(el) => { fileInputRefs.current[job.id] = el; }}
+                    ref={(el) => {
+                      fileInputRefs.current[job.id] = el
+                    }}
                     type="file"
                     accept="image/*"
                     capture="environment"
@@ -307,7 +327,7 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
                     onChange={(e) => handleFileSelect(job.id, e.target.files)}
                   />
                 </div>
-              );
+              )
             })}
           </div>
         </CardContent>
@@ -317,7 +337,7 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
         open={scannerOpen}
         onOpenChange={setScannerOpen}
         onScan={handleBarcodeScan}
-        title={t("scanPart")}
+        title={t('scanPart')}
       />
 
       <CreatePartDialog
@@ -327,5 +347,5 @@ export function MyActiveJobs({ jobs }: MyActiveJobsProps) {
         onCreated={handlePartCreated}
       />
     </>
-  );
+  )
 }