Explorar o código

date time settings

Bernt Christian Egeland hai 7 meses
pai
achega
bb41ed7db0
Modificáronse 26 ficheiros con 513 adicións e 75 borrados
  1. 3 2
      src/app/(authenticated)/billing/billing-client.tsx
  2. 4 3
      src/app/(authenticated)/dashboard-client.tsx
  3. 7 0
      src/app/(authenticated)/layout.tsx
  4. 4 3
      src/app/(authenticated)/quotes/[id]/quote-detail-client.tsx
  5. 3 2
      src/app/(authenticated)/quotes/quotes-client.tsx
  6. 174 2
      src/app/(authenticated)/settings/appearance/appearance-settings.tsx
  7. 25 2
      src/app/(authenticated)/settings/appearance/page.tsx
  8. 2 3
      src/app/(authenticated)/settings/currency/currency-settings.tsx
  9. 3 2
      src/app/(authenticated)/vehicles/[id]/service-records-table.tsx
  10. 4 3
      src/app/(authenticated)/vehicles/[id]/service/[serviceId]/service-detail-client.tsx
  11. 5 4
      src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx
  12. 3 2
      src/app/(authenticated)/work-orders/work-orders-client.tsx
  13. 15 13
      src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx
  14. 4 0
      src/app/(public)/share/invoice/[orgId]/[token]/page.tsx
  15. 2 0
      src/app/api/quotes/[id]/pdf/route.ts
  16. 7 5
      src/app/api/services/[id]/pdf/route.ts
  17. 7 1
      src/app/api/share/invoice/[orgId]/[token]/pdf/route.ts
  18. 46 0
      src/components/date-settings-context.tsx
  19. 3 2
      src/features/billing/Components/RecurringInvoicesClient.tsx
  20. 9 11
      src/features/quotes/Components/QuotePDF.tsx
  21. 3 0
      src/features/settings/Schema/settingsSchema.ts
  22. 10 13
      src/features/vehicles/Components/InvoicePDF.tsx
  23. 1 2
      src/features/vehicles/Components/ReminderForm.tsx
  24. 140 0
      src/lib/format.ts
  25. 9 0
      src/lib/get-layout-data.ts
  26. 20 0
      src/lib/use-format-date.ts

+ 3 - 2
src/app/(authenticated)/billing/billing-client.tsx

@@ -3,7 +3,7 @@
 import { useState, useCallback, useTransition } from "react";
 import { useState, useCallback, useTransition } from "react";
 import Link from "next/link";
 import Link from "next/link";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
-import { format } from "date-fns";
+import { useFormatDate } from "@/lib/use-format-date";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
@@ -85,6 +85,7 @@ export default function BillingClient({
   statusFilter,
   statusFilter,
 }: BillingClientProps) {
 }: BillingClientProps) {
   const router = useRouter();
   const router = useRouter();
+  const { formatDate } = useFormatDate();
   const pathname = usePathname();
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
   const [isPending, startTransition] = useTransition();
@@ -322,7 +323,7 @@ export default function BillingClient({
                       {record.vehicle.customer?.name || "—"}
                       {record.vehicle.customer?.name || "—"}
                     </TableCell>
                     </TableCell>
                     <TableCell>
                     <TableCell>
-                      {format(new Date(record.serviceDate), "MMM d, yyyy")}
+                      {formatDate(new Date(record.serviceDate))}
                     </TableCell>
                     </TableCell>
                     <TableCell className="text-right">
                     <TableCell className="text-right">
                       {fmt(record.totalAmount)}
                       {fmt(record.totalAmount)}

+ 4 - 3
src/app/(authenticated)/dashboard-client.tsx

@@ -2,7 +2,7 @@
 
 
 import { useState } from "react";
 import { useState } from "react";
 import { useRouter } from "next/navigation";
 import { useRouter } from "next/navigation";
-import { format } from "date-fns";
+import { useFormatDate } from "@/lib/use-format-date";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
@@ -79,6 +79,7 @@ export function DashboardClient({
   upcomingReminders?: ReminderItem[];
   upcomingReminders?: ReminderItem[];
 }) {
 }) {
   const router = useRouter();
   const router = useRouter();
+  const { formatDate } = useFormatDate();
   const [navigatingId, setNavigatingId] = useState<string | null>(null);
   const [navigatingId, setNavigatingId] = useState<string | null>(null);
 
 
   return (
   return (
@@ -200,7 +201,7 @@ export function DashboardClient({
                       )}
                       )}
                       {r.dueDate && !isOverdue && !isDueSoon && (
                       {r.dueDate && !isOverdue && !isDueSoon && (
                         <span className="text-xs text-muted-foreground">
                         <span className="text-xs text-muted-foreground">
-                          {format(new Date(r.dueDate), "MMM d")}
+                          {formatDate(new Date(r.dueDate))}
                         </span>
                         </span>
                       )}
                       )}
                     </div>
                     </div>
@@ -248,7 +249,7 @@ export function DashboardClient({
                       }}
                       }}
                     >
                     >
                       <TableCell className="font-mono text-xs">
                       <TableCell className="font-mono text-xs">
-                        {format(new Date(s.serviceDate), "MM/dd/yy")}
+                        {formatDate(new Date(s.serviceDate))}
                       </TableCell>
                       </TableCell>
                       <TableCell>
                       <TableCell>
                         <div>
                         <div>

+ 7 - 0
src/app/(authenticated)/layout.tsx

@@ -6,6 +6,7 @@ import { ConfirmProvider } from "@/components/confirm-dialog";
 import { getLayoutData } from "@/lib/get-layout-data";
 import { getLayoutData } from "@/lib/get-layout-data";
 import { getFeatures, isCloudMode } from "@/lib/features";
 import { getFeatures, isCloudMode } from "@/lib/features";
 import { WhiteLabelCtaProvider } from "@/components/white-label-cta-context";
 import { WhiteLabelCtaProvider } from "@/components/white-label-cta-context";
+import { DateSettingsProvider } from "@/components/date-settings-context";
 
 
 export default async function DashboardLayout({
 export default async function DashboardLayout({
   children,
   children,
@@ -21,6 +22,11 @@ export default async function DashboardLayout({
   const showWhiteLabelCta = !isCloudMode() && !features.brandingRemoved;
   const showWhiteLabelCta = !isCloudMode() && !features.brandingRemoved;
 
 
   return (
   return (
+    <DateSettingsProvider
+      dateFormat={data.dateFormat}
+      timeFormat={data.timeFormat}
+      timezone={data.timezone}
+    >
     <WhiteLabelCtaProvider show={showWhiteLabelCta}>
     <WhiteLabelCtaProvider show={showWhiteLabelCta}>
     <SidebarProvider
     <SidebarProvider
       style={
       style={
@@ -42,5 +48,6 @@ export default async function DashboardLayout({
       </ConfirmProvider>
       </ConfirmProvider>
     </SidebarProvider>
     </SidebarProvider>
     </WhiteLabelCtaProvider>
     </WhiteLabelCtaProvider>
+    </DateSettingsProvider>
   );
   );
 }
 }

+ 4 - 3
src/app/(authenticated)/quotes/[id]/quote-detail-client.tsx

@@ -3,7 +3,7 @@
 import { useState } from "react";
 import { useState } from "react";
 import Link from "next/link";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
 import { useRouter } from "next/navigation";
-import { format } from "date-fns";
+import { useFormatDate } from "@/lib/use-format-date";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
@@ -95,6 +95,7 @@ export function QuoteDetailClient({
 }) {
 }) {
   const cs = getCurrencySymbol(currencyCode);
   const cs = getCurrencySymbol(currencyCode);
   const router = useRouter();
   const router = useRouter();
+  const { formatDate } = useFormatDate();
   const modal = useGlassModal();
   const modal = useGlassModal();
   const confirm = useConfirm();
   const confirm = useConfirm();
   const [downloading, setDownloading] = useState(false);
   const [downloading, setDownloading] = useState(false);
@@ -175,8 +176,8 @@ export function QuoteDetailClient({
               <h1 className="text-2xl font-bold">{quote.title}</h1>
               <h1 className="text-2xl font-bold">{quote.title}</h1>
             </div>
             </div>
             <p className="mt-1 text-sm text-muted-foreground">
             <p className="mt-1 text-sm text-muted-foreground">
-              {quote.quoteNumber} · Created {format(new Date(quote.createdAt), "MMMM d, yyyy")}
-              {quote.validUntil && ` · Valid until ${format(new Date(quote.validUntil), "MMM d, yyyy")}`}
+              {quote.quoteNumber} · Created {formatDate(new Date(quote.createdAt))}
+              {quote.validUntil && ` · Valid until ${formatDate(new Date(quote.validUntil))}`}
             </p>
             </p>
           </div>
           </div>
           <div className="flex items-center gap-2">
           <div className="flex items-center gap-2">

+ 3 - 2
src/app/(authenticated)/quotes/quotes-client.tsx

@@ -2,7 +2,7 @@
 
 
 import { useState, useCallback, useTransition } from "react";
 import { useState, useCallback, useTransition } from "react";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
-import { format } from "date-fns";
+import { useFormatDate } from "@/lib/use-format-date";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
@@ -64,6 +64,7 @@ export function QuotesClient({
   statusFilter: string;
   statusFilter: string;
 }) {
 }) {
   const router = useRouter();
   const router = useRouter();
+  const { formatDate } = useFormatDate();
   const pathname = usePathname();
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
   const [isPending, startTransition] = useTransition();
@@ -181,7 +182,7 @@ export function QuotesClient({
                     </Badge>
                     </Badge>
                   </TableCell>
                   </TableCell>
                   <TableCell className="font-mono text-xs">
                   <TableCell className="font-mono text-xs">
-                    {format(new Date(q.createdAt), "MM/dd/yy")}
+                    {formatDate(new Date(q.createdAt))}
                   </TableCell>
                   </TableCell>
                   <TableCell className="text-right font-semibold">
                   <TableCell className="text-right font-semibold">
                     {formatCurrency(q.totalAmount, currencyCode)}
                     {formatCurrency(q.totalAmount, currencyCode)}

+ 174 - 2
src/app/(authenticated)/settings/appearance/appearance-settings.tsx

@@ -1,13 +1,104 @@
 'use client'
 'use client'
 
 
+import { useState } from 'react'
+import { useRouter } from 'next/navigation'
 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
 import { Label } from '@/components/ui/label'
 import { Label } from '@/components/ui/label'
 import { Switch } from '@/components/ui/switch'
 import { Switch } from '@/components/ui/switch'
+import { Button } from '@/components/ui/button'
 import { useTheme } from '@/components/theme-provider'
 import { useTheme } from '@/components/theme-provider'
-import { Moon, Palette, Sun } from 'lucide-react'
+import { Calendar, Clock, Loader2, Moon, Palette, Save, Sun } from 'lucide-react'
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from '@/components/ui/select'
+import { setSettings } from '@/features/settings/Actions/settingsActions'
+import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
+import { formatDate, formatDateTime, DEFAULT_DATE_FORMAT } from '@/lib/format'
+import { toast } from 'sonner'
 
 
-export function AppearanceSettings() {
+const DATE_FORMAT_OPTIONS = [
+  { value: "MMM d, yyyy", label: "MMM D, YYYY", example: "Feb 15, 2026" },
+  { value: "dd/MM/yyyy", label: "DD/MM/YYYY", example: "15/02/2026" },
+  { value: "MM/dd/yyyy", label: "MM/DD/YYYY", example: "02/15/2026" },
+  { value: "yyyy-MM-dd", label: "YYYY-MM-DD", example: "2026-02-15" },
+  { value: "d. MMM yyyy", label: "D. MMM YYYY", example: "15. Feb 2026" },
+  { value: "d MMMM yyyy", label: "D MMMM YYYY", example: "15 February 2026" },
+];
+
+const TIMEZONE_OPTIONS = [
+  "UTC",
+  "America/New_York",
+  "America/Chicago",
+  "America/Denver",
+  "America/Los_Angeles",
+  "America/Anchorage",
+  "Pacific/Honolulu",
+  "America/Toronto",
+  "America/Vancouver",
+  "America/Mexico_City",
+  "America/Sao_Paulo",
+  "America/Argentina/Buenos_Aires",
+  "Europe/London",
+  "Europe/Paris",
+  "Europe/Berlin",
+  "Europe/Madrid",
+  "Europe/Rome",
+  "Europe/Amsterdam",
+  "Europe/Brussels",
+  "Europe/Zurich",
+  "Europe/Vienna",
+  "Europe/Stockholm",
+  "Europe/Oslo",
+  "Europe/Copenhagen",
+  "Europe/Helsinki",
+  "Europe/Warsaw",
+  "Europe/Prague",
+  "Europe/Budapest",
+  "Europe/Bucharest",
+  "Europe/Athens",
+  "Europe/Istanbul",
+  "Europe/Moscow",
+  "Asia/Dubai",
+  "Asia/Kolkata",
+  "Asia/Bangkok",
+  "Asia/Singapore",
+  "Asia/Hong_Kong",
+  "Asia/Shanghai",
+  "Asia/Tokyo",
+  "Asia/Seoul",
+  "Australia/Sydney",
+  "Australia/Melbourne",
+  "Australia/Perth",
+  "Pacific/Auckland",
+  "Africa/Johannesburg",
+  "Africa/Cairo",
+  "Africa/Lagos",
+];
+
+export function AppearanceSettings({ settings }: { settings: Record<string, string> }) {
   const { theme, setTheme } = useTheme()
   const { theme, setTheme } = useTheme()
+  const router = useRouter()
+  const [saving, setSaving] = useState(false)
+
+  const [dateFormat, setDateFormat] = useState(settings[SETTING_KEYS.DATE_FORMAT] || DEFAULT_DATE_FORMAT)
+  const [timeFormat, setTimeFormat] = useState(settings[SETTING_KEYS.TIME_FORMAT] || "12h")
+  const [timezone, setTimezone] = useState(settings[SETTING_KEYS.TIMEZONE] || "")
+
+  const handleSave = async () => {
+    setSaving(true)
+    await setSettings({
+      [SETTING_KEYS.DATE_FORMAT]: dateFormat,
+      [SETTING_KEYS.TIME_FORMAT]: timeFormat,
+      [SETTING_KEYS.TIMEZONE]: timezone,
+    })
+    setSaving(false)
+    router.refresh()
+    toast.success("Settings saved")
+  }
 
 
   return (
   return (
     <div className="space-y-6">
     <div className="space-y-6">
@@ -40,6 +131,87 @@ export function AppearanceSettings() {
           </div>
           </div>
         </CardContent>
         </CardContent>
       </Card>
       </Card>
+
+      <Card className="border-0 shadow-sm">
+        <CardHeader className="flex flex-row items-center gap-3 pb-4">
+          <Calendar className="h-5 w-5 text-muted-foreground" />
+          <CardTitle className="text-lg">Date & Time</CardTitle>
+        </CardHeader>
+        <CardContent className="space-y-6">
+          <p className="text-sm text-muted-foreground">
+            Configure how dates and times are displayed across the application.
+          </p>
+
+          <div className="space-y-2">
+            <Label>Date Format</Label>
+            <Select value={dateFormat} onValueChange={setDateFormat}>
+              <SelectTrigger className="w-64">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                {DATE_FORMAT_OPTIONS.map((opt) => (
+                  <SelectItem key={opt.value} value={opt.value}>
+                    {opt.label} ({opt.example})
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          </div>
+
+          <div className="space-y-2">
+            <Label>Time Format</Label>
+            <Select value={timeFormat} onValueChange={setTimeFormat}>
+              <SelectTrigger className="w-48">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="12h">12-hour (2:30 PM)</SelectItem>
+                <SelectItem value="24h">24-hour (14:30)</SelectItem>
+              </SelectContent>
+            </Select>
+          </div>
+
+          <div className="space-y-2">
+            <Label>Timezone</Label>
+            <Select value={timezone || "__auto__"} onValueChange={(v) => setTimezone(v === "__auto__" ? "" : v)}>
+              <SelectTrigger className="w-72">
+                <SelectValue />
+              </SelectTrigger>
+              <SelectContent>
+                <SelectItem value="__auto__">Auto-detect (browser)</SelectItem>
+                {TIMEZONE_OPTIONS.map((tz) => (
+                  <SelectItem key={tz} value={tz}>
+                    {tz.replace(/_/g, " ")}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            <p className="text-xs text-muted-foreground">
+              When set to auto-detect, each user sees dates in their browser&apos;s timezone.
+            </p>
+          </div>
+
+          <div className="rounded-lg border p-3 text-sm text-muted-foreground">
+            <div className="flex items-center gap-2 mb-1">
+              <Clock className="h-3.5 w-3.5" />
+              <span className="font-medium text-foreground">Preview</span>
+            </div>
+            <p>Date: <span className="font-medium text-foreground">{formatDate(new Date(), dateFormat, timezone || undefined)}</span></p>
+            <p>Date & Time: <span className="font-medium text-foreground">{formatDateTime(new Date(), dateFormat, timeFormat as "12h" | "24h", timezone || undefined)}</span></p>
+          </div>
+
+          <div className="flex items-center gap-3">
+            <Button onClick={handleSave} disabled={saving}>
+              {saving ? (
+                <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+              ) : (
+                <Save className="mr-2 h-4 w-4" />
+              )}
+              Save Settings
+            </Button>
+          </div>
+        </CardContent>
+      </Card>
     </div>
     </div>
   )
   )
 }
 }

+ 25 - 2
src/app/(authenticated)/settings/appearance/page.tsx

@@ -1,5 +1,28 @@
 import { AppearanceSettings } from "./appearance-settings";
 import { AppearanceSettings } from "./appearance-settings";
+import { db } from "@/lib/db";
+import { getAuthContext } from "@/lib/get-auth-context";
+import { redirect } from "next/navigation";
+import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 
 
-export default function AppearanceSettingsPage() {
-  return <AppearanceSettings />;
+export default async function AppearanceSettingsPage() {
+  const ctx = await getAuthContext();
+  if (!ctx) redirect("/sign-in");
+
+  const rows = await db.appSetting.findMany({
+    where: {
+      organizationId: ctx.organizationId,
+      key: {
+        in: [
+          SETTING_KEYS.DATE_FORMAT,
+          SETTING_KEYS.TIME_FORMAT,
+          SETTING_KEYS.TIMEZONE,
+        ],
+      },
+    },
+  });
+
+  const settings: Record<string, string> = {};
+  for (const r of rows) settings[r.key] = r.value;
+
+  return <AppearanceSettings settings={settings} />;
 }
 }

+ 2 - 3
src/app/(authenticated)/settings/currency/currency-settings.tsx

@@ -64,7 +64,6 @@ export function CurrencySettings({ settings }: { settings: Record<string, string
   const [taxEnabled, setTaxEnabled] = useState(settings[SETTING_KEYS.TAX_ENABLED] !== "false");
   const [taxEnabled, setTaxEnabled] = useState(settings[SETTING_KEYS.TAX_ENABLED] !== "false");
   const [defaultTaxRate, setDefaultTaxRate] = useState(settings[SETTING_KEYS.DEFAULT_TAX_RATE] || "0");
   const [defaultTaxRate, setDefaultTaxRate] = useState(settings[SETTING_KEYS.DEFAULT_TAX_RATE] || "0");
   const [unitSystem, setUnitSystem] = useState(settings[SETTING_KEYS.UNIT_SYSTEM] || "imperial");
   const [unitSystem, setUnitSystem] = useState(settings[SETTING_KEYS.UNIT_SYSTEM] || "imperial");
-
   const handleSave = async () => {
   const handleSave = async () => {
     setSaving(true);
     setSaving(true);
     await setSettings({
     await setSettings({
@@ -75,7 +74,7 @@ export function CurrencySettings({ settings }: { settings: Record<string, string
     });
     });
     setSaving(false);
     setSaving(false);
     router.refresh();
     router.refresh();
-    toast.success("Currency settings saved");
+    toast.success("Settings saved");
   };
   };
 
 
   return (
   return (
@@ -188,7 +187,7 @@ export function CurrencySettings({ settings }: { settings: Record<string, string
               ) : (
               ) : (
                 <Save className="mr-2 h-4 w-4" />
                 <Save className="mr-2 h-4 w-4" />
               )}
               )}
-              Save Currency Settings
+              Save Settings
             </Button>
             </Button>
           </div>
           </div>
         </CardContent>
         </CardContent>

+ 3 - 2
src/app/(authenticated)/vehicles/[id]/service-records-table.tsx

@@ -3,7 +3,7 @@
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useCallback, useState, useTransition } from "react";
 import { useCallback, useState, useTransition } from "react";
 import Link from "next/link";
 import Link from "next/link";
-import { format } from "date-fns";
+import { useFormatDate } from "@/lib/use-format-date";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
@@ -75,6 +75,7 @@ export function ServiceRecordsTable({
   currencyCode = "USD",
   currencyCode = "USD",
 }: ServiceRecordsTableProps) {
 }: ServiceRecordsTableProps) {
   const router = useRouter();
   const router = useRouter();
+  const { formatDate } = useFormatDate();
   const pathname = usePathname();
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
   const [isPending, startTransition] = useTransition();
@@ -205,7 +206,7 @@ export function ServiceRecordsTable({
                     }}
                     }}
                   >
                   >
                     <TableCell className="font-mono text-xs">
                     <TableCell className="font-mono text-xs">
-                      {format(new Date(record.serviceDate), "MM/dd/yyyy")}
+                      {formatDate(new Date(record.serviceDate))}
                     </TableCell>
                     </TableCell>
                     <TableCell className="max-w-0">
                     <TableCell className="max-w-0">
                       <div className="truncate">
                       <div className="truncate">

+ 4 - 3
src/app/(authenticated)/vehicles/[id]/service/[serviceId]/service-detail-client.tsx

@@ -3,7 +3,7 @@
 import { useState, useEffect, useRef } from "react";
 import { useState, useEffect, useRef } from "react";
 import Link from "next/link";
 import Link from "next/link";
 import { useRouter } from "next/navigation";
 import { useRouter } from "next/navigation";
-import { format } from "date-fns";
+import { useFormatDate } from "@/lib/use-format-date";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
@@ -165,6 +165,7 @@ export function ServiceDetailClient({
   const cs = getCurrencySymbol(currencyCode);
   const cs = getCurrencySymbol(currencyCode);
   const distUnit = unitSystem === "metric" ? "km" : "mi";
   const distUnit = unitSystem === "metric" ? "km" : "mi";
   const router = useRouter();
   const router = useRouter();
+  const { formatDate } = useFormatDate();
   const modal = useGlassModal();
   const modal = useGlassModal();
   const confirm = useConfirm();
   const confirm = useConfirm();
   const [downloading, setDownloading] = useState(false);
   const [downloading, setDownloading] = useState(false);
@@ -371,7 +372,7 @@ export function ServiceDetailClient({
                 <h1 className="text-lg font-semibold leading-tight">{record.title}</h1>
                 <h1 className="text-lg font-semibold leading-tight">{record.title}</h1>
               </div>
               </div>
               <p className="text-xs text-muted-foreground">
               <p className="text-xs text-muted-foreground">
-                {format(new Date(record.serviceDate), "MMMM d, yyyy")}
+                {formatDate(new Date(record.serviceDate))}
                 {record.shopName && ` · ${record.shopName}`}
                 {record.shopName && ` · ${record.shopName}`}
                 {record.techName && ` · Tech: ${record.techName}`}
                 {record.techName && ` · Tech: ${record.techName}`}
               </p>
               </p>
@@ -976,7 +977,7 @@ export function ServiceDetailClient({
                 <tbody className="divide-y">
                 <tbody className="divide-y">
                   {record.payments.map((payment) => (
                   {record.payments.map((payment) => (
                     <tr key={payment.id}>
                     <tr key={payment.id}>
-                      <td className="py-2">{format(new Date(payment.date), "MMM d, yyyy")}</td>
+                      <td className="py-2">{formatDate(new Date(payment.date))}</td>
                       <td className="py-2 text-right font-medium">{formatCurrency(payment.amount, currencyCode)}</td>
                       <td className="py-2 text-right font-medium">{formatCurrency(payment.amount, currencyCode)}</td>
                       <td className="py-2">
                       <td className="py-2">
                         <Badge variant="outline" className="text-xs capitalize">
                         <Badge variant="outline" className="text-xs capitalize">

+ 5 - 4
src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx

@@ -3,7 +3,7 @@
 import { useState } from 'react'
 import { useState } from 'react'
 import { useRouter } from 'next/navigation'
 import { useRouter } from 'next/navigation'
 import Link from 'next/link'
 import Link from 'next/link'
-import { format } from 'date-fns'
+import { useFormatDate } from '@/lib/use-format-date'
 import { Card, CardContent } from '@/components/ui/card'
 import { Card, CardContent } from '@/components/ui/card'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { Badge } from '@/components/ui/badge'
 import { Badge } from '@/components/ui/badge'
@@ -148,6 +148,7 @@ export function VehicleDetailClient({
 }) {
 }) {
   const distUnit = unitSystem === 'metric' ? 'km' : 'mi'
   const distUnit = unitSystem === 'metric' ? 'km' : 'mi'
   const router = useRouter()
   const router = useRouter()
+  const { formatDate } = useFormatDate()
   const modal = useGlassModal()
   const modal = useGlassModal()
   const [showEditForm, setShowEditForm] = useState(false)
   const [showEditForm, setShowEditForm] = useState(false)
   const [showNoteForm, setShowNoteForm] = useState(false)
   const [showNoteForm, setShowNoteForm] = useState(false)
@@ -332,7 +333,7 @@ export function VehicleDetailClient({
           {vehicle.purchaseDate && (
           {vehicle.purchaseDate && (
             <div className="flex items-center gap-1.5 text-muted-foreground">
             <div className="flex items-center gap-1.5 text-muted-foreground">
               <span className="text-xs">
               <span className="text-xs">
-                Purchased {format(new Date(vehicle.purchaseDate), 'MMM yyyy')}
+                Purchased {formatDate(new Date(vehicle.purchaseDate))}
               </span>
               </span>
             </div>
             </div>
           )}
           )}
@@ -414,7 +415,7 @@ export function VehicleDetailClient({
                         <p className="truncate text-sm text-muted-foreground">{n.content}</p>
                         <p className="truncate text-sm text-muted-foreground">{n.content}</p>
                       </TableCell>
                       </TableCell>
                       <TableCell className="font-mono text-xs text-muted-foreground">
                       <TableCell className="font-mono text-xs text-muted-foreground">
-                        {format(new Date(n.createdAt), 'MMM d, yyyy')}
+                        {formatDate(new Date(n.createdAt))}
                       </TableCell>
                       </TableCell>
                       <TableCell className="px-2">
                       <TableCell className="px-2">
                         <DropdownMenu>
                         <DropdownMenu>
@@ -541,7 +542,7 @@ export function VehicleDetailClient({
                             )}
                             )}
                           </div>
                           </div>
                           <p className="text-xs text-muted-foreground">
                           <p className="text-xs text-muted-foreground">
-                            {r.dueDate && `Due ${format(new Date(r.dueDate), 'MMM d, yyyy')}`}
+                            {r.dueDate && `Due ${formatDate(new Date(r.dueDate))}`}
                             {r.dueMileage &&
                             {r.dueMileage &&
                               `${r.dueDate ? ' · ' : 'Due at '}${r.dueMileage.toLocaleString()} ${distUnit}`}
                               `${r.dueDate ? ' · ' : 'Due at '}${r.dueMileage.toLocaleString()} ${distUnit}`}
                           </p>
                           </p>

+ 3 - 2
src/app/(authenticated)/work-orders/work-orders-client.tsx

@@ -3,7 +3,7 @@
 import { useState, useCallback, useTransition } from "react";
 import { useState, useCallback, useTransition } from "react";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { useRouter, usePathname, useSearchParams } from "next/navigation";
 import { toast } from "sonner";
 import { toast } from "sonner";
-import { format } from "date-fns";
+import { useFormatDate } from "@/lib/use-format-date";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Input } from "@/components/ui/input";
@@ -124,6 +124,7 @@ export function WorkOrdersClient({
   statusFilter: string;
   statusFilter: string;
 }) {
 }) {
   const router = useRouter();
   const router = useRouter();
+  const { formatDate } = useFormatDate();
   const pathname = usePathname();
   const pathname = usePathname();
   const searchParams = useSearchParams();
   const searchParams = useSearchParams();
   const [isPending, startTransition] = useTransition();
   const [isPending, startTransition] = useTransition();
@@ -276,7 +277,7 @@ export function WorkOrdersClient({
                       {r.techName || "-"}
                       {r.techName || "-"}
                     </TableCell>
                     </TableCell>
                     <TableCell className="font-mono text-xs">
                     <TableCell className="font-mono text-xs">
-                      {format(new Date(r.serviceDate), "MM/dd/yy")}
+                      {formatDate(new Date(r.serviceDate))}
                     </TableCell>
                     </TableCell>
                     <TableCell className="text-right font-semibold">
                     <TableCell className="text-right font-semibold">
                       {formatCurrency(displayTotal, currencyCode)}
                       {formatCurrency(displayTotal, currencyCode)}

+ 15 - 13
src/app/(public)/share/invoice/[orgId]/[token]/invoice-view.tsx

@@ -13,7 +13,7 @@ import {
   Paperclip,
   Paperclip,
   X,
   X,
 } from 'lucide-react'
 } from 'lucide-react'
-import { formatCurrency } from '@/lib/format'
+import { formatCurrency, formatDate as fmtDate, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
 
 
 interface InvoiceRecord {
 interface InvoiceRecord {
   id: string
   id: string
@@ -102,6 +102,8 @@ export function InvoiceView({
   showLogo = true,
   showLogo = true,
   showCompanyName = true,
   showCompanyName = true,
   showTorqvoiceBranding,
   showTorqvoiceBranding,
+  dateFormat,
+  timezone,
 }: {
 }: {
   record: InvoiceRecord
   record: InvoiceRecord
   workshop: { name: string; address: string; phone: string; email: string }
   workshop: { name: string; address: string; phone: string; email: string }
@@ -114,6 +116,8 @@ export function InvoiceView({
   showLogo?: boolean
   showLogo?: boolean
   showCompanyName?: boolean
   showCompanyName?: boolean
   showTorqvoiceBranding?: boolean
   showTorqvoiceBranding?: boolean
+  dateFormat?: string
+  timezone?: string
 }) {
 }) {
   const [carouselIndex, setCarouselIndex] = useState<number | null>(null)
   const [carouselIndex, setCarouselIndex] = useState<number | null>(null)
   const [paymentAmount, setPaymentAmount] = useState('')
   const [paymentAmount, setPaymentAmount] = useState('')
@@ -126,11 +130,9 @@ export function InvoiceView({
   const vehicleName = `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`
   const vehicleName = `${record.vehicle.year} ${record.vehicle.make} ${record.vehicle.model}`
   const displayTotal = record.totalAmount > 0 ? record.totalAmount : record.cost
   const displayTotal = record.totalAmount > 0 ? record.totalAmount : record.cost
   const invoiceNum = record.invoiceNumber || `INV-${record.id.slice(-8).toUpperCase()}`
   const invoiceNum = record.invoiceNumber || `INV-${record.id.slice(-8).toUpperCase()}`
-  const serviceDate = new Date(record.serviceDate).toLocaleDateString('en-US', {
-    year: 'numeric',
-    month: 'long',
-    day: 'numeric',
-  })
+  const df = dateFormat || DEFAULT_DATE_FORMAT
+  const tz = timezone || undefined
+  const serviceDate = fmtDate(record.serviceDate, df, tz)
   const totalPaid = record.payments.reduce((sum, p) => sum + p.amount, 0)
   const totalPaid = record.payments.reduce((sum, p) => sum + p.amount, 0)
   const balanceDue = displayTotal - totalPaid
   const balanceDue = displayTotal - totalPaid
   const shopName = workshop.name || record.shopName || 'Torqvoice'
   const shopName = workshop.name || record.shopName || 'Torqvoice'
@@ -664,13 +666,13 @@ export function InvoiceView({
                   <div>
                   <div>
                     <p className="text-xs text-gray-500 dark:text-gray-400">Due Date</p>
                     <p className="text-xs text-gray-500 dark:text-gray-400">Due Date</p>
                     <p className="font-medium">
                     <p className="font-medium">
-                      {new Date(
-                        new Date(record.serviceDate).getTime() + invoiceSettings.dueDays * 86400000
-                      ).toLocaleDateString('en-US', {
-                        year: 'numeric',
-                        month: 'long',
-                        day: 'numeric',
-                      })}
+                      {fmtDate(
+                        new Date(
+                          new Date(record.serviceDate).getTime() + invoiceSettings.dueDays * 86400000
+                        ),
+                        df,
+                        tz,
+                      )}
                     </p>
                     </p>
                   </div>
                   </div>
                 )}
                 )}

+ 4 - 0
src/app/(public)/share/invoice/[orgId]/[token]/page.tsx

@@ -82,6 +82,8 @@ export default async function PublicInvoicePage({
             "invoice.showLogo",
             "invoice.showLogo",
             "invoice.showCompanyName",
             "invoice.showCompanyName",
             "payment.providersEnabled",
             "payment.providersEnabled",
+            "workshop.dateFormat",
+            "workshop.timezone",
           ],
           ],
         },
         },
       },
       },
@@ -154,6 +156,8 @@ export default async function PublicInvoicePage({
       showLogo={showLogo}
       showLogo={showLogo}
       showCompanyName={showCompanyName}
       showCompanyName={showCompanyName}
       showTorqvoiceBranding={!features.brandingRemoved}
       showTorqvoiceBranding={!features.brandingRemoved}
+      dateFormat={settingsMap["workshop.dateFormat"] || undefined}
+      timezone={settingsMap["workshop.timezone"] || undefined}
     />
     />
   );
   );
 }
 }

+ 2 - 0
src/app/api/quotes/[id]/pdf/route.ts

@@ -78,6 +78,8 @@ export async function GET(
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
       logoDataUri,
       logoDataUri,
       torqvoiceLogoDataUri,
       torqvoiceLogoDataUri,
+      dateFormat: settingsMap["workshop.dateFormat"] || undefined,
+      timezone: settingsMap["workshop.timezone"] || undefined,
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
       // eslint-disable-next-line @typescript-eslint/no-explicit-any
     }) as any;
     }) as any;
     const buffer = await renderToBuffer(element);
     const buffer = await renderToBuffer(element);

+ 7 - 5
src/app/api/services/[id]/pdf/route.ts

@@ -9,6 +9,7 @@ import { PDFDocument } from "pdf-lib";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
+import { formatDateForPdf } from "@/lib/format";
 
 
 export async function GET(
 export async function GET(
   _request: Request,
   _request: Request,
@@ -138,19 +139,20 @@ export async function GET(
       dueDays: Number(settingsMap["invoice.dueDays"]) || 0,
       dueDays: Number(settingsMap["invoice.dueDays"]) || 0,
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
       unitSystem: settingsMap["workshop.unitSystem"] || "imperial",
       unitSystem: settingsMap["workshop.unitSystem"] || "imperial",
+      dateFormat: settingsMap["workshop.dateFormat"] || undefined,
+      timezone: settingsMap["workshop.timezone"] || undefined,
     };
     };
 
 
     // Build payment summary
     // Build payment summary
+    const pdfDateFormat = settingsMap["workshop.dateFormat"] || undefined;
+    const pdfTimezone = settingsMap["workshop.timezone"] || undefined;
+
     const paymentSummary = record.payments && record.payments.length > 0
     const paymentSummary = record.payments && record.payments.length > 0
       ? {
       ? {
           totalPaid: record.payments.reduce((sum: number, p: { amount: number }) => sum + p.amount, 0),
           totalPaid: record.payments.reduce((sum: number, p: { amount: number }) => sum + p.amount, 0),
           payments: record.payments.map((p: { amount: number; date: Date; method: string }) => ({
           payments: record.payments.map((p: { amount: number; date: Date; method: string }) => ({
             amount: p.amount,
             amount: p.amount,
-            date: new Date(p.date).toLocaleDateString("en-US", {
-              year: "numeric",
-              month: "short",
-              day: "numeric",
-            }),
+            date: formatDateForPdf(p.date, pdfDateFormat, pdfTimezone),
             method: p.method,
             method: p.method,
           })),
           })),
         }
         }

+ 7 - 1
src/app/api/share/invoice/[orgId]/[token]/pdf/route.ts

@@ -8,6 +8,7 @@ import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
 import { rateLimit } from "@/lib/rate-limit";
 import { rateLimit } from "@/lib/rate-limit";
+import { formatDateForPdf } from "@/lib/format";
 
 
 export async function GET(
 export async function GET(
   _request: Request,
   _request: Request,
@@ -94,14 +95,19 @@ export async function GET(
       dueDays: Number(settingsMap["invoice.dueDays"]) || 0,
       dueDays: Number(settingsMap["invoice.dueDays"]) || 0,
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
       currencyCode: settingsMap["workshop.currencyCode"] || "USD",
       unitSystem: settingsMap["workshop.unitSystem"] || "imperial",
       unitSystem: settingsMap["workshop.unitSystem"] || "imperial",
+      dateFormat: settingsMap["workshop.dateFormat"] || undefined,
+      timezone: settingsMap["workshop.timezone"] || undefined,
     };
     };
 
 
+    const pdfDateFormat = settingsMap["workshop.dateFormat"] || undefined;
+    const pdfTimezone = settingsMap["workshop.timezone"] || undefined;
+
     const paymentSummary = record.payments.length > 0
     const paymentSummary = record.payments.length > 0
       ? {
       ? {
           totalPaid: record.payments.reduce((sum: number, p: { amount: number }) => sum + p.amount, 0),
           totalPaid: record.payments.reduce((sum: number, p: { amount: number }) => sum + p.amount, 0),
           payments: record.payments.map((p: { amount: number; date: Date; method: string }) => ({
           payments: record.payments.map((p: { amount: number; date: Date; method: string }) => ({
             amount: p.amount,
             amount: p.amount,
-            date: new Date(p.date).toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" }),
+            date: formatDateForPdf(p.date, pdfDateFormat, pdfTimezone),
             method: p.method,
             method: p.method,
           })),
           })),
         }
         }

+ 46 - 0
src/components/date-settings-context.tsx

@@ -0,0 +1,46 @@
+"use client";
+
+import { createContext, useContext } from "react";
+import { DEFAULT_DATE_FORMAT, DEFAULT_TIME_FORMAT } from "@/lib/format";
+
+interface DateSettings {
+  dateFormat: string;
+  timeFormat: "12h" | "24h";
+  timezone: string;
+}
+
+const defaultSettings: DateSettings = {
+  dateFormat: DEFAULT_DATE_FORMAT,
+  timeFormat: DEFAULT_TIME_FORMAT,
+  timezone: "",
+};
+
+const DateSettingsContext = createContext<DateSettings>(defaultSettings);
+
+export function DateSettingsProvider({
+  dateFormat,
+  timeFormat,
+  timezone,
+  children,
+}: {
+  dateFormat?: string;
+  timeFormat?: string;
+  timezone?: string;
+  children: React.ReactNode;
+}) {
+  const value: DateSettings = {
+    dateFormat: dateFormat || DEFAULT_DATE_FORMAT,
+    timeFormat: (timeFormat === "24h" ? "24h" : "12h"),
+    timezone: timezone || "",
+  };
+
+  return (
+    <DateSettingsContext.Provider value={value}>
+      {children}
+    </DateSettingsContext.Provider>
+  );
+}
+
+export function useDateSettings() {
+  return useContext(DateSettingsContext);
+}

+ 3 - 2
src/features/billing/Components/RecurringInvoicesClient.tsx

@@ -3,7 +3,7 @@
 import { useState, useTransition } from "react";
 import { useState, useTransition } from "react";
 import { useRouter } from "next/navigation";
 import { useRouter } from "next/navigation";
 import Link from "next/link";
 import Link from "next/link";
-import { format } from "date-fns";
+import { useFormatDate } from "@/lib/use-format-date";
 import { Button } from "@/components/ui/button";
 import { Button } from "@/components/ui/button";
 import { Badge } from "@/components/ui/badge";
 import { Badge } from "@/components/ui/badge";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
@@ -138,6 +138,7 @@ export default function RecurringInvoicesClient({
   currencyCode,
   currencyCode,
 }: RecurringInvoicesClientProps) {
 }: RecurringInvoicesClientProps) {
   const router = useRouter();
   const router = useRouter();
+  const { formatDate } = useFormatDate();
   const [isPending, startTransition] = useTransition();
   const [isPending, startTransition] = useTransition();
   const [showCreate, setShowCreate] = useState(false);
   const [showCreate, setShowCreate] = useState(false);
 
 
@@ -341,7 +342,7 @@ export default function RecurringInvoicesClient({
                       <TableCell className="text-sm">{inv.vehicle.customer?.name ?? "-"}</TableCell>
                       <TableCell className="text-sm">{inv.vehicle.customer?.name ?? "-"}</TableCell>
                       <TableCell className="text-sm">{FREQUENCY_LABELS[inv.frequency] ?? inv.frequency}</TableCell>
                       <TableCell className="text-sm">{FREQUENCY_LABELS[inv.frequency] ?? inv.frequency}</TableCell>
                       <TableCell className="text-sm">
                       <TableCell className="text-sm">
-                        {format(new Date(inv.nextRunDate), "MMM d, yyyy")}
+                        {formatDate(new Date(inv.nextRunDate))}
                       </TableCell>
                       </TableCell>
                       <TableCell className="text-right text-sm">{fmt(total)}</TableCell>
                       <TableCell className="text-right text-sm">{fmt(total)}</TableCell>
                       <TableCell>
                       <TableCell>

+ 9 - 11
src/features/quotes/Components/QuotePDF.tsx

@@ -1,5 +1,5 @@
 import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer'
 import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer'
-import { formatCurrency, getCurrencySymbol } from '@/lib/format'
+import { formatCurrency, getCurrencySymbol, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
 
 
 const amber = '#d97706'
 const amber = '#d97706'
 const amberLight = '#fef3c7'
 const amberLight = '#fef3c7'
@@ -141,26 +141,24 @@ export function QuotePDF({
   currencyCode = 'USD',
   currencyCode = 'USD',
   logoDataUri,
   logoDataUri,
   torqvoiceLogoDataUri,
   torqvoiceLogoDataUri,
+  dateFormat,
+  timezone,
 }: {
 }: {
   data: QuoteData
   data: QuoteData
   workshop?: WorkshopInfo
   workshop?: WorkshopInfo
   currencyCode?: string
   currencyCode?: string
   logoDataUri?: string
   logoDataUri?: string
   torqvoiceLogoDataUri?: string
   torqvoiceLogoDataUri?: string
+  dateFormat?: string
+  timezone?: string
 }) {
 }) {
   const cs = getCurrencySymbol(currencyCode)
   const cs = getCurrencySymbol(currencyCode)
   const quoteNum = data.quoteNumber || 'QUOTE'
   const quoteNum = data.quoteNumber || 'QUOTE'
-  const createdDate = new Date(data.createdAt).toLocaleDateString('en-US', {
-    year: 'numeric',
-    month: 'long',
-    day: 'numeric',
-  })
+  const df = dateFormat || DEFAULT_DATE_FORMAT
+  const tz = timezone || undefined
+  const createdDate = formatDateForPdf(data.createdAt, df, tz)
   const validDate = data.validUntil
   const validDate = data.validUntil
-    ? new Date(data.validUntil).toLocaleDateString('en-US', {
-        year: 'numeric',
-        month: 'long',
-        day: 'numeric',
-      })
+    ? formatDateForPdf(data.validUntil, df, tz)
     : null
     : null
   const shopName = workshop?.name || 'Torqvoice'
   const shopName = workshop?.name || 'Torqvoice'
 
 

+ 3 - 0
src/features/settings/Schema/settingsSchema.ts

@@ -45,6 +45,9 @@ export const SETTING_KEYS = {
   LICENSE_VALID: "license.valid",
   LICENSE_VALID: "license.valid",
   LICENSE_CHECKED_AT: "license.checkedAt",
   LICENSE_CHECKED_AT: "license.checkedAt",
   LICENSE_PLAN: "license.plan",
   LICENSE_PLAN: "license.plan",
+  DATE_FORMAT: "workshop.dateFormat",
+  TIME_FORMAT: "workshop.timeFormat",
+  TIMEZONE: "workshop.timezone",
 } as const;
 } as const;
 
 
 export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];
 export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];

+ 10 - 13
src/features/vehicles/Components/InvoicePDF.tsx

@@ -1,5 +1,5 @@
 import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer'
 import { Document, Page, Text, View, StyleSheet, Image } from '@react-pdf/renderer'
-import { formatCurrency, getCurrencySymbol } from '@/lib/format'
+import { formatCurrency, getCurrencySymbol, formatDateForPdf, DEFAULT_DATE_FORMAT } from '@/lib/format'
 
 
 interface TemplateConfig {
 interface TemplateConfig {
   primaryColor?: string
   primaryColor?: string
@@ -191,6 +191,8 @@ interface InvoiceSettingsProps {
   dueDays?: number
   dueDays?: number
   currencyCode?: string
   currencyCode?: string
   unitSystem?: string
   unitSystem?: string
+  dateFormat?: string
+  timezone?: string
 }
 }
 
 
 interface PaymentSummary {
 interface PaymentSummary {
@@ -245,22 +247,17 @@ export function InvoicePDF({
   const laborSubtotal = data.laborItems.reduce((sum, l) => sum + l.total, 0)
   const laborSubtotal = data.laborItems.reduce((sum, l) => sum + l.total, 0)
   const displayTotal = data.totalAmount > 0 ? data.totalAmount : data.cost
   const displayTotal = data.totalAmount > 0 ? data.totalAmount : data.cost
   const invoiceNum = data.invoiceNumber || `INV-${data.id.slice(-8).toUpperCase()}`
   const invoiceNum = data.invoiceNumber || `INV-${data.id.slice(-8).toUpperCase()}`
-  const serviceDate = new Date(data.serviceDate).toLocaleDateString('en-US', {
-    year: 'numeric',
-    month: 'long',
-    day: 'numeric',
-  })
+  const df = invoiceSettings?.dateFormat || DEFAULT_DATE_FORMAT
+  const tz = invoiceSettings?.timezone || undefined
+  const serviceDate = formatDateForPdf(data.serviceDate, df, tz)
 
 
   const dueDays = invoiceSettings?.dueDays || 0
   const dueDays = invoiceSettings?.dueDays || 0
   const dueDate =
   const dueDate =
     dueDays > 0
     dueDays > 0
-      ? new Date(new Date(data.serviceDate).getTime() + dueDays * 86400000).toLocaleDateString(
-          'en-US',
-          {
-            year: 'numeric',
-            month: 'long',
-            day: 'numeric',
-          }
+      ? formatDateForPdf(
+          new Date(new Date(data.serviceDate).getTime() + dueDays * 86400000),
+          df,
+          tz,
         )
         )
       : null
       : null
 
 

+ 1 - 2
src/features/vehicles/Components/ReminderForm.tsx

@@ -16,7 +16,6 @@ import { useGlassModal } from "@/components/glass-modal";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { createReminder, updateReminder } from "../Actions/reminderActions";
 import { createReminder, updateReminder } from "../Actions/reminderActions";
 import { Loader2 } from "lucide-react";
 import { Loader2 } from "lucide-react";
-import { format } from "date-fns";
 
 
 interface ReminderData {
 interface ReminderData {
   id: string;
   id: string;
@@ -48,7 +47,7 @@ export function ReminderForm({ vehicleId, open, onOpenChange, reminder }: Remind
     if (open && reminder) {
     if (open && reminder) {
       setTitle(reminder.title);
       setTitle(reminder.title);
       setDescription(reminder.description || "");
       setDescription(reminder.description || "");
-      setDueDate(reminder.dueDate ? format(new Date(reminder.dueDate), "yyyy-MM-dd") : "");
+      setDueDate(reminder.dueDate ? new Date(reminder.dueDate).toISOString().split("T")[0] : "");
       setDueMileage(reminder.dueMileage ? String(reminder.dueMileage) : "");
       setDueMileage(reminder.dueMileage ? String(reminder.dueMileage) : "");
     } else if (open) {
     } else if (open) {
       setTitle("");
       setTitle("");

+ 140 - 0
src/lib/format.ts

@@ -91,3 +91,143 @@ export function getCurrencySymbol(currencyCode: string = "USD"): string {
     return currencyCode;
     return currencyCode;
   }
   }
 }
 }
+
+/**
+ * Convert a date to a specific timezone by shifting its local representation.
+ * Uses Intl.DateTimeFormat to get the date parts in the target timezone,
+ * then reconstructs a Date object whose local time matches those parts.
+ */
+function toTimezone(date: Date, timezone: string): Date {
+  try {
+    const parts = new Intl.DateTimeFormat("en-US", {
+      timeZone: timezone,
+      year: "numeric",
+      month: "2-digit",
+      day: "2-digit",
+      hour: "2-digit",
+      minute: "2-digit",
+      second: "2-digit",
+      hour12: false,
+    }).formatToParts(date);
+
+    const get = (type: Intl.DateTimeFormatPartTypes) =>
+      Number(parts.find((p) => p.type === type)?.value || 0);
+
+    return new Date(
+      get("year"),
+      get("month") - 1,
+      get("day"),
+      get("hour") === 24 ? 0 : get("hour"),
+      get("minute"),
+      get("second"),
+    );
+  } catch {
+    return date;
+  }
+}
+
+/**
+ * Default date format matching the most common existing pattern.
+ */
+export const DEFAULT_DATE_FORMAT = "MMM d, yyyy";
+export const DEFAULT_TIME_FORMAT = "12h" as const;
+
+/**
+ * Simple date-fns-style format function that handles common tokens.
+ * Tokens: yyyy, yy, MMMM, MMM, MM, dd, d, HH, hh, mm, ss, a
+ */
+function formatWithPattern(date: Date, pattern: string): string {
+  const months = [
+    "January", "February", "March", "April", "May", "June",
+    "July", "August", "September", "October", "November", "December",
+  ];
+  const monthsShort = [
+    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+  ];
+
+  const y = date.getFullYear();
+  const M = date.getMonth();
+  const d = date.getDate();
+  const H = date.getHours();
+  const m = date.getMinutes();
+  const s = date.getSeconds();
+  const h12 = H % 12 || 12;
+  const ampm = H < 12 ? "AM" : "PM";
+
+  return pattern
+    .replace("yyyy", String(y))
+    .replace("yy", String(y).slice(-2))
+    .replace("MMMM", months[M])
+    .replace("MMM", monthsShort[M])
+    .replace("MM", String(M + 1).padStart(2, "0"))
+    .replace(/\bdd\b/, String(d).padStart(2, "0"))
+    .replace(/\bd\b/, String(d))
+    .replace("HH", String(H).padStart(2, "0"))
+    .replace("hh", String(h12).padStart(2, "0"))
+    .replace("mm", String(m).padStart(2, "0"))
+    .replace("ss", String(s).padStart(2, "0"))
+    .replace("a", ampm);
+}
+
+/**
+ * Format a date using the org date format and optional timezone.
+ */
+export function formatDate(
+  date: Date | string,
+  dateFormat: string = DEFAULT_DATE_FORMAT,
+  timezone?: string,
+): string {
+  let d = typeof date === "string" ? new Date(date) : date;
+  if (timezone) d = toTimezone(d, timezone);
+  return formatWithPattern(d, dateFormat);
+}
+
+/**
+ * Format a date and time using org settings.
+ */
+export function formatDateTime(
+  date: Date | string,
+  dateFormat: string = DEFAULT_DATE_FORMAT,
+  timeFormat: "12h" | "24h" = "12h",
+  timezone?: string,
+): string {
+  let d = typeof date === "string" ? new Date(date) : date;
+  if (timezone) d = toTimezone(d, timezone);
+  const datePart = formatWithPattern(d, dateFormat);
+  const timePart = timeFormat === "24h"
+    ? formatWithPattern(d, "HH:mm")
+    : formatWithPattern(d, "hh:mm a");
+  return `${datePart} ${timePart}`;
+}
+
+/**
+ * Format time only using org settings.
+ */
+export function formatTime(
+  date: Date | string,
+  timeFormat: "12h" | "24h" = "12h",
+  timezone?: string,
+): string {
+  let d = typeof date === "string" ? new Date(date) : date;
+  if (timezone) d = toTimezone(d, timezone);
+  return timeFormat === "24h"
+    ? formatWithPattern(d, "HH:mm")
+    : formatWithPattern(d, "hh:mm a");
+}
+
+/**
+ * Format a date for server-side PDF/invoice rendering (uses toLocaleDateString for full month names).
+ * Applies timezone if provided, otherwise UTC for server contexts.
+ */
+export function formatDateForPdf(
+  date: Date | string,
+  dateFormat: string = DEFAULT_DATE_FORMAT,
+  timezone?: string,
+): string {
+  let d = typeof date === "string" ? new Date(date) : date;
+  if (timezone) {
+    d = toTimezone(d, timezone);
+  }
+  return formatWithPattern(d, dateFormat);
+}

+ 9 - 0
src/lib/get-layout-data.ts

@@ -12,6 +12,9 @@ type AuthResult =
       role: string;
       role: string;
       isSuperAdmin: boolean;
       isSuperAdmin: boolean;
       companyLogo: string | undefined;
       companyLogo: string | undefined;
+      dateFormat: string | undefined;
+      timeFormat: string | undefined;
+      timezone: string | undefined;
       organizations: { id: string; name: string; role: string }[];
       organizations: { id: string; name: string; role: string }[];
     };
     };
 
 
@@ -39,6 +42,9 @@ export async function getLayoutData(): Promise<AuthResult> {
             key: {
             key: {
               in: [
               in: [
                 SETTING_KEYS.COMPANY_LOGO,
                 SETTING_KEYS.COMPANY_LOGO,
+                SETTING_KEYS.DATE_FORMAT,
+                SETTING_KEYS.TIME_FORMAT,
+                SETTING_KEYS.TIMEZONE,
               ],
               ],
             },
             },
           },
           },
@@ -63,6 +69,9 @@ export async function getLayoutData(): Promise<AuthResult> {
     role: isSuperAdmin ? "super_admin" : (membership?.role ?? "member"),
     role: isSuperAdmin ? "super_admin" : (membership?.role ?? "member"),
     isSuperAdmin,
     isSuperAdmin,
     companyLogo: orgMap.get(SETTING_KEYS.COMPANY_LOGO) || undefined,
     companyLogo: orgMap.get(SETTING_KEYS.COMPANY_LOGO) || undefined,
+    dateFormat: orgMap.get(SETTING_KEYS.DATE_FORMAT) || undefined,
+    timeFormat: orgMap.get(SETTING_KEYS.TIME_FORMAT) || undefined,
+    timezone: orgMap.get(SETTING_KEYS.TIMEZONE) || undefined,
     organizations: memberships.map((m) => ({
     organizations: memberships.map((m) => ({
       id: m.organization.id,
       id: m.organization.id,
       name: m.organization.name,
       name: m.organization.name,

+ 20 - 0
src/lib/use-format-date.ts

@@ -0,0 +1,20 @@
+"use client";
+
+import { useDateSettings } from "@/components/date-settings-context";
+import {
+  formatDate as fmtDate,
+  formatDateTime as fmtDateTime,
+  formatTime as fmtTime,
+} from "@/lib/format";
+
+export function useFormatDate() {
+  const { dateFormat, timeFormat, timezone } = useDateSettings();
+  const tz = timezone || undefined;
+
+  return {
+    formatDate: (date: Date | string) => fmtDate(date, dateFormat, tz),
+    formatDateTime: (date: Date | string) =>
+      fmtDateTime(date, dateFormat, timeFormat, tz),
+    formatTime: (date: Date | string) => fmtTime(date, timeFormat, tz),
+  };
+}