Bernt Christian Egeland 7 месяцев назад
Родитель
Сommit
b032bcc341

+ 2 - 0
prisma/migrations/20260218071159_qoute_share_link/migration.sql

@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "quotes" ADD COLUMN     "publicToken" TEXT;

+ 1 - 0
prisma/schema.prisma

@@ -372,6 +372,7 @@ model Quote {
   discountAmount Float     @default(0)
   discountAmount Float     @default(0)
   totalAmount    Float     @default(0)
   totalAmount    Float     @default(0)
   notes          String?
   notes          String?
+  publicToken    String?
   convertedToId  String?
   convertedToId  String?
   createdAt      DateTime  @default(now())
   createdAt      DateTime  @default(now())
   updatedAt      DateTime  @updatedAt
   updatedAt      DateTime  @updatedAt

+ 0 - 91
src/app/(authenticated)/quotes/[id]/edit/page.tsx

@@ -1,91 +0,0 @@
-import { getQuote } from "@/features/quotes/Actions/quoteActions";
-import { getSettings } from "@/features/settings/Actions/settingsActions";
-import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
-import { getCustomers } from "@/features/customers/Actions/customerActions";
-import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
-import { PageHeader } from "@/components/page-header";
-import { QuoteForm } from "@/features/quotes/Components/QuoteForm";
-
-export default async function EditQuotePage({
-  params,
-}: {
-  params: Promise<{ id: string }>;
-}) {
-  const { id } = await params;
-  const [result, settingsResult, customersResult, vehiclesResult] = await Promise.all([
-    getQuote(id),
-    getSettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.DEFAULT_TAX_RATE, SETTING_KEYS.TAX_ENABLED, SETTING_KEYS.DEFAULT_LABOR_RATE]),
-    getCustomers(),
-    getVehicles(),
-  ]);
-
-  if (!result.success || !result.data) {
-    return (
-      <>
-        <PageHeader />
-        <div className="flex h-[50vh] items-center justify-center">
-          <p className="text-muted-foreground">{result.error || "Quote not found"}</p>
-        </div>
-      </>
-    );
-  }
-
-  const quote = result.data;
-  const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
-  const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
-  const taxEnabled = settings[SETTING_KEYS.TAX_ENABLED] !== "false";
-  const defaultTaxRate = taxEnabled ? (Number(settings[SETTING_KEYS.DEFAULT_TAX_RATE]) || 0) : 0;
-  const defaultLaborRate = Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0;
-  const customers = customersResult.success && customersResult.data ? customersResult.data : [];
-  const vehicles = vehiclesResult.success && vehiclesResult.data ? vehiclesResult.data : [];
-
-  return (
-    <>
-      <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
-        <QuoteForm
-          currencyCode={currencyCode}
-          defaultTaxRate={defaultTaxRate}
-          taxEnabled={taxEnabled}
-          defaultLaborRate={defaultLaborRate}
-          customers={customers.map((c: { id: string; name: string; email: string | null; company: string | null }) => ({
-            id: c.id, name: c.name, email: c.email, company: c.company,
-          }))}
-          vehicles={vehicles.map((v: { id: string; make: string; model: string; year: number; licensePlate: string | null }) => ({
-            id: v.id, make: v.make, model: v.model, year: v.year, licensePlate: v.licensePlate,
-          }))}
-          initialData={{
-            id: quote.id,
-            title: quote.title,
-            description: quote.description,
-            status: quote.status,
-            validUntil: quote.validUntil ? new Date(quote.validUntil).toISOString() : null,
-            customerId: quote.customer?.id || null,
-            vehicleId: quote.vehicle?.id || null,
-            notes: quote.notes,
-            partItems: quote.partItems.map((p) => ({
-              partNumber: p.partNumber || "",
-              name: p.name,
-              quantity: p.quantity,
-              unitPrice: p.unitPrice,
-              total: p.total,
-            })),
-            laborItems: quote.laborItems.map((l) => ({
-              description: l.description,
-              hours: l.hours,
-              rate: l.rate,
-              total: l.total,
-            })),
-            subtotal: quote.subtotal,
-            taxRate: quote.taxRate,
-            taxAmount: quote.taxAmount,
-            discountType: quote.discountType,
-            discountValue: quote.discountValue,
-            discountAmount: quote.discountAmount,
-            totalAmount: quote.totalAmount,
-          }}
-        />
-      </div>
-    </>
-  );
-}

+ 79 - 14
src/app/(authenticated)/quotes/[id]/page.tsx

@@ -1,9 +1,11 @@
 import { getQuote } from "@/features/quotes/Actions/quoteActions";
 import { getQuote } from "@/features/quotes/Actions/quoteActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
+import { getCustomers } from "@/features/customers/Actions/customerActions";
 import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
 import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
+import { getAuthContext } from "@/lib/get-auth-context";
 import { PageHeader } from "@/components/page-header";
 import { PageHeader } from "@/components/page-header";
-import { QuoteDetailClient } from "./quote-detail-client";
+import { QuotePageClient } from "@/features/quotes/Components/QuotePageClient";
 
 
 export default async function QuoteDetailPage({
 export default async function QuoteDetailPage({
   params,
   params,
@@ -11,35 +13,98 @@ export default async function QuoteDetailPage({
   params: Promise<{ id: string }>;
   params: Promise<{ id: string }>;
 }) {
 }) {
   const { id } = await params;
   const { id } = await params;
-  const [result, settingsResult, vehiclesResult] = await Promise.all([
-    getQuote(id),
-    getSettings([SETTING_KEYS.CURRENCY_CODE]),
-    getVehicles(),
-  ]);
+  const [result, settingsResult, customersResult, vehiclesResult, authContext] =
+    await Promise.all([
+      getQuote(id),
+      getSettings([
+        SETTING_KEYS.CURRENCY_CODE,
+        SETTING_KEYS.DEFAULT_TAX_RATE,
+        SETTING_KEYS.TAX_ENABLED,
+        SETTING_KEYS.DEFAULT_LABOR_RATE,
+      ]),
+      getCustomers(),
+      getVehicles(),
+      getAuthContext(),
+    ]);
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
     return (
     return (
       <>
       <>
         <PageHeader />
         <PageHeader />
         <div className="flex h-[50vh] items-center justify-center">
         <div className="flex h-[50vh] items-center justify-center">
-          <p className="text-muted-foreground">{result.error || "Quote not found"}</p>
+          <p className="text-muted-foreground">
+            {result.error || "Quote not found"}
+          </p>
         </div>
         </div>
       </>
       </>
     );
     );
   }
   }
 
 
-  const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
+  const settings =
+    settingsResult.success && settingsResult.data ? settingsResult.data : {};
   const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
   const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
-  const vehicles = vehiclesResult.success && vehiclesResult.data
-    ? vehiclesResult.data.map((v) => ({ id: v.id, make: v.make, model: v.model, year: v.year, licensePlate: v.licensePlate }))
-    : [];
+  const taxEnabled = settings[SETTING_KEYS.TAX_ENABLED] !== "false";
+  const defaultTaxRate = taxEnabled
+    ? Number(settings[SETTING_KEYS.DEFAULT_TAX_RATE]) || 0
+    : 0;
+  const defaultLaborRate =
+    Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0;
+  const customers =
+    customersResult.success && customersResult.data
+      ? customersResult.data
+      : [];
+  const vehicles =
+    vehiclesResult.success && vehiclesResult.data
+      ? vehiclesResult.data
+      : [];
+  const organizationId = authContext?.organizationId || "";
 
 
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
-        <QuoteDetailClient quote={result.data} currencyCode={currencyCode} vehicles={vehicles} />
-      </div>
+      <QuotePageClient
+        quote={result.data}
+        organizationId={organizationId}
+        currencyCode={currencyCode}
+        defaultTaxRate={defaultTaxRate}
+        taxEnabled={taxEnabled}
+        defaultLaborRate={defaultLaborRate}
+        customers={customers.map(
+          (c: {
+            id: string;
+            name: string;
+            email: string | null;
+            company: string | null;
+          }) => ({
+            id: c.id,
+            name: c.name,
+            email: c.email,
+            company: c.company,
+          })
+        )}
+        vehicles={vehicles.map(
+          (v: {
+            id: string;
+            make: string;
+            model: string;
+            year: number;
+            licensePlate: string | null;
+            customer: {
+              id: string;
+              name: string;
+              company: string | null;
+            } | null;
+          }) => ({
+            id: v.id,
+            make: v.make,
+            model: v.model,
+            year: v.year,
+            licensePlate: v.licensePlate,
+            customerId: v.customer?.id || null,
+            customerName: v.customer?.name || null,
+          })
+        )}
+      />
     </>
     </>
   );
   );
 }
 }

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

@@ -1,417 +0,0 @@
-"use client";
-
-import { useState } from "react";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { useFormatDate } from "@/lib/use-format-date";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
-import { Button } from "@/components/ui/button";
-import { Badge } from "@/components/ui/badge";
-import { Separator } from "@/components/ui/separator";
-import { useGlassModal } from "@/components/glass-modal";
-import { useConfirm } from "@/components/confirm-dialog";
-import { toast } from "sonner";
-import { deleteQuote, updateQuoteStatus, convertQuoteToServiceRecord } from "@/features/quotes/Actions/quoteActions";
-import { sendQuoteEmail } from "@/features/email/Actions/emailActions";
-import { SendEmailDialog } from "@/features/email/Components/SendEmailDialog";
-import {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from "@/components/ui/select";
-import {
-  Dialog,
-  DialogContent,
-  DialogHeader,
-  DialogTitle,
-} from "@/components/ui/dialog";
-import {
-  ArrowLeft,
-  ArrowRight,
-  Building2,
-  Car,
-  Download,
-  Loader2,
-  Mail,
-  MapPin,
-  Pencil,
-  Phone,
-  Trash2,
-  Users,
-  Wrench,
-} from "lucide-react";
-import { formatCurrency, getCurrencySymbol } from "@/lib/format";
-
-const statusColors: Record<string, string> = {
-  draft: "bg-gray-500/10 text-gray-500 border-gray-500/20",
-  sent: "bg-blue-500/10 text-blue-500 border-blue-500/20",
-  accepted: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20",
-  rejected: "bg-red-500/10 text-red-500 border-red-500/20",
-  expired: "bg-amber-500/10 text-amber-600 border-amber-500/20",
-  converted: "bg-purple-500/10 text-purple-500 border-purple-500/20",
-};
-
-interface QuoteDetail {
-  id: string;
-  quoteNumber: string | null;
-  title: string;
-  description: string | null;
-  status: string;
-  validUntil: Date | null;
-  subtotal: number;
-  taxRate: number;
-  taxAmount: number;
-  discountType: string | null;
-  discountValue: number;
-  discountAmount: number;
-  totalAmount: number;
-  notes: string | null;
-  convertedToId: string | null;
-  createdAt: Date;
-  partItems: { id: string; partNumber: string | null; name: string; quantity: number; unitPrice: number; total: number }[];
-  laborItems: { id: string; description: string; hours: number; rate: number; total: number }[];
-  customer: { id: string; name: string; email: string | null; phone: string | null; address: string | null; company: string | null } | null;
-  vehicle: { id: string; make: string; model: string; year: number; vin: string | null; licensePlate: string | null; mileage: number } | null;
-}
-
-interface VehicleOption {
-  id: string;
-  make: string;
-  model: string;
-  year: number;
-  licensePlate: string | null;
-}
-
-export function QuoteDetailClient({
-  quote,
-  currencyCode = "USD",
-  vehicles = [],
-}: {
-  quote: QuoteDetail;
-  currencyCode?: string;
-  vehicles?: VehicleOption[];
-}) {
-  const cs = getCurrencySymbol(currencyCode);
-  const router = useRouter();
-  const { formatDate } = useFormatDate();
-  const modal = useGlassModal();
-  const confirm = useConfirm();
-  const [downloading, setDownloading] = useState(false);
-  const [showEmailDialog, setShowEmailDialog] = useState(false);
-  const [showConvertDialog, setShowConvertDialog] = useState(false);
-  const [convertVehicleId, setConvertVehicleId] = useState(quote.vehicle?.id || "");
-  const [converting, setConverting] = useState(false);
-
-  const partsSubtotal = quote.partItems.reduce((sum, p) => sum + p.total, 0);
-  const laborSubtotal = quote.laborItems.reduce((sum, l) => sum + l.total, 0);
-
-  const handleDelete = async () => {
-    const ok = await confirm({ title: "Delete Quote", description: "This will permanently delete this quote.", confirmLabel: "Delete", destructive: true });
-    if (!ok) return;
-    const result = await deleteQuote(quote.id);
-    if (result.success) {
-      router.push("/quotes");
-      router.refresh();
-    } else {
-      modal.open("error", "Error", result.error || "Failed to delete");
-    }
-  };
-
-  const handleDownloadPDF = async () => {
-    setDownloading(true);
-    try {
-      const res = await fetch(`/api/quotes/${quote.id}/pdf`);
-      if (!res.ok) throw new Error("Failed to generate PDF");
-      const blob = await res.blob();
-      const url = URL.createObjectURL(blob);
-      const a = document.createElement("a");
-      a.href = url;
-      a.download = `quote-${quote.quoteNumber || quote.id}.pdf`;
-      a.click();
-      URL.revokeObjectURL(url);
-    } catch {
-      modal.open("error", "Error", "Failed to generate PDF");
-    }
-    setDownloading(false);
-  };
-
-  const handleStatusChange = async (newStatus: string) => {
-    await updateQuoteStatus(quote.id, newStatus);
-    toast.success("Quote status updated");
-    router.refresh();
-  };
-
-  const handleConvert = async () => {
-    if (!convertVehicleId) {
-      modal.open("error", "Error", "Please select a vehicle");
-      return;
-    }
-    setConverting(true);
-    const result = await convertQuoteToServiceRecord(quote.id, convertVehicleId);
-    if (result.success && result.data) {
-      router.push(`/vehicles/${convertVehicleId}/service/${result.data.id}`);
-      router.refresh();
-    } else {
-      modal.open("error", "Error", result.error || "Failed to convert");
-    }
-    setConverting(false);
-  };
-
-  return (
-    <div className="space-y-6">
-      <div>
-        <Link
-          href="/quotes"
-          className="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
-        >
-          <ArrowLeft className="h-4 w-4" />
-          Back to Quotes
-        </Link>
-        <div className="flex items-start justify-between">
-          <div>
-            <div className="flex items-center gap-3">
-              <Badge variant="outline" className={statusColors[quote.status] || ""}>{quote.status}</Badge>
-              <h1 className="text-2xl font-bold">{quote.title}</h1>
-            </div>
-            <p className="mt-1 text-sm text-muted-foreground">
-              {quote.quoteNumber} · Created {formatDate(new Date(quote.createdAt))}
-              {quote.validUntil && ` · Valid until ${formatDate(new Date(quote.validUntil))}`}
-            </p>
-          </div>
-          <div className="flex items-center gap-2">
-            {quote.status !== "converted" && (
-              <>
-                <Button variant="outline" size="sm" asChild>
-                  <Link href={`/quotes/${quote.id}/edit`}>
-                    <Pencil className="mr-1 h-3.5 w-3.5" /> Edit
-                  </Link>
-                </Button>
-                <Button variant="outline" size="sm" onClick={() => setShowConvertDialog(true)}>
-                  <ArrowRight className="mr-1 h-3.5 w-3.5" /> Convert to Work Order
-                </Button>
-              </>
-            )}
-            <Button variant="outline" size="sm" onClick={handleDownloadPDF} disabled={downloading}>
-              {downloading ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Download className="mr-1 h-3.5 w-3.5" />}
-              PDF
-            </Button>
-            <Button variant="outline" size="sm" onClick={() => setShowEmailDialog(true)}>
-              <Mail className="mr-1 h-3.5 w-3.5" />
-              Email
-            </Button>
-            <Button variant="outline" size="sm" className="text-destructive hover:bg-destructive/10" onClick={handleDelete}>
-              <Trash2 className="mr-1 h-3.5 w-3.5" /> Delete
-            </Button>
-          </div>
-        </div>
-      </div>
-
-      {/* Status Actions */}
-      {quote.status !== "converted" && (
-        <Card className="border-0 shadow-sm">
-          <CardContent className="flex items-center gap-3 pt-6">
-            <span className="text-sm text-muted-foreground">Update status:</span>
-            {["draft", "sent", "accepted", "rejected"].map((s) => (
-              <Button
-                key={s}
-                variant={quote.status === s ? "default" : "outline"}
-                size="sm"
-                onClick={() => handleStatusChange(s)}
-                className="capitalize"
-              >
-                {s}
-              </Button>
-            ))}
-          </CardContent>
-        </Card>
-      )}
-
-      {/* Customer & Vehicle */}
-      <div className="grid gap-4 sm:grid-cols-2">
-        {quote.customer && (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-2">
-              <CardTitle className="flex items-center gap-2 text-sm text-muted-foreground">
-                <Users className="h-4 w-4" /> Customer
-              </CardTitle>
-            </CardHeader>
-            <CardContent>
-              <Link href={`/customers/${quote.customer.id}`} className="font-semibold hover:underline">{quote.customer.name}</Link>
-              <div className="mt-1 space-y-0.5 text-sm text-muted-foreground">
-                {quote.customer.company && <div className="flex items-center gap-1.5"><Building2 className="h-3 w-3" />{quote.customer.company}</div>}
-                {quote.customer.email && <div className="flex items-center gap-1.5"><Mail className="h-3 w-3" />{quote.customer.email}</div>}
-                {quote.customer.phone && <div className="flex items-center gap-1.5"><Phone className="h-3 w-3" />{quote.customer.phone}</div>}
-                {quote.customer.address && <div className="flex items-center gap-1.5"><MapPin className="h-3 w-3" />{quote.customer.address}</div>}
-              </div>
-            </CardContent>
-          </Card>
-        )}
-        {quote.vehicle && (
-          <Card className="border-0 shadow-sm">
-            <CardHeader className="pb-2">
-              <CardTitle className="flex items-center gap-2 text-sm text-muted-foreground">
-                <Car className="h-4 w-4" /> Vehicle
-              </CardTitle>
-            </CardHeader>
-            <CardContent>
-              <p className="font-semibold">{quote.vehicle.year} {quote.vehicle.make} {quote.vehicle.model}</p>
-              {quote.vehicle.licensePlate && <p className="mt-0.5 font-mono text-xs text-muted-foreground">{quote.vehicle.licensePlate}</p>}
-              {quote.vehicle.vin && <p className="font-mono text-xs text-muted-foreground">VIN: {quote.vehicle.vin}</p>}
-            </CardContent>
-          </Card>
-        )}
-      </div>
-
-      {/* Description */}
-      {quote.description && (
-        <Card className="border-0 shadow-sm">
-          <CardHeader><CardTitle className="text-base">Description</CardTitle></CardHeader>
-          <CardContent><p className="whitespace-pre-wrap text-sm">{quote.description}</p></CardContent>
-        </Card>
-      )}
-
-      {/* Parts */}
-      {quote.partItems.length > 0 && (
-        <Card className="border-0 shadow-sm">
-          <CardHeader>
-            <CardTitle className="flex items-center gap-2 text-base">
-              <Wrench className="h-4 w-4" /> Parts ({quote.partItems.length})
-            </CardTitle>
-          </CardHeader>
-          <CardContent>
-            <div className="overflow-x-auto">
-              <table className="w-full text-sm">
-                <thead>
-                  <tr className="border-b text-left text-muted-foreground">
-                    <th className="pb-2 font-medium">Part #</th>
-                    <th className="pb-2 font-medium">Name</th>
-                    <th className="pb-2 text-right font-medium">Qty</th>
-                    <th className="pb-2 text-right font-medium">Unit Price</th>
-                    <th className="pb-2 text-right font-medium">Total</th>
-                  </tr>
-                </thead>
-                <tbody className="divide-y">
-                  {quote.partItems.map((p) => (
-                    <tr key={p.id}>
-                      <td className="py-2 font-mono text-xs">{p.partNumber || "-"}</td>
-                      <td className="py-2">{p.name}</td>
-                      <td className="py-2 text-right">{p.quantity}</td>
-                      <td className="py-2 text-right">{formatCurrency(p.unitPrice, currencyCode)}</td>
-                      <td className="py-2 text-right font-medium">{formatCurrency(p.total, currencyCode)}</td>
-                    </tr>
-                  ))}
-                </tbody>
-                <tfoot>
-                  <tr className="border-t"><td colSpan={4} className="pt-2 text-right font-medium">Parts Subtotal</td><td className="pt-2 text-right font-bold">{formatCurrency(partsSubtotal, currencyCode)}</td></tr>
-                </tfoot>
-              </table>
-            </div>
-          </CardContent>
-        </Card>
-      )}
-
-      {/* Labor */}
-      {quote.laborItems.length > 0 && (
-        <Card className="border-0 shadow-sm">
-          <CardHeader><CardTitle className="text-base">Labor ({quote.laborItems.length})</CardTitle></CardHeader>
-          <CardContent>
-            <div className="overflow-x-auto">
-              <table className="w-full text-sm">
-                <thead>
-                  <tr className="border-b text-left text-muted-foreground">
-                    <th className="pb-2 font-medium">Description</th>
-                    <th className="pb-2 text-right font-medium">Hours</th>
-                    <th className="pb-2 text-right font-medium">Rate</th>
-                    <th className="pb-2 text-right font-medium">Total</th>
-                  </tr>
-                </thead>
-                <tbody className="divide-y">
-                  {quote.laborItems.map((l) => (
-                    <tr key={l.id}>
-                      <td className="py-2">{l.description}</td>
-                      <td className="py-2 text-right">{l.hours}</td>
-                      <td className="py-2 text-right">{formatCurrency(l.rate, currencyCode)}/hr</td>
-                      <td className="py-2 text-right font-medium">{formatCurrency(l.total, currencyCode)}</td>
-                    </tr>
-                  ))}
-                </tbody>
-                <tfoot>
-                  <tr className="border-t"><td colSpan={3} className="pt-2 text-right font-medium">Labor Subtotal</td><td className="pt-2 text-right font-bold">{formatCurrency(laborSubtotal, currencyCode)}</td></tr>
-                </tfoot>
-              </table>
-            </div>
-          </CardContent>
-        </Card>
-      )}
-
-      {/* Totals */}
-      <Card className="border-0 shadow-sm">
-        <CardHeader><CardTitle className="text-base">Quote Summary</CardTitle></CardHeader>
-        <CardContent>
-          <div className="space-y-2">
-            {quote.subtotal > 0 && <div className="flex justify-between text-sm"><span className="text-muted-foreground">Subtotal</span><span>{formatCurrency(quote.subtotal, currencyCode)}</span></div>}
-            {quote.discountAmount > 0 && (
-              <div className="flex justify-between text-sm">
-                <span className="text-muted-foreground">Discount{quote.discountType === "percentage" ? ` (${quote.discountValue}%)` : ""}</span>
-                <span className="text-destructive">{formatCurrency(-quote.discountAmount, currencyCode)}</span>
-              </div>
-            )}
-            {quote.taxRate > 0 && <div className="flex justify-between text-sm"><span className="text-muted-foreground">Tax ({quote.taxRate}%)</span><span>{formatCurrency(quote.taxAmount, currencyCode)}</span></div>}
-            <Separator />
-            <div className="flex justify-between text-lg font-bold"><span>Total</span><span>{formatCurrency(quote.totalAmount, currencyCode)}</span></div>
-          </div>
-        </CardContent>
-      </Card>
-
-      {/* Notes */}
-      {quote.notes && (
-        <Card className="border-0 shadow-sm">
-          <CardHeader><CardTitle className="text-base">Internal Notes</CardTitle></CardHeader>
-          <CardContent><p className="whitespace-pre-wrap text-sm">{quote.notes}</p></CardContent>
-        </Card>
-      )}
-
-      {/* Email Quote Dialog */}
-      <SendEmailDialog
-        open={showEmailDialog}
-        onOpenChange={setShowEmailDialog}
-        defaultEmail={quote.customer?.email || ""}
-        entityLabel="Quote"
-        onSend={async (email, message) => {
-          return sendQuoteEmail({ quoteId: quote.id, recipientEmail: email, message });
-        }}
-      />
-
-      {/* Convert to Work Order Dialog */}
-      <Dialog open={showConvertDialog} onOpenChange={setShowConvertDialog}>
-        <DialogContent className="max-w-md">
-          <DialogHeader>
-            <DialogTitle>Convert Quote to Work Order</DialogTitle>
-          </DialogHeader>
-          <div className="space-y-4">
-            <p className="text-sm text-muted-foreground">
-              This will create a new service record (work order) from this quote. Select a vehicle for the work order.
-            </p>
-            <Select value={convertVehicleId} onValueChange={setConvertVehicleId}>
-              <SelectTrigger><SelectValue placeholder="Select vehicle..." /></SelectTrigger>
-              <SelectContent>
-                {vehicles.map((v) => (
-                  <SelectItem key={v.id} value={v.id}>{v.year} {v.make} {v.model}{v.licensePlate ? ` (${v.licensePlate})` : ""}</SelectItem>
-                ))}
-              </SelectContent>
-            </Select>
-            <div className="flex gap-2">
-              <Button onClick={handleConvert} disabled={converting || !convertVehicleId}>
-                {converting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-                Convert
-              </Button>
-              <Button variant="ghost" onClick={() => setShowConvertDialog(false)}>Cancel</Button>
-            </div>
-          </div>
-        </DialogContent>
-      </Dialog>
-    </div>
-  );
-}

+ 4 - 2
src/app/(authenticated)/quotes/new/page.tsx

@@ -24,7 +24,7 @@ export default async function NewQuotePage() {
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
-      <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
+      <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
         <QuoteForm
         <QuoteForm
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           defaultTaxRate={defaultTaxRate}
           defaultTaxRate={defaultTaxRate}
@@ -37,12 +37,14 @@ export default async function NewQuotePage() {
             email: c.email,
             email: c.email,
             company: c.company,
             company: c.company,
           }))}
           }))}
-          vehicles={vehicles.map((v: { id: string; make: string; model: string; year: number; licensePlate: string | null }) => ({
+          vehicles={vehicles.map((v: { id: string; make: string; model: string; year: number; licensePlate: string | null; customer: { id: string; name: string; company: string | null } | null }) => ({
             id: v.id,
             id: v.id,
             make: v.make,
             make: v.make,
             model: v.model,
             model: v.model,
             year: v.year,
             year: v.year,
             licensePlate: v.licensePlate,
             licensePlate: v.licensePlate,
+            customerId: v.customer?.id || null,
+            customerName: v.customer?.name || null,
           }))}
           }))}
         />
         />
       </div>
       </div>

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

@@ -143,13 +143,13 @@ export function QuotesClient({
         <Table>
         <Table>
           <TableHeader>
           <TableHeader>
             <TableRow>
             <TableRow>
-              <TableHead className="w-[100px]">Quote #</TableHead>
+              <TableHead className="w-25">Quote #</TableHead>
               <TableHead>Title</TableHead>
               <TableHead>Title</TableHead>
               <TableHead className="hidden md:table-cell">Customer</TableHead>
               <TableHead className="hidden md:table-cell">Customer</TableHead>
               <TableHead className="hidden lg:table-cell">Vehicle</TableHead>
               <TableHead className="hidden lg:table-cell">Vehicle</TableHead>
-              <TableHead className="w-[110px]">Status</TableHead>
-              <TableHead className="w-[90px]">Date</TableHead>
-              <TableHead className="w-[90px] text-right">Total</TableHead>
+              <TableHead className="w-27.5">Status</TableHead>
+              <TableHead className="w-22.5">Date</TableHead>
+              <TableHead className="w-22.5 text-right">Total</TableHead>
             </TableRow>
             </TableRow>
           </TableHeader>
           </TableHeader>
           <TableBody>
           <TableBody>

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

@@ -0,0 +1,108 @@
+import { db } from "@/lib/db";
+import { notFound } from "next/navigation";
+import { QuoteView } from "./quote-view";
+import { getFeatures } from "@/lib/features";
+import type { Metadata } from "next";
+
+export const revalidate = 60;
+
+export const metadata: Metadata = {
+  robots: { index: false, follow: false },
+};
+
+export default async function PublicQuotePage({
+  params,
+}: {
+  params: Promise<{ orgId: string; token: string }>;
+}) {
+  const { orgId, token } = await params;
+
+  const quote = await db.quote.findFirst({
+    where: { publicToken: token, organizationId: orgId },
+    include: {
+      partItems: true,
+      laborItems: true,
+      customer: {
+        select: {
+          name: true,
+          email: true,
+          phone: true,
+          address: true,
+          company: true,
+        },
+      },
+      vehicle: {
+        select: {
+          make: true,
+          model: true,
+          year: true,
+          vin: true,
+          licensePlate: true,
+        },
+      },
+    },
+  });
+
+  if (!quote) {
+    notFound();
+  }
+
+  const [settings, org, features] = await Promise.all([
+    db.appSetting.findMany({
+      where: {
+        organizationId: orgId,
+        key: {
+          in: [
+            "workshop.address",
+            "workshop.phone",
+            "workshop.email",
+            "workshop.logo",
+            "workshop.currencyCode",
+            "workshop.dateFormat",
+            "workshop.timezone",
+          ],
+        },
+      },
+    }),
+    db.organization.findUnique({
+      where: { id: orgId },
+      select: { name: true },
+    }),
+    getFeatures(orgId),
+  ]);
+
+  const settingsMap: Record<string, string> = {};
+  for (const s of settings) settingsMap[s.key] = s.value;
+
+  const workshop = {
+    name: org?.name || "",
+    address: settingsMap["workshop.address"] || "",
+    phone: settingsMap["workshop.phone"] || "",
+    email: settingsMap["workshop.email"] || "",
+  };
+
+  const currencyCode = settingsMap["workshop.currencyCode"] || "USD";
+
+  // Rewrite logo URL for public access
+  const rawLogoUrl = settingsMap["workshop.logo"] || "";
+  let logoUrl = "";
+  if (rawLogoUrl) {
+    const match = rawLogoUrl.match(/^\/api\/files\/[^/]+\/(.+)$/);
+    if (match) logoUrl = `/api/files/public/${token}/${match[1]}`;
+    else logoUrl = rawLogoUrl;
+  }
+
+  return (
+    <QuoteView
+      quote={quote}
+      workshop={workshop}
+      currencyCode={currencyCode}
+      orgId={orgId}
+      token={token}
+      logoUrl={logoUrl}
+      showTorqvoiceBranding={!features.brandingRemoved}
+      dateFormat={settingsMap["workshop.dateFormat"] || undefined}
+      timezone={settingsMap["workshop.timezone"] || undefined}
+    />
+  );
+}

+ 322 - 0
src/app/(public)/share/quote/[orgId]/[token]/quote-view.tsx

@@ -0,0 +1,322 @@
+"use client";
+
+import { Download, Loader2 } from "lucide-react";
+import { useState } from "react";
+import { formatCurrency, formatDate as fmtDate, DEFAULT_DATE_FORMAT } from "@/lib/format";
+import { sanitizeHtml } from "@/lib/sanitize-html";
+
+interface QuoteRecord {
+  id: string;
+  quoteNumber: string | null;
+  title: string;
+  description: string | null;
+  status: string;
+  validUntil: Date | null;
+  createdAt: Date;
+  subtotal: number;
+  taxRate: number;
+  taxAmount: number;
+  discountType: string | null;
+  discountValue: number;
+  discountAmount: number;
+  totalAmount: number;
+  notes: string | null;
+  partItems: {
+    partNumber: string | null;
+    name: string;
+    quantity: number;
+    unitPrice: number;
+    total: number;
+  }[];
+  laborItems: {
+    description: string;
+    hours: number;
+    rate: number;
+    total: number;
+  }[];
+  customer: {
+    name: string;
+    email: string | null;
+    phone: string | null;
+    address: string | null;
+    company: string | null;
+  } | null;
+  vehicle: {
+    make: string;
+    model: string;
+    year: number;
+    vin: string | null;
+    licensePlate: string | null;
+  } | null;
+}
+
+const statusLabels: Record<string, string> = {
+  draft: "Draft",
+  sent: "Sent",
+  accepted: "Accepted",
+  rejected: "Rejected",
+  expired: "Expired",
+  converted: "Converted",
+};
+
+export function QuoteView({
+  quote,
+  workshop,
+  currencyCode,
+  orgId,
+  token,
+  logoUrl,
+  showTorqvoiceBranding,
+  dateFormat,
+  timezone,
+}: {
+  quote: QuoteRecord;
+  workshop: { name: string; address: string; phone: string; email: string };
+  currencyCode: string;
+  orgId: string;
+  token: string;
+  logoUrl?: string;
+  showTorqvoiceBranding?: boolean;
+  dateFormat?: string;
+  timezone?: string;
+}) {
+  const [downloading, setDownloading] = useState(false);
+
+  const quoteNum = quote.quoteNumber || `QT-${quote.id.slice(-8).toUpperCase()}`;
+  const df = dateFormat || DEFAULT_DATE_FORMAT;
+  const tz = timezone || undefined;
+  const createdDate = fmtDate(quote.createdAt, df, tz);
+  const validUntilDate = quote.validUntil ? fmtDate(quote.validUntil, df, tz) : null;
+  const shopName = workshop.name || "Torqvoice";
+
+  const handleDownloadPDF = async () => {
+    setDownloading(true);
+    try {
+      const res = await fetch(`/api/share/quote/${orgId}/${token}/pdf`);
+      if (!res.ok) throw new Error("Failed");
+      const blob = await res.blob();
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement("a");
+      a.href = url;
+      a.download = `${quoteNum}.pdf`;
+      a.click();
+      URL.revokeObjectURL(url);
+    } catch {
+      // silent
+    }
+    setDownloading(false);
+  };
+
+  return (
+    <div className="mx-auto max-w-3xl p-4 sm:p-8">
+      <div className="mb-6 flex items-center justify-between">
+        <h1 className="text-2xl font-bold">Quote</h1>
+        <button
+          onClick={handleDownloadPDF}
+          disabled={downloading}
+          className="inline-flex items-center gap-2 rounded-lg bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-700 disabled:opacity-50"
+        >
+          {downloading ? <Loader2 className="h-4 w-4 animate-spin" /> : <Download className="h-4 w-4" />}
+          Download PDF
+        </button>
+      </div>
+
+      <div className="rounded-xl border bg-white p-6 shadow-sm sm:p-8 dark:bg-gray-900">
+        {/* Header */}
+        <div className="flex flex-col gap-4 border-b-2 border-amber-500 pb-6 sm:flex-row sm:items-start sm:justify-between">
+          <div>
+            {logoUrl && (
+              <img
+                src={logoUrl}
+                alt={shopName}
+                className="mb-2 max-h-16 max-w-[180px] object-contain object-left"
+              />
+            )}
+            <h2 className="text-xl font-bold text-amber-600 sm:text-2xl">{shopName}</h2>
+            {workshop.address && <p className="mt-1 text-sm text-gray-500">{workshop.address}</p>}
+            {workshop.phone && <p className="text-sm text-gray-500">Tel: {workshop.phone}</p>}
+            {workshop.email && <p className="text-sm text-gray-500">{workshop.email}</p>}
+          </div>
+          <div className="sm:text-right">
+            {showTorqvoiceBranding && (
+              <div className="mb-2 inline-flex items-center gap-1.5 rounded-full bg-gray-100 px-2.5 py-1 dark:bg-gray-800">
+                <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-4 w-4" />
+                <span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Torqvoice</span>
+              </div>
+            )}
+            <h3 className="text-xl font-bold">QUOTE</h3>
+            <p className="mt-1 text-sm text-gray-500">{quoteNum}</p>
+            <p className="text-sm text-gray-500">{createdDate}</p>
+            {validUntilDate && (
+              <p className="text-sm text-gray-500">Valid until: {validUntilDate}</p>
+            )}
+            <span className={`mt-2 inline-block rounded-full px-2.5 py-0.5 text-xs font-semibold capitalize ${
+              quote.status === "accepted"
+                ? "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400"
+                : quote.status === "rejected"
+                ? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
+                : "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
+            }`}>
+              {statusLabels[quote.status] || quote.status}
+            </span>
+          </div>
+        </div>
+
+        {/* Info Boxes */}
+        <div className="mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3">
+          {quote.customer && (
+            <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+              <p className="mb-1 text-xs font-bold uppercase text-amber-600">Prepared For</p>
+              <p className="font-semibold">{quote.customer.name}</p>
+              {quote.customer.company && <p className="text-sm">{quote.customer.company}</p>}
+              {quote.customer.address && <p className="text-sm text-gray-500">{quote.customer.address}</p>}
+              {quote.customer.email && <p className="text-sm text-gray-500">{quote.customer.email}</p>}
+              {quote.customer.phone && <p className="text-sm text-gray-500">{quote.customer.phone}</p>}
+            </div>
+          )}
+          {quote.vehicle && (
+            <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+              <p className="mb-1 text-xs font-bold uppercase text-amber-600">Vehicle</p>
+              <p className="font-semibold">{quote.vehicle.year} {quote.vehicle.make} {quote.vehicle.model}</p>
+              {quote.vehicle.vin && <p className="text-sm text-gray-500">VIN: {quote.vehicle.vin}</p>}
+              {quote.vehicle.licensePlate && <p className="text-sm text-gray-500">Plate: {quote.vehicle.licensePlate}</p>}
+            </div>
+          )}
+          <div className="rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+            <p className="mb-1 text-xs font-bold uppercase text-amber-600">Quote Details</p>
+            <p className="font-semibold">{quote.title}</p>
+          </div>
+        </div>
+
+        {/* Description */}
+        {quote.description && (
+          <div className="mt-6 rounded-lg bg-gray-50 p-4 dark:bg-gray-800">
+            <p className="mb-1 text-xs font-bold uppercase text-amber-600">Description</p>
+            <div
+              className="notes-content text-sm text-gray-600 dark:text-gray-400"
+              dangerouslySetInnerHTML={{ __html: sanitizeHtml(quote.description) }}
+            />
+          </div>
+        )}
+
+        {/* Parts */}
+        {quote.partItems.length > 0 && (
+          <div className="mt-6">
+            <h4 className="mb-3 font-semibold">Parts</h4>
+            <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
+              <table className="w-full min-w-125 text-sm">
+                <thead>
+                  <tr className="border-b bg-amber-50 text-left dark:bg-amber-900/20">
+                    <th className="p-2 font-medium">Part #</th>
+                    <th className="p-2 font-medium">Description</th>
+                    <th className="p-2 text-right font-medium">Qty</th>
+                    <th className="p-2 text-right font-medium">Unit Price</th>
+                    <th className="p-2 text-right font-medium">Total</th>
+                  </tr>
+                </thead>
+                <tbody className="divide-y">
+                  {quote.partItems.map((p, i) => (
+                    <tr key={i}>
+                      <td className="p-2 font-mono text-xs">{p.partNumber || "-"}</td>
+                      <td className="p-2">{p.name}</td>
+                      <td className="p-2 text-right">{p.quantity}</td>
+                      <td className="p-2 text-right">{formatCurrency(p.unitPrice, currencyCode)}</td>
+                      <td className="p-2 text-right font-medium">{formatCurrency(p.total, currencyCode)}</td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          </div>
+        )}
+
+        {/* Labor */}
+        {quote.laborItems.length > 0 && (
+          <div className="mt-6">
+            <h4 className="mb-3 font-semibold">Labor</h4>
+            <div className="-mx-6 overflow-x-auto px-6 sm:mx-0 sm:px-0">
+              <table className="w-full min-w-112.5 text-sm">
+                <thead>
+                  <tr className="border-b bg-amber-50 text-left dark:bg-amber-900/20">
+                    <th className="p-2 font-medium">Description</th>
+                    <th className="p-2 text-right font-medium">Hours</th>
+                    <th className="p-2 text-right font-medium">Rate</th>
+                    <th className="p-2 text-right font-medium">Total</th>
+                  </tr>
+                </thead>
+                <tbody className="divide-y">
+                  {quote.laborItems.map((l, i) => (
+                    <tr key={i}>
+                      <td className="p-2">{l.description}</td>
+                      <td className="p-2 text-right">{l.hours}</td>
+                      <td className="p-2 text-right">{formatCurrency(l.rate, currencyCode)}/hr</td>
+                      <td className="p-2 text-right font-medium">{formatCurrency(l.total, currencyCode)}</td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </div>
+          </div>
+        )}
+
+        {/* Totals */}
+        <div className="mt-6 ml-auto max-w-xs space-y-2">
+          {quote.subtotal > 0 && (
+            <div className="flex justify-between text-sm">
+              <span className="text-gray-500">Subtotal</span>
+              <span>{formatCurrency(quote.subtotal, currencyCode)}</span>
+            </div>
+          )}
+          {quote.discountAmount > 0 && (
+            <div className="flex justify-between text-sm">
+              <span className="text-gray-500">
+                Discount{quote.discountType === "percentage" ? ` (${quote.discountValue}%)` : ""}
+              </span>
+              <span className="text-red-500">{formatCurrency(-quote.discountAmount, currencyCode)}</span>
+            </div>
+          )}
+          {quote.taxRate > 0 && (
+            <div className="flex justify-between text-sm">
+              <span className="text-gray-500">Tax ({quote.taxRate}%)</span>
+              <span>{formatCurrency(quote.taxAmount, currencyCode)}</span>
+            </div>
+          )}
+          <div className="border-t border-amber-500 pt-2">
+            <div className="flex justify-between text-lg font-bold">
+              <span>Total</span>
+              <span className="text-amber-600">{formatCurrency(quote.totalAmount, currencyCode)}</span>
+            </div>
+          </div>
+        </div>
+
+        {/* Torqvoice branding near totals */}
+        {showTorqvoiceBranding && (
+          <div className="mt-3 flex items-center justify-end gap-1.5">
+            <span className="text-xs text-gray-400">Powered by</span>
+            <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-3.5 w-3.5" />
+            <span className="text-xs font-semibold text-gray-500 dark:text-gray-400">Torqvoice</span>
+          </div>
+        )}
+      </div>
+
+      <div className="mt-4 flex items-center justify-center gap-1.5">
+        {showTorqvoiceBranding ? (
+          <>
+            <span className="text-xs text-gray-400">Powered by</span>
+            <img src="/torqvoice_app_logo.png" alt="Torqvoice" className="h-4 w-4" />
+            <a
+              href="https://torqvoice.com"
+              target="_blank"
+              rel="noopener noreferrer"
+              className="text-xs font-semibold text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
+            >
+              Torqvoice
+            </a>
+          </>
+        ) : (
+          <p className="text-center text-xs text-gray-400">{shopName}</p>
+        )}
+      </div>
+    </div>
+  );
+}

+ 122 - 0
src/app/api/share/quote/[orgId]/[token]/pdf/route.ts

@@ -0,0 +1,122 @@
+import { NextResponse } from "next/server";
+import { renderToBuffer } from "@react-pdf/renderer";
+import { db } from "@/lib/db";
+import { QuotePDF } from "@/features/quotes/Components/QuotePDF";
+import React from "react";
+import { readFile } from "fs/promises";
+import { resolveUploadPath } from "@/lib/resolve-upload-path";
+import { getFeatures } from "@/lib/features";
+import { getTorqvoiceLogoDataUri } from "@/lib/torqvoice-branding";
+
+export async function GET(
+  _request: Request,
+  { params }: { params: Promise<{ orgId: string; token: string }> }
+) {
+  try {
+    const { orgId, token } = await params;
+
+    const quote = await db.quote.findFirst({
+      where: { publicToken: token, organizationId: orgId },
+      include: {
+        partItems: true,
+        laborItems: true,
+        customer: {
+          select: { name: true, email: true, phone: true, address: true, company: true },
+        },
+        vehicle: {
+          select: { make: true, model: true, year: true, vin: true, licensePlate: true },
+        },
+      },
+    });
+
+    if (!quote) {
+      return NextResponse.json({ error: "Quote not found" }, { status: 404 });
+    }
+
+    const [settings, org] = await Promise.all([
+      db.appSetting.findMany({ where: { organizationId: orgId } }),
+      db.organization.findUnique({
+        where: { id: orgId },
+        select: { name: true },
+      }),
+    ]);
+
+    const settingsMap: Record<string, string> = {};
+    for (const s of settings) settingsMap[s.key] = s.value;
+
+    let logoDataUri: string | undefined;
+    const logoPath = settingsMap["workshop.logo"];
+    if (logoPath) {
+      try {
+        const fullPath = resolveUploadPath(logoPath);
+        const logoBuffer = await readFile(fullPath);
+        const ext = logoPath.split(".").pop()?.toLowerCase() || "png";
+        const mimeMap: Record<string, string> = {
+          png: "image/png",
+          jpg: "image/jpeg",
+          jpeg: "image/jpeg",
+          webp: "image/webp",
+          svg: "image/svg+xml",
+        };
+        const mime = mimeMap[ext] || "image/png";
+        logoDataUri = `data:${mime};base64,${logoBuffer.toString("base64")}`;
+      } catch {
+        // Skip
+      }
+    }
+
+    const features = await getFeatures(orgId);
+    let torqvoiceLogoDataUri: string | undefined;
+    if (!features.brandingRemoved) {
+      torqvoiceLogoDataUri = await getTorqvoiceLogoDataUri();
+    }
+
+    const template = {
+      primaryColor: settingsMap["invoice.primaryColor"] || "#d97706",
+      fontFamily: settingsMap["invoice.fontFamily"] || "Helvetica",
+      showLogo: settingsMap["invoice.showLogo"] !== "false",
+      showCompanyName: settingsMap["invoice.showCompanyName"] !== "false",
+      headerStyle: settingsMap["invoice.headerStyle"] || "standard",
+    };
+
+    const element = React.createElement(QuotePDF, {
+      data: quote,
+      workshop: {
+        name: org?.name || "",
+        address: settingsMap["workshop.address"] || "",
+        phone: settingsMap["workshop.phone"] || "",
+        email: settingsMap["workshop.email"] || "",
+      },
+      currencyCode: settingsMap["workshop.currencyCode"] || "USD",
+      logoDataUri,
+      torqvoiceLogoDataUri,
+      dateFormat: settingsMap["workshop.dateFormat"] || undefined,
+      timezone: settingsMap["workshop.timezone"] || undefined,
+      template,
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    }) as any;
+    const buffer = await renderToBuffer(element);
+
+    const quoteNum =
+      quote.quoteNumber || `QT-${quote.id.slice(-8).toUpperCase()}`;
+
+    return new NextResponse(
+      buffer.buffer.slice(
+        buffer.byteOffset,
+        buffer.byteOffset + buffer.byteLength
+      ) as ArrayBuffer,
+      {
+        headers: {
+          "Content-Type": "application/pdf",
+          "Content-Disposition": `attachment; filename="${quoteNum}.pdf"`,
+        },
+      }
+    );
+  } catch (error) {
+    console.error("[Public Quote PDF] Error:", error);
+    return NextResponse.json(
+      { error: "Failed to generate PDF" },
+      { status: 500 }
+    );
+  }
+}

+ 42 - 0
src/features/quotes/Actions/quoteShareActions.ts

@@ -0,0 +1,42 @@
+"use server";
+
+import { randomUUID } from "crypto";
+import { db } from "@/lib/db";
+import { withAuth } from "@/lib/with-auth";
+import { revalidatePath } from "next/cache";
+import { PermissionAction, PermissionSubject } from "@/lib/permissions";
+
+export async function generateQuotePublicLink(quoteId: string) {
+  return withAuth(async ({ organizationId }) => {
+    const quote = await db.quote.findFirst({
+      where: { id: quoteId, organizationId },
+    });
+    if (!quote) throw new Error("Quote not found");
+
+    const token = randomUUID();
+    await db.quote.update({
+      where: { id: quoteId },
+      data: { publicToken: token },
+    });
+
+    revalidatePath(`/quotes/${quoteId}`);
+    return { token, organizationId };
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.QUOTES }] });
+}
+
+export async function revokeQuotePublicLink(quoteId: string) {
+  return withAuth(async ({ organizationId }) => {
+    const quote = await db.quote.findFirst({
+      where: { id: quoteId, organizationId },
+    });
+    if (!quote) throw new Error("Quote not found");
+
+    await db.quote.update({
+      where: { id: quoteId },
+      data: { publicToken: null },
+    });
+
+    revalidatePath(`/quotes/${quoteId}`);
+    return { revoked: true };
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.QUOTES }] });
+}

+ 371 - 232
src/features/quotes/Components/QuoteForm.tsx

@@ -1,14 +1,12 @@
 "use client";
 "use client";
 
 
-import { useState, useCallback } from "react";
+import { useState, useCallback, useEffect } from "react";
 import { useRouter } from "next/navigation";
 import { useRouter } from "next/navigation";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import Link from "next/link";
 import Link from "next/link";
 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 { Label } from "@/components/ui/label";
 import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
 import {
 import {
   Select,
   Select,
   SelectContent,
   SelectContent,
@@ -16,10 +14,16 @@ import {
   SelectTrigger,
   SelectTrigger,
   SelectValue,
   SelectValue,
 } from "@/components/ui/select";
 } from "@/components/ui/select";
+import {
+  ResizablePanelGroup,
+  ResizablePanel,
+  ResizableHandle,
+} from "@/components/ui/resizable";
 import { useGlassModal } from "@/components/glass-modal";
 import { useGlassModal } from "@/components/glass-modal";
 import { createQuote, updateQuote } from "@/features/quotes/Actions/quoteActions";
 import { createQuote, updateQuote } from "@/features/quotes/Actions/quoteActions";
 import type { QuotePartInput, QuoteLaborInput } from "@/features/quotes/Schema/quoteSchema";
 import type { QuotePartInput, QuoteLaborInput } from "@/features/quotes/Schema/quoteSchema";
-import { ArrowLeft, Loader2, Plus, Trash2 } from "lucide-react";
+import { RichTextEditor } from "@/features/vehicles/Components/service-edit/RichTextEditor";
+import { ArrowLeft, Car, FileText, Loader2, Plus, Save, Trash2, Users, X } from "lucide-react";
 import { formatCurrency, getCurrencySymbol } from "@/lib/format";
 import { formatCurrency, getCurrencySymbol } from "@/lib/format";
 
 
 interface CustomerOption {
 interface CustomerOption {
@@ -35,6 +39,8 @@ interface VehicleOption {
   model: string;
   model: string;
   year: number;
   year: number;
   licensePlate: string | null;
   licensePlate: string | null;
+  customerId: string | null;
+  customerName: string | null;
 }
 }
 
 
 interface InitialData {
 interface InitialData {
@@ -72,6 +78,20 @@ const makeEmptyLabor = (defaultRate: number): QuoteLaborInput => ({
   total: 0,
   total: 0,
 });
 });
 
 
+const LG_BREAKPOINT = 1024;
+
+function useIsLargeScreen() {
+  const [isLarge, setIsLarge] = useState(false);
+  useEffect(() => {
+    const mql = window.matchMedia(`(min-width: ${LG_BREAKPOINT}px)`);
+    const onChange = () => setIsLarge(mql.matches);
+    mql.addEventListener("change", onChange);
+    setIsLarge(mql.matches);
+    return () => mql.removeEventListener("change", onChange);
+  }, []);
+  return isLarge;
+}
+
 export function QuoteForm({
 export function QuoteForm({
   currencyCode = "USD",
   currencyCode = "USD",
   defaultTaxRate = 0,
   defaultTaxRate = 0,
@@ -95,6 +115,7 @@ export function QuoteForm({
   const isEdit = !!initialData;
   const isEdit = !!initialData;
   const router = useRouter();
   const router = useRouter();
   const modal = useGlassModal();
   const modal = useGlassModal();
+  const isLarge = useIsLargeScreen();
   const [loading, setLoading] = useState(false);
   const [loading, setLoading] = useState(false);
   const [status, setStatus] = useState(initialData?.status || "draft");
   const [status, setStatus] = useState(initialData?.status || "draft");
   const [customerId, setCustomerId] = useState(initialData?.customerId || "");
   const [customerId, setCustomerId] = useState(initialData?.customerId || "");
@@ -104,6 +125,9 @@ export function QuoteForm({
   const [taxRate, setTaxRate] = useState(initialData?.taxRate ?? defaultTaxRate);
   const [taxRate, setTaxRate] = useState(initialData?.taxRate ?? defaultTaxRate);
   const [discountType, setDiscountType] = useState<string>(initialData?.discountType || "none");
   const [discountType, setDiscountType] = useState<string>(initialData?.discountType || "none");
   const [discountValue, setDiscountValue] = useState(initialData?.discountValue ?? 0);
   const [discountValue, setDiscountValue] = useState(initialData?.discountValue ?? 0);
+  const [noteType, setNoteType] = useState<"public" | "internal">("public");
+  const [description, setDescription] = useState(initialData?.description || "");
+  const [notes, setNotes] = useState(initialData?.notes || "");
 
 
   const [defaultValidDate] = useState(() =>
   const [defaultValidDate] = useState(() =>
     initialData?.validUntil
     initialData?.validUntil
@@ -122,6 +146,20 @@ export function QuoteForm({
   const taxAmount = (subtotal - discountAmount) * (taxRate / 100);
   const taxAmount = (subtotal - discountAmount) * (taxRate / 100);
   const totalAmount = subtotal - discountAmount + taxAmount;
   const totalAmount = subtotal - discountAmount + taxAmount;
 
 
+  const selectedVehicle = vehicles.find((v) => v.id === vehicleId);
+  const selectedCustomer = customers.find((c) => c.id === customerId);
+
+  const handleVehicleChange = (v: string) => {
+    const vid = v === "none" ? "" : v;
+    setVehicleId(vid);
+    if (vid) {
+      const vehicle = vehicles.find((veh) => veh.id === vid);
+      if (vehicle?.customerId) {
+        setCustomerId(vehicle.customerId);
+      }
+    }
+  };
+
   const updatePart = useCallback((index: number, field: keyof QuotePartInput, value: string | number) => {
   const updatePart = useCallback((index: number, field: keyof QuotePartInput, value: string | number) => {
     setPartItems((prev) => {
     setPartItems((prev) => {
       const updated = [...prev];
       const updated = [...prev];
@@ -153,12 +191,12 @@ export function QuoteForm({
     const formData = new FormData(e.currentTarget);
     const formData = new FormData(e.currentTarget);
     const payload = {
     const payload = {
       title: formData.get("title") as string,
       title: formData.get("title") as string,
-      description: (formData.get("description") as string) || undefined,
+      description: description || undefined,
       status,
       status,
       validUntil: (formData.get("validUntil") as string) || undefined,
       validUntil: (formData.get("validUntil") as string) || undefined,
       customerId: customerId || undefined,
       customerId: customerId || undefined,
       vehicleId: vehicleId || undefined,
       vehicleId: vehicleId || undefined,
-      notes: (formData.get("notes") as string) || undefined,
+      notes: notes || undefined,
       partItems: partItems.filter((p) => p.name),
       partItems: partItems.filter((p) => p.name),
       laborItems: laborItems.filter((l) => l.description),
       laborItems: laborItems.filter((l) => l.description),
       subtotal,
       subtotal,
@@ -184,247 +222,348 @@ export function QuoteForm({
     setLoading(false);
     setLoading(false);
   };
   };
 
 
-  return (
-    <div className="space-y-6">
-      <div>
-        <Link
-          href="/quotes"
-          className="mb-4 inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
-        >
-          <ArrowLeft className="h-4 w-4" />
-          Back to Quotes
-        </Link>
-        <h1 className="text-2xl font-bold">{isEdit ? "Edit Quote" : "New Quote"}</h1>
+  // --- Left column: Parts, Labor, Notes ---
+  const leftColumn = (
+    <div className="space-y-3">
+      {/* Parts */}
+      <div className="rounded-lg border p-3 space-y-2">
+        <div className="flex items-center justify-between">
+          <h3 className="text-sm font-semibold">Parts</h3>
+          <Button type="button" variant="outline" size="sm" onClick={() => setPartItems([...partItems, emptyPart()])}>
+            <Plus className="mr-1 h-3.5 w-3.5" /> Add Part
+          </Button>
+        </div>
+        {partItems.length > 0 && (
+          <>
+            <div className="hidden grid-cols-[1fr_2fr_0.7fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
+              <span>Part #</span><span>Name</span><span>Qty</span><span>Unit Price</span><span>Total</span><span />
+            </div>
+            {partItems.map((part, i) => (
+              <div key={i} className="grid grid-cols-2 gap-2 sm:grid-cols-[1fr_2fr_0.7fr_1fr_1fr_auto]">
+                <Input placeholder="Part #" value={part.partNumber ?? ""} onChange={(e) => updatePart(i, "partNumber", e.target.value)} />
+                <Input placeholder="Name *" value={part.name} onChange={(e) => updatePart(i, "name", e.target.value)} />
+                <Input type="number" min="0" step="1" value={part.quantity} onChange={(e) => updatePart(i, "quantity", Number(e.target.value))} />
+                <Input type="number" min="0" step="0.01" value={part.unitPrice} onChange={(e) => updatePart(i, "unitPrice", Number(e.target.value))} />
+                <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">{formatCurrency(part.total, currencyCode)}</div>
+                <Button type="button" variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground hover:text-destructive" onClick={() => setPartItems(partItems.filter((_, j) => j !== i))}>
+                  <Trash2 className="h-4 w-4" />
+                </Button>
+              </div>
+            ))}
+            <button
+              type="button"
+              className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
+              onClick={() => setPartItems([...partItems, emptyPart()])}
+            >
+              <Plus className="h-4 w-4" />
+            </button>
+            <div className="flex justify-end pt-1 text-sm">
+              <span className="font-medium">Parts Subtotal: {formatCurrency(partsSubtotal, currencyCode)}</span>
+            </div>
+          </>
+        )}
+        {partItems.length === 0 && (
+          <button
+            type="button"
+            className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
+            onClick={() => setPartItems([...partItems, emptyPart()])}
+          >
+            <Plus className="mr-1 h-4 w-4" />
+            <span className="text-sm">Add Part</span>
+          </button>
+        )}
       </div>
       </div>
 
 
-      <form onSubmit={handleSubmit} className="space-y-6">
-        <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
-          {/* Column 1: Basic Info */}
-          <Card className="border-0 shadow-sm">
-            <CardHeader>
-              <CardTitle className="text-base">Quote Details</CardTitle>
-            </CardHeader>
-            <CardContent className="space-y-3">
-              <div className="space-y-2">
-                <Label htmlFor="title">Title *</Label>
-                <Input id="title" name="title" placeholder="Vehicle repair estimate" defaultValue={initialData?.title || ""} required />
-              </div>
-              <div className="space-y-2">
-                <Label>Status</Label>
-                <Select value={status} onValueChange={setStatus}>
-                  <SelectTrigger><SelectValue /></SelectTrigger>
-                  <SelectContent>
-                    <SelectItem value="draft">Draft</SelectItem>
-                    <SelectItem value="sent">Sent</SelectItem>
-                    <SelectItem value="accepted">Accepted</SelectItem>
-                    <SelectItem value="rejected">Rejected</SelectItem>
-                  </SelectContent>
-                </Select>
-              </div>
-              <div className="space-y-2">
-                <Label htmlFor="validUntil">Valid Until</Label>
-                <Input id="validUntil" name="validUntil" type="date" defaultValue={defaultValidDate} />
-              </div>
-              <div className="space-y-2">
-                <Label>Customer</Label>
-                <Select value={customerId || "none"} onValueChange={(v) => setCustomerId(v === "none" ? "" : v)}>
-                  <SelectTrigger><SelectValue placeholder="Select customer..." /></SelectTrigger>
-                  <SelectContent>
-                    <SelectItem value="none">None</SelectItem>
-                    {customers.map((c) => (
-                      <SelectItem key={c.id} value={c.id}>{c.name}{c.company ? ` (${c.company})` : ""}</SelectItem>
-                    ))}
-                  </SelectContent>
-                </Select>
-              </div>
-              <div className="space-y-2">
-                <Label>Vehicle</Label>
-                <Select value={vehicleId || "none"} onValueChange={(v) => setVehicleId(v === "none" ? "" : v)}>
-                  <SelectTrigger><SelectValue placeholder="Select vehicle..." /></SelectTrigger>
-                  <SelectContent>
-                    <SelectItem value="none">None</SelectItem>
-                    {vehicles.map((v) => (
-                      <SelectItem key={v.id} value={v.id}>{v.year} {v.make} {v.model}{v.licensePlate ? ` (${v.licensePlate})` : ""}</SelectItem>
-                    ))}
-                  </SelectContent>
-                </Select>
+      {/* Labor */}
+      <div className="rounded-lg border p-3 space-y-2">
+        <div className="flex items-center justify-between">
+          <h3 className="text-sm font-semibold">Labor</h3>
+          <Button type="button" variant="outline" size="sm" onClick={() => setLaborItems([...laborItems, makeEmptyLabor(defaultLaborRate)])}>
+            <Plus className="mr-1 h-3.5 w-3.5" /> Add Labor
+          </Button>
+        </div>
+        {laborItems.length > 0 && (
+          <>
+            <div className="hidden grid-cols-[2fr_1fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
+              <span>Description</span><span>Hours</span><span>Rate ({cs}/hr)</span><span>Total</span><span />
+            </div>
+            {laborItems.map((labor, i) => (
+              <div key={i} className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_1fr_auto]">
+                <Input placeholder="Description *" value={labor.description} onChange={(e) => updateLabor(i, "description", e.target.value)} className="col-span-2 sm:col-span-1" />
+                <Input type="number" min="0" step="0.5" value={labor.hours} onChange={(e) => updateLabor(i, "hours", Number(e.target.value))} />
+                <Input type="number" min="0" step="0.01" value={labor.rate} onChange={(e) => updateLabor(i, "rate", Number(e.target.value))} />
+                <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">{formatCurrency(labor.total, currencyCode)}</div>
+                <Button type="button" variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground hover:text-destructive" onClick={() => setLaborItems(laborItems.filter((_, j) => j !== i))}>
+                  <Trash2 className="h-4 w-4" />
+                </Button>
               </div>
               </div>
-            </CardContent>
-          </Card>
+            ))}
+            <button
+              type="button"
+              className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
+              onClick={() => setLaborItems([...laborItems, makeEmptyLabor(defaultLaborRate)])}
+            >
+              <Plus className="h-4 w-4" />
+            </button>
+            <div className="flex justify-end pt-1 text-sm">
+              <span className="font-medium">Labor Subtotal: {formatCurrency(laborSubtotal, currencyCode)}</span>
+            </div>
+          </>
+        )}
+        {laborItems.length === 0 && (
+          <button
+            type="button"
+            className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground"
+            onClick={() => setLaborItems([...laborItems, makeEmptyLabor(defaultLaborRate)])}
+          >
+            <Plus className="mr-1 h-4 w-4" />
+            <span className="text-sm">Add Labor</span>
+          </button>
+        )}
+      </div>
 
 
-          {/* Column 2: Notes */}
-          <Card className="border-0 shadow-sm">
-            <CardHeader>
-              <CardTitle className="text-base">Notes</CardTitle>
-            </CardHeader>
-            <CardContent className="space-y-4">
-              <Textarea
-                name="description"
-                placeholder="Quote description..."
-                rows={6}
-                defaultValue={initialData?.description || ""}
-              />
-              <div className="space-y-2">
-                <Label className="text-base font-semibold">Internal Notes</Label>
-                <Textarea
-                  name="notes"
-                  placeholder="Internal notes (not shown to customer)..."
-                  rows={6}
-                  defaultValue={initialData?.notes || ""}
-                />
-              </div>
-            </CardContent>
-          </Card>
+      {/* Notes */}
+      <div className="rounded-lg border p-3 space-y-3">
+        <div className="flex items-center justify-between">
+          <h3 className="flex items-center gap-2 text-sm font-semibold">
+            <FileText className="h-3.5 w-3.5" />
+            Notes
+          </h3>
+          <Select value={noteType} onValueChange={(v) => setNoteType(v as "public" | "internal")}>
+            <SelectTrigger className="h-7 w-[120px] text-xs">
+              <SelectValue />
+            </SelectTrigger>
+            <SelectContent>
+              <SelectItem value="public">Public</SelectItem>
+              <SelectItem value="internal">Internal</SelectItem>
+            </SelectContent>
+          </Select>
+        </div>
+        {noteType === "public" && (
+          <div className="space-y-1">
+            <RichTextEditor
+              content={description}
+              onChange={setDescription}
+              placeholder="Quote description shown to the customer..."
+            />
+            <p className="text-xs text-muted-foreground">Shown on the quote document</p>
+          </div>
+        )}
+        {noteType === "internal" && (
+          <div className="space-y-1">
+            <RichTextEditor
+              content={notes}
+              onChange={setNotes}
+              placeholder="Internal notes (not shown to customer)..."
+            />
+            <p className="text-xs text-muted-foreground">Only visible to your team</p>
+          </div>
+        )}
+      </div>
+    </div>
+  );
 
 
-          {/* Column 3: Totals */}
-          <Card className="border-0 shadow-sm">
-            <CardHeader>
-              <CardTitle className="text-base">Totals</CardTitle>
-            </CardHeader>
-            <CardContent>
-              <div className="space-y-3">
-                <div className="flex items-center justify-between text-sm">
-                  <span className="text-muted-foreground">Parts</span>
-                  <span>{formatCurrency(partsSubtotal, currencyCode)}</span>
-                </div>
-                <div className="flex items-center justify-between text-sm">
-                  <span className="text-muted-foreground">Labor</span>
-                  <span>{formatCurrency(laborSubtotal, currencyCode)}</span>
-                </div>
-                <div className="flex items-center justify-between text-sm">
-                  <span className="text-muted-foreground">Subtotal</span>
-                  <span className="font-medium">{formatCurrency(subtotal, currencyCode)}</span>
-                </div>
-                <div className="space-y-2">
-                  <div className="flex items-center justify-between text-sm">
-                    <div className="flex items-center gap-2">
-                      <span className="text-muted-foreground">Discount</span>
-                      <Select value={discountType} onValueChange={setDiscountType}>
-                        <SelectTrigger className="h-7 w-28 text-xs"><SelectValue /></SelectTrigger>
-                        <SelectContent>
-                          <SelectItem value="none">None</SelectItem>
-                          <SelectItem value="percentage">Percentage</SelectItem>
-                          <SelectItem value="fixed">Fixed</SelectItem>
-                        </SelectContent>
-                      </Select>
-                      {discountType !== "none" && (
-                        <Input
-                          type="number"
-                          min="0"
-                          step="0.01"
-                          value={discountValue}
-                          onChange={(e) => setDiscountValue(Number(e.target.value))}
-                          className="h-7 w-20 text-right text-xs"
-                        />
-                      )}
-                      {discountType === "percentage" && <span className="text-muted-foreground">%</span>}
-                    </div>
-                    {discountAmount > 0 && (
-                      <span className="text-destructive">{formatCurrency(-discountAmount, currencyCode)}</span>
-                    )}
-                  </div>
-                </div>
-                {taxEnabled && (
-                  <div className="flex items-center justify-between text-sm">
-                    <div className="flex items-center gap-2">
-                      <span className="text-muted-foreground">Tax</span>
-                      <Input
-                        type="number"
-                        min="0"
-                        step="0.1"
-                        value={taxRate}
-                        onChange={(e) => setTaxRate(Number(e.target.value))}
-                        className="h-7 w-20 text-right text-xs"
-                      />
-                      <span className="text-muted-foreground">%</span>
-                    </div>
-                    <span>{formatCurrency(taxAmount, currencyCode)}</span>
-                  </div>
-                )}
-                <div className="flex items-center justify-between border-t pt-3 text-lg font-bold">
-                  <span>Total</span>
-                  <span>{formatCurrency(totalAmount, currencyCode)}</span>
-                </div>
-              </div>
-            </CardContent>
-          </Card>
+  // --- Right column: Vehicle & Customer, Quote Details, Totals ---
+  const rightColumn = (
+    <div className="space-y-3">
+      {/* Vehicle & Customer */}
+      <div className="rounded-lg border p-3 space-y-3">
+        <div className="space-y-1">
+          <Label className="text-xs">Vehicle</Label>
+          <Select value={vehicleId || "none"} onValueChange={handleVehicleChange}>
+            <SelectTrigger><SelectValue placeholder="Select vehicle..." /></SelectTrigger>
+            <SelectContent>
+              <SelectItem value="none">None</SelectItem>
+              {vehicles.map((v) => (
+                <SelectItem key={v.id} value={v.id}>{v.year} {v.make} {v.model}{v.licensePlate ? ` (${v.licensePlate})` : ""}</SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
         </div>
         </div>
 
 
-        {/* Parts */}
-        <Card className="border-0 shadow-sm">
-          <CardHeader className="flex flex-row items-center justify-between">
-            <CardTitle className="text-base">Parts</CardTitle>
-            <Button type="button" variant="outline" size="sm" onClick={() => setPartItems([...partItems, emptyPart()])}>
-              <Plus className="mr-1 h-3.5 w-3.5" /> Add Part
+        {selectedVehicle && (
+          <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
+            <Car className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
+            <div className="min-w-0 flex-1 text-sm">
+              <span className="font-medium">{selectedVehicle.year} {selectedVehicle.make} {selectedVehicle.model}</span>
+              {selectedVehicle.licensePlate && (
+                <span className="ml-1.5 text-muted-foreground">{selectedVehicle.licensePlate}</span>
+              )}
+            </div>
+            <Button type="button" variant="ghost" size="icon" className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground" onClick={() => setVehicleId("")}>
+              <X className="h-3 w-3" />
             </Button>
             </Button>
-          </CardHeader>
-          <CardContent>
-            {partItems.length === 0 ? (
-              <p className="py-4 text-center text-sm text-muted-foreground">No parts added yet</p>
-            ) : (
-              <div className="space-y-3">
-                <div className="hidden grid-cols-[1fr_2fr_0.7fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
-                  <span>Part #</span><span>Name</span><span>Qty</span><span>Unit Price</span><span>Total</span><span />
-                </div>
-                {partItems.map((part, i) => (
-                  <div key={i} className="grid grid-cols-2 gap-2 sm:grid-cols-[1fr_2fr_0.7fr_1fr_1fr_auto]">
-                    <Input placeholder="Part #" value={part.partNumber ?? ""} onChange={(e) => updatePart(i, "partNumber", e.target.value)} />
-                    <Input placeholder="Name *" value={part.name} onChange={(e) => updatePart(i, "name", e.target.value)} />
-                    <Input type="number" min="0" step="1" value={part.quantity} onChange={(e) => updatePart(i, "quantity", Number(e.target.value))} />
-                    <Input type="number" min="0" step="0.01" value={part.unitPrice} onChange={(e) => updatePart(i, "unitPrice", Number(e.target.value))} />
-                    <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">{formatCurrency(part.total, currencyCode)}</div>
-                    <Button type="button" variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground hover:text-destructive" onClick={() => setPartItems(partItems.filter((_, j) => j !== i))}>
-                      <Trash2 className="h-4 w-4" />
-                    </Button>
-                  </div>
-                ))}
-              </div>
-            )}
-          </CardContent>
-        </Card>
+          </div>
+        )}
 
 
-        {/* Labor */}
-        <Card className="border-0 shadow-sm">
-          <CardHeader className="flex flex-row items-center justify-between">
-            <CardTitle className="text-base">Labor</CardTitle>
-            <Button type="button" variant="outline" size="sm" onClick={() => setLaborItems([...laborItems, makeEmptyLabor(defaultLaborRate)])}>
-              <Plus className="mr-1 h-3.5 w-3.5" /> Add Labor
+        <div className="space-y-1">
+          <Label className="text-xs">Customer</Label>
+          <Select value={customerId || "none"} onValueChange={(v) => setCustomerId(v === "none" ? "" : v)}>
+            <SelectTrigger><SelectValue placeholder="Select customer..." /></SelectTrigger>
+            <SelectContent>
+              <SelectItem value="none">None</SelectItem>
+              {customers.map((c) => (
+                <SelectItem key={c.id} value={c.id}>{c.name}{c.company ? ` (${c.company})` : ""}</SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+        </div>
+
+        {selectedCustomer && (
+          <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
+            <Users className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
+            <div className="min-w-0 flex-1 text-sm">
+              <span className="font-medium">{selectedCustomer.name}</span>
+              {selectedCustomer.company && (
+                <span className="ml-1.5 text-muted-foreground">{selectedCustomer.company}</span>
+              )}
+            </div>
+            <Button type="button" variant="ghost" size="icon" className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground" onClick={() => setCustomerId("")}>
+              <X className="h-3 w-3" />
             </Button>
             </Button>
-          </CardHeader>
-          <CardContent>
-            {laborItems.length === 0 ? (
-              <p className="py-4 text-center text-sm text-muted-foreground">No labor added yet</p>
-            ) : (
-              <div className="space-y-3">
-                <div className="hidden grid-cols-[2fr_1fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
-                  <span>Description</span><span>Hours</span><span>Rate ({cs}/hr)</span><span>Total</span><span />
-                </div>
-                {laborItems.map((labor, i) => (
-                  <div key={i} className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_1fr_auto]">
-                    <Input placeholder="Description *" value={labor.description} onChange={(e) => updateLabor(i, "description", e.target.value)} className="col-span-2 sm:col-span-1" />
-                    <Input type="number" min="0" step="0.5" value={labor.hours} onChange={(e) => updateLabor(i, "hours", Number(e.target.value))} />
-                    <Input type="number" min="0" step="0.01" value={labor.rate} onChange={(e) => updateLabor(i, "rate", Number(e.target.value))} />
-                    <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">{formatCurrency(labor.total, currencyCode)}</div>
-                    <Button type="button" variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground hover:text-destructive" onClick={() => setLaborItems(laborItems.filter((_, j) => j !== i))}>
-                      <Trash2 className="h-4 w-4" />
-                    </Button>
-                  </div>
-                ))}
-              </div>
+          </div>
+        )}
+      </div>
+
+      {/* Quote Details */}
+      <div className="rounded-lg border p-3 space-y-3">
+        <h3 className="text-sm font-semibold">Quote Details</h3>
+        <div className="space-y-1">
+          <Label htmlFor="title" className="text-xs">Title *</Label>
+          <Input id="title" name="title" placeholder="Vehicle repair estimate" defaultValue={initialData?.title || ""} required />
+        </div>
+        <div className="grid grid-cols-2 gap-2">
+          <div className="space-y-1">
+            <Label className="text-xs">Status</Label>
+            <Select value={status} onValueChange={setStatus}>
+              <SelectTrigger><SelectValue /></SelectTrigger>
+              <SelectContent>
+                <SelectItem value="draft">Draft</SelectItem>
+                <SelectItem value="sent">Sent</SelectItem>
+                <SelectItem value="accepted">Accepted</SelectItem>
+                <SelectItem value="rejected">Rejected</SelectItem>
+              </SelectContent>
+            </Select>
+          </div>
+          <div className="space-y-1">
+            <Label htmlFor="validUntil" className="text-xs">Valid Until</Label>
+            <Input id="validUntil" name="validUntil" type="date" defaultValue={defaultValidDate} />
+          </div>
+        </div>
+      </div>
+
+      {/* Totals */}
+      <div className="rounded-lg border p-3 space-y-2">
+        <h3 className="text-sm font-semibold">Totals</h3>
+        <div className="space-y-2">
+          <div className="flex items-center justify-between text-sm">
+            <span className="text-muted-foreground">Parts</span>
+            <span>{formatCurrency(partsSubtotal, currencyCode)}</span>
+          </div>
+          <div className="flex items-center justify-between text-sm">
+            <span className="text-muted-foreground">Labor</span>
+            <span>{formatCurrency(laborSubtotal, currencyCode)}</span>
+          </div>
+          <div className="flex items-center justify-between text-sm">
+            <span className="text-muted-foreground">Subtotal</span>
+            <span className="font-medium">{formatCurrency(subtotal, currencyCode)}</span>
+          </div>
+          <div className="flex items-center justify-between text-sm">
+            <div className="flex items-center gap-2">
+              <span className="text-muted-foreground">Discount</span>
+              <Select value={discountType} onValueChange={setDiscountType}>
+                <SelectTrigger className="h-7 w-28 text-xs"><SelectValue /></SelectTrigger>
+                <SelectContent>
+                  <SelectItem value="none">None</SelectItem>
+                  <SelectItem value="percentage">Percentage</SelectItem>
+                  <SelectItem value="fixed">Fixed</SelectItem>
+                </SelectContent>
+              </Select>
+              {discountType !== "none" && (
+                <Input
+                  type="number" min="0" step="0.01"
+                  value={discountValue}
+                  onChange={(e) => setDiscountValue(Number(e.target.value))}
+                  className="h-7 w-20 text-right text-xs"
+                />
+              )}
+              {discountType === "percentage" && <span className="text-muted-foreground">%</span>}
+            </div>
+            {discountAmount > 0 && (
+              <span className="text-destructive">{formatCurrency(-discountAmount, currencyCode)}</span>
             )}
             )}
-          </CardContent>
-        </Card>
+          </div>
+          {taxEnabled && (
+            <div className="flex items-center justify-between text-sm">
+              <div className="flex items-center gap-2">
+                <span className="text-muted-foreground">Tax</span>
+                <Input
+                  type="number" min="0" step="0.1"
+                  value={taxRate}
+                  onChange={(e) => setTaxRate(Number(e.target.value))}
+                  className="h-7 w-20 text-right text-xs"
+                />
+                <span className="text-muted-foreground">%</span>
+              </div>
+              <span>{formatCurrency(taxAmount, currencyCode)}</span>
+            </div>
+          )}
+          <div className="flex items-center justify-between border-t pt-2 text-lg font-bold">
+            <span>Total</span>
+            <span>{formatCurrency(totalAmount, currencyCode)}</span>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
 
 
-        <div className="flex justify-end gap-3 pb-8">
-          <Button type="button" variant="outline" asChild>
+  return (
+    <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col overflow-hidden">
+      {/* Header */}
+      <div className="flex items-center justify-between border-b px-4 py-2">
+        <div className="flex items-center gap-3">
+          <Link
+            href="/quotes"
+            className="inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
+          >
+            <ArrowLeft className="h-4 w-4" />
+          </Link>
+          <h1 className="text-lg font-semibold">{isEdit ? "Edit Quote" : "New Quote"}</h1>
+        </div>
+        <div className="flex items-center gap-2">
+          <Button type="button" variant="outline" size="sm" asChild>
             <Link href="/quotes">Cancel</Link>
             <Link href="/quotes">Cancel</Link>
           </Button>
           </Button>
-          <Button type="submit" disabled={loading}>
-            {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
-            {isEdit ? "Update Quote" : "Create Quote"}
+          <Button type="submit" size="sm" disabled={loading}>
+            {loading ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Save className="mr-1 h-3.5 w-3.5" />}
+            {isEdit ? "Update" : "Create"} Quote
           </Button>
           </Button>
         </div>
         </div>
-      </form>
-    </div>
+      </div>
+
+      {/* Content */}
+      {isLarge ? (
+        <ResizablePanelGroup orientation="horizontal" className="flex-1 overflow-hidden">
+          <ResizablePanel defaultSize={75} minSize={40}>
+            <div className="h-full overflow-y-auto overscroll-contain p-4 pr-2">
+              <div className="space-y-3 pb-40">{leftColumn}</div>
+            </div>
+          </ResizablePanel>
+          <ResizableHandle withHandle />
+          <ResizablePanel defaultSize={25} minSize={15}>
+            <div className="h-full overflow-y-auto overscroll-contain p-4 pl-2">
+              <div className="space-y-3 pb-40">{rightColumn}</div>
+            </div>
+          </ResizablePanel>
+        </ResizablePanelGroup>
+      ) : (
+        <div className="flex-1 overflow-y-auto overscroll-contain p-4">
+          <div className="space-y-3 pb-40">
+            {leftColumn}
+            {rightColumn}
+          </div>
+        </div>
+      )}
+    </form>
   );
   );
 }
 }

+ 653 - 0
src/features/quotes/Components/QuotePageClient.tsx

@@ -0,0 +1,653 @@
+"use client";
+
+import { useState, useCallback, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import Link from "next/link";
+import { toast } from "sonner";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { ButtonGroup } from "@/components/ui/button-group";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+  Select,
+  SelectContent,
+  SelectItem,
+  SelectTrigger,
+  SelectValue,
+} from "@/components/ui/select";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import {
+  ResizablePanelGroup,
+  ResizablePanel,
+  ResizableHandle,
+} from "@/components/ui/resizable";
+import { useGlassModal } from "@/components/glass-modal";
+import { useConfirm } from "@/components/confirm-dialog";
+import { updateQuote, deleteQuote, convertQuoteToServiceRecord } from "@/features/quotes/Actions/quoteActions";
+import { sendQuoteEmail } from "@/features/email/Actions/emailActions";
+import { SendEmailDialog } from "@/features/email/Components/SendEmailDialog";
+import { QuoteShareDialog } from "@/features/quotes/Components/QuoteShareDialog";
+import { RichTextEditor } from "@/features/vehicles/Components/service-edit/RichTextEditor";
+import type { QuotePartInput, QuoteLaborInput } from "@/features/quotes/Schema/quoteSchema";
+import {
+  ArrowLeft,
+  ArrowRight,
+  Car,
+  Download,
+  FileText,
+  Globe,
+  Loader2,
+  Mail,
+  Plus,
+  Save,
+  Trash2,
+  Users,
+  X,
+} from "lucide-react";
+import { formatCurrency, getCurrencySymbol } from "@/lib/format";
+
+const statusColors: Record<string, string> = {
+  draft: "bg-gray-500/10 text-gray-500 border-gray-500/20",
+  sent: "bg-blue-500/10 text-blue-500 border-blue-500/20",
+  accepted: "bg-emerald-500/10 text-emerald-600 border-emerald-500/20",
+  rejected: "bg-red-500/10 text-red-500 border-red-500/20",
+  expired: "bg-amber-500/10 text-amber-600 border-amber-500/20",
+  converted: "bg-purple-500/10 text-purple-500 border-purple-500/20",
+};
+
+interface CustomerOption {
+  id: string;
+  name: string;
+  email: string | null;
+  company: string | null;
+}
+
+interface VehicleOption {
+  id: string;
+  make: string;
+  model: string;
+  year: number;
+  licensePlate: string | null;
+  customerId: string | null;
+  customerName: string | null;
+}
+
+interface QuoteRecord {
+  id: string;
+  quoteNumber: string | null;
+  title: string;
+  description: string | null;
+  status: string;
+  validUntil: Date | null;
+  subtotal: number;
+  taxRate: number;
+  taxAmount: number;
+  discountType: string | null;
+  discountValue: number;
+  discountAmount: number;
+  totalAmount: number;
+  notes: string | null;
+  publicToken: string | null;
+  convertedToId: string | null;
+  createdAt: Date;
+  partItems: { id: string; partNumber: string | null; name: string; quantity: number; unitPrice: number; total: number }[];
+  laborItems: { id: string; description: string; hours: number; rate: number; total: number }[];
+  customer: { id: string; name: string; email: string | null; phone: string | null; address: string | null; company: string | null } | null;
+  vehicle: { id: string; make: string; model: string; year: number; vin: string | null; licensePlate: string | null; mileage: number } | null;
+}
+
+const emptyPart = (): QuotePartInput => ({
+  partNumber: "",
+  name: "",
+  quantity: 1,
+  unitPrice: 0,
+  total: 0,
+});
+
+const makeEmptyLabor = (defaultRate: number): QuoteLaborInput => ({
+  description: "",
+  hours: 0,
+  rate: defaultRate,
+  total: 0,
+});
+
+const LG_BREAKPOINT = 1024;
+
+function useIsLargeScreen() {
+  const [isLarge, setIsLarge] = useState(false);
+  useEffect(() => {
+    const mql = window.matchMedia(`(min-width: ${LG_BREAKPOINT}px)`);
+    const onChange = () => setIsLarge(mql.matches);
+    mql.addEventListener("change", onChange);
+    setIsLarge(mql.matches);
+    return () => mql.removeEventListener("change", onChange);
+  }, []);
+  return isLarge;
+}
+
+export function QuotePageClient({
+  quote,
+  organizationId,
+  currencyCode = "USD",
+  defaultTaxRate = 0,
+  taxEnabled = true,
+  defaultLaborRate = 0,
+  customers = [],
+  vehicles = [],
+}: {
+  quote: QuoteRecord;
+  organizationId: string;
+  currencyCode?: string;
+  defaultTaxRate?: number;
+  taxEnabled?: boolean;
+  defaultLaborRate?: number;
+  customers?: CustomerOption[];
+  vehicles?: VehicleOption[];
+}) {
+  const cs = getCurrencySymbol(currencyCode);
+  const router = useRouter();
+  const modal = useGlassModal();
+  const confirm = useConfirm();
+  const isLarge = useIsLargeScreen();
+
+  // Form state
+  const [saving, setSaving] = useState(false);
+  const [downloading, setDownloading] = useState(false);
+  const [status, setStatus] = useState(quote.status);
+  const [customerId, setCustomerId] = useState(quote.customer?.id || "");
+  const [vehicleId, setVehicleId] = useState(quote.vehicle?.id || "");
+  const [partItems, setPartItems] = useState<QuotePartInput[]>(
+    quote.partItems.map((p) => ({ partNumber: p.partNumber || "", name: p.name, quantity: p.quantity, unitPrice: p.unitPrice, total: p.total }))
+  );
+  const [laborItems, setLaborItems] = useState<QuoteLaborInput[]>(
+    quote.laborItems.map((l) => ({ description: l.description, hours: l.hours, rate: l.rate, total: l.total }))
+  );
+  const [taxRate, setTaxRate] = useState(quote.taxRate ?? defaultTaxRate);
+  const [discountType, setDiscountType] = useState<string>(quote.discountType || "none");
+  const [discountValue, setDiscountValue] = useState(quote.discountValue ?? 0);
+  const [noteType, setNoteType] = useState<"public" | "internal">("public");
+  const [description, setDescription] = useState(quote.description || "");
+  const [notes, setNotes] = useState(quote.notes || "");
+
+  // Dialog state
+  const [showEmailDialog, setShowEmailDialog] = useState(false);
+  const [showShareDialog, setShowShareDialog] = useState(false);
+  const [showConvertDialog, setShowConvertDialog] = useState(false);
+  const [convertVehicleId, setConvertVehicleId] = useState(quote.vehicle?.id || "");
+  const [converting, setConverting] = useState(false);
+
+  const [defaultValidDate] = useState(() =>
+    quote.validUntil ? new Date(quote.validUntil).toISOString().split("T")[0] : ""
+  );
+
+  // Calculations
+  const partsSubtotal = partItems.reduce((sum, p) => sum + p.total, 0);
+  const laborSubtotal = laborItems.reduce((sum, l) => sum + l.total, 0);
+  const subtotal = partsSubtotal + laborSubtotal;
+  const discountAmount = discountType === "percentage"
+    ? subtotal * (discountValue / 100)
+    : discountType === "fixed"
+    ? Math.min(discountValue, subtotal)
+    : 0;
+  const taxAmount = (subtotal - discountAmount) * (taxRate / 100);
+  const totalAmount = subtotal - discountAmount + taxAmount;
+
+  const selectedVehicle = vehicles.find((v) => v.id === vehicleId);
+  const selectedCustomer = customers.find((c) => c.id === customerId);
+
+  const handleVehicleChange = (v: string) => {
+    const vid = v === "none" ? "" : v;
+    setVehicleId(vid);
+    if (vid) {
+      const vehicle = vehicles.find((veh) => veh.id === vid);
+      if (vehicle?.customerId) setCustomerId(vehicle.customerId);
+    }
+  };
+
+  const updatePart = useCallback((index: number, field: keyof QuotePartInput, value: string | number) => {
+    setPartItems((prev) => {
+      const updated = [...prev];
+      const part = { ...updated[index], [field]: value };
+      if (field === "quantity" || field === "unitPrice") part.total = Number(part.quantity) * Number(part.unitPrice);
+      updated[index] = part;
+      return updated;
+    });
+  }, []);
+
+  const updateLabor = useCallback((index: number, field: keyof QuoteLaborInput, value: string | number) => {
+    setLaborItems((prev) => {
+      const updated = [...prev];
+      const labor = { ...updated[index], [field]: value };
+      if (field === "hours" || field === "rate") labor.total = Number(labor.hours) * Number(labor.rate);
+      updated[index] = labor;
+      return updated;
+    });
+  }, []);
+
+  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    setSaving(true);
+    const formData = new FormData(e.currentTarget);
+    const result = await updateQuote({
+      id: quote.id,
+      title: formData.get("title") as string,
+      description: description || undefined,
+      status,
+      validUntil: (formData.get("validUntil") as string) || undefined,
+      customerId: customerId || undefined,
+      vehicleId: vehicleId || undefined,
+      notes: notes || undefined,
+      partItems: partItems.filter((p) => p.name),
+      laborItems: laborItems.filter((l) => l.description),
+      subtotal,
+      taxRate,
+      taxAmount,
+      discountType: discountType === "none" ? undefined : discountType,
+      discountValue,
+      discountAmount,
+      totalAmount,
+    });
+    if (result.success) {
+      toast.success("Quote saved");
+      router.refresh();
+    } else {
+      modal.open("error", "Error", result.error || "Failed to save quote");
+    }
+    setSaving(false);
+  };
+
+  const handleDelete = async () => {
+    const ok = await confirm({ title: "Delete Quote", description: "This will permanently delete this quote.", confirmLabel: "Delete", destructive: true });
+    if (!ok) return;
+    const result = await deleteQuote(quote.id);
+    if (result.success) {
+      router.push("/quotes");
+      router.refresh();
+    } else {
+      modal.open("error", "Error", result.error || "Failed to delete");
+    }
+  };
+
+  const handleDownloadPDF = async () => {
+    setDownloading(true);
+    try {
+      const res = await fetch(`/api/quotes/${quote.id}/pdf`);
+      if (!res.ok) throw new Error("Failed to generate PDF");
+      const blob = await res.blob();
+      const url = URL.createObjectURL(blob);
+      const a = document.createElement("a");
+      a.href = url;
+      a.download = `quote-${quote.quoteNumber || quote.id}.pdf`;
+      a.click();
+      URL.revokeObjectURL(url);
+    } catch {
+      modal.open("error", "Error", "Failed to generate PDF");
+    }
+    setDownloading(false);
+  };
+
+  const handleConvert = async () => {
+    if (!convertVehicleId) {
+      modal.open("error", "Error", "Please select a vehicle");
+      return;
+    }
+    setConverting(true);
+    const result = await convertQuoteToServiceRecord(quote.id, convertVehicleId);
+    if (result.success && result.data) {
+      router.push(`/vehicles/${convertVehicleId}/service/${result.data.id}`);
+      router.refresh();
+    } else {
+      modal.open("error", "Error", result.error || "Failed to convert");
+    }
+    setConverting(false);
+  };
+
+  // --- Left column: Parts, Labor, Notes ---
+  const leftColumn = (
+    <div className="space-y-3">
+      {/* Parts */}
+      <div className="rounded-lg border p-3 space-y-2">
+        <div className="flex items-center justify-between">
+          <h3 className="text-sm font-semibold">Parts</h3>
+          <Button type="button" variant="outline" size="sm" onClick={() => setPartItems([...partItems, emptyPart()])}>
+            <Plus className="mr-1 h-3.5 w-3.5" /> Add Part
+          </Button>
+        </div>
+        {partItems.length > 0 && (
+          <>
+            <div className="hidden grid-cols-[1fr_2fr_0.7fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
+              <span>Part #</span><span>Name</span><span>Qty</span><span>Unit Price</span><span>Total</span><span />
+            </div>
+            {partItems.map((part, i) => (
+              <div key={i} className="grid grid-cols-2 gap-2 sm:grid-cols-[1fr_2fr_0.7fr_1fr_1fr_auto]">
+                <Input placeholder="Part #" value={part.partNumber ?? ""} onChange={(e) => updatePart(i, "partNumber", e.target.value)} />
+                <Input placeholder="Name *" value={part.name} onChange={(e) => updatePart(i, "name", e.target.value)} />
+                <Input type="number" min="0" step="1" value={part.quantity} onChange={(e) => updatePart(i, "quantity", Number(e.target.value))} />
+                <Input type="number" min="0" step="0.01" value={part.unitPrice} onChange={(e) => updatePart(i, "unitPrice", Number(e.target.value))} />
+                <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">{formatCurrency(part.total, currencyCode)}</div>
+                <Button type="button" variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground hover:text-destructive" onClick={() => setPartItems(partItems.filter((_, j) => j !== i))}><Trash2 className="h-4 w-4" /></Button>
+              </div>
+            ))}
+            <button type="button" className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={() => setPartItems([...partItems, emptyPart()])}><Plus className="h-4 w-4" /></button>
+            <div className="flex justify-end pt-1 text-sm"><span className="font-medium">Parts Subtotal: {formatCurrency(partsSubtotal, currencyCode)}</span></div>
+          </>
+        )}
+        {partItems.length === 0 && (
+          <button type="button" className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={() => setPartItems([...partItems, emptyPart()])}>
+            <Plus className="mr-1 h-4 w-4" /><span className="text-sm">Add Part</span>
+          </button>
+        )}
+      </div>
+
+      {/* Labor */}
+      <div className="rounded-lg border p-3 space-y-2">
+        <div className="flex items-center justify-between">
+          <h3 className="text-sm font-semibold">Labor</h3>
+          <Button type="button" variant="outline" size="sm" onClick={() => setLaborItems([...laborItems, makeEmptyLabor(defaultLaborRate)])}><Plus className="mr-1 h-3.5 w-3.5" /> Add Labor</Button>
+        </div>
+        {laborItems.length > 0 && (
+          <>
+            <div className="hidden grid-cols-[2fr_1fr_1fr_1fr_auto] gap-2 text-xs font-medium text-muted-foreground sm:grid">
+              <span>Description</span><span>Hours</span><span>Rate ({cs}/hr)</span><span>Total</span><span />
+            </div>
+            {laborItems.map((labor, i) => (
+              <div key={i} className="grid grid-cols-2 gap-2 sm:grid-cols-[2fr_1fr_1fr_1fr_auto]">
+                <Input placeholder="Description *" value={labor.description} onChange={(e) => updateLabor(i, "description", e.target.value)} className="col-span-2 sm:col-span-1" />
+                <Input type="number" min="0" step="0.5" value={labor.hours} onChange={(e) => updateLabor(i, "hours", Number(e.target.value))} />
+                <Input type="number" min="0" step="0.01" value={labor.rate} onChange={(e) => updateLabor(i, "rate", Number(e.target.value))} />
+                <div className="flex items-center rounded-md bg-muted/50 px-3 text-sm font-medium">{formatCurrency(labor.total, currencyCode)}</div>
+                <Button type="button" variant="ghost" size="icon" className="h-9 w-9 text-muted-foreground hover:text-destructive" onClick={() => setLaborItems(laborItems.filter((_, j) => j !== i))}><Trash2 className="h-4 w-4" /></Button>
+              </div>
+            ))}
+            <button type="button" className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={() => setLaborItems([...laborItems, makeEmptyLabor(defaultLaborRate)])}><Plus className="h-4 w-4" /></button>
+            <div className="flex justify-end pt-1 text-sm"><span className="font-medium">Labor Subtotal: {formatCurrency(laborSubtotal, currencyCode)}</span></div>
+          </>
+        )}
+        {laborItems.length === 0 && (
+          <button type="button" className="flex w-full items-center justify-center rounded-md border border-dashed border-muted-foreground/25 py-1.5 text-muted-foreground transition-colors hover:border-muted-foreground/50 hover:text-foreground" onClick={() => setLaborItems([...laborItems, makeEmptyLabor(defaultLaborRate)])}>
+            <Plus className="mr-1 h-4 w-4" /><span className="text-sm">Add Labor</span>
+          </button>
+        )}
+      </div>
+
+      {/* Notes */}
+      <div className="rounded-lg border p-3 space-y-3">
+        <div className="flex items-center justify-between">
+          <h3 className="flex items-center gap-2 text-sm font-semibold"><FileText className="h-3.5 w-3.5" />Notes</h3>
+          <Select value={noteType} onValueChange={(v) => setNoteType(v as "public" | "internal")}>
+            <SelectTrigger className="h-7 w-[120px] text-xs"><SelectValue /></SelectTrigger>
+            <SelectContent>
+              <SelectItem value="public">Public</SelectItem>
+              <SelectItem value="internal">Internal</SelectItem>
+            </SelectContent>
+          </Select>
+        </div>
+        {noteType === "public" && (
+          <div className="space-y-1">
+            <RichTextEditor content={description} onChange={setDescription} placeholder="Quote description shown to the customer..." />
+            <p className="text-xs text-muted-foreground">Shown on the quote document</p>
+          </div>
+        )}
+        {noteType === "internal" && (
+          <div className="space-y-1">
+            <RichTextEditor content={notes} onChange={setNotes} placeholder="Internal notes (not shown to customer)..." />
+            <p className="text-xs text-muted-foreground">Only visible to your team</p>
+          </div>
+        )}
+      </div>
+    </div>
+  );
+
+  // --- Right column: Vehicle & Customer, Quote Details, Totals ---
+  const rightColumn = (
+    <div className="space-y-3">
+      {/* Vehicle & Customer */}
+      <div className="rounded-lg border p-3 space-y-3">
+        <div className="space-y-1">
+          <Label className="text-xs">Vehicle</Label>
+          <Select value={vehicleId || "none"} onValueChange={handleVehicleChange}>
+            <SelectTrigger><SelectValue placeholder="Select vehicle..." /></SelectTrigger>
+            <SelectContent>
+              <SelectItem value="none">None</SelectItem>
+              {vehicles.map((v) => (
+                <SelectItem key={v.id} value={v.id}>{v.year} {v.make} {v.model}{v.licensePlate ? ` (${v.licensePlate})` : ""}</SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+        </div>
+        {selectedVehicle && (
+          <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
+            <Car className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
+            <div className="min-w-0 flex-1 text-sm">
+              <span className="font-medium">{selectedVehicle.year} {selectedVehicle.make} {selectedVehicle.model}</span>
+              {selectedVehicle.licensePlate && <span className="ml-1.5 text-muted-foreground">{selectedVehicle.licensePlate}</span>}
+            </div>
+            <Button type="button" variant="ghost" size="icon" className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground" onClick={() => setVehicleId("")}><X className="h-3 w-3" /></Button>
+          </div>
+        )}
+        <div className="space-y-1">
+          <Label className="text-xs">Customer</Label>
+          <Select value={customerId || "none"} onValueChange={(v) => setCustomerId(v === "none" ? "" : v)}>
+            <SelectTrigger><SelectValue placeholder="Select customer..." /></SelectTrigger>
+            <SelectContent>
+              <SelectItem value="none">None</SelectItem>
+              {customers.map((c) => (
+                <SelectItem key={c.id} value={c.id}>{c.name}{c.company ? ` (${c.company})` : ""}</SelectItem>
+              ))}
+            </SelectContent>
+          </Select>
+        </div>
+        {selectedCustomer && (
+          <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
+            <Users className="h-3.5 w-3.5 shrink-0 text-muted-foreground" />
+            <div className="min-w-0 flex-1 text-sm">
+              <span className="font-medium">{selectedCustomer.name}</span>
+              {selectedCustomer.company && <span className="ml-1.5 text-muted-foreground">{selectedCustomer.company}</span>}
+            </div>
+            <Button type="button" variant="ghost" size="icon" className="h-6 w-6 shrink-0 text-muted-foreground hover:text-foreground" onClick={() => setCustomerId("")}><X className="h-3 w-3" /></Button>
+          </div>
+        )}
+      </div>
+
+      {/* Quote Details */}
+      <div className="rounded-lg border p-3 space-y-3">
+        <h3 className="text-sm font-semibold">Quote Details</h3>
+        <div className="space-y-1">
+          <Label htmlFor="title" className="text-xs">Title *</Label>
+          <Input id="title" name="title" placeholder="Vehicle repair estimate" defaultValue={quote.title} required />
+        </div>
+        <div className="grid grid-cols-2 gap-2">
+          <div className="space-y-1">
+            <Label className="text-xs">Status</Label>
+            <Select value={status} onValueChange={setStatus}>
+              <SelectTrigger><SelectValue /></SelectTrigger>
+              <SelectContent>
+                <SelectItem value="draft">Draft</SelectItem>
+                <SelectItem value="sent">Sent</SelectItem>
+                <SelectItem value="accepted">Accepted</SelectItem>
+                <SelectItem value="rejected">Rejected</SelectItem>
+              </SelectContent>
+            </Select>
+          </div>
+          <div className="space-y-1">
+            <Label htmlFor="validUntil" className="text-xs">Valid Until</Label>
+            <Input id="validUntil" name="validUntil" type="date" defaultValue={defaultValidDate} />
+          </div>
+        </div>
+      </div>
+
+      {/* Totals */}
+      <div className="rounded-lg border p-3 space-y-2">
+        <h3 className="text-sm font-semibold">Totals</h3>
+        <div className="space-y-2">
+          <div className="flex items-center justify-between text-sm"><span className="text-muted-foreground">Parts</span><span>{formatCurrency(partsSubtotal, currencyCode)}</span></div>
+          <div className="flex items-center justify-between text-sm"><span className="text-muted-foreground">Labor</span><span>{formatCurrency(laborSubtotal, currencyCode)}</span></div>
+          <div className="flex items-center justify-between text-sm"><span className="text-muted-foreground">Subtotal</span><span className="font-medium">{formatCurrency(subtotal, currencyCode)}</span></div>
+          <div className="flex items-center justify-between text-sm">
+            <div className="flex items-center gap-2">
+              <span className="text-muted-foreground">Discount</span>
+              <Select value={discountType} onValueChange={setDiscountType}>
+                <SelectTrigger className="h-7 w-28 text-xs"><SelectValue /></SelectTrigger>
+                <SelectContent>
+                  <SelectItem value="none">None</SelectItem>
+                  <SelectItem value="percentage">Percentage</SelectItem>
+                  <SelectItem value="fixed">Fixed</SelectItem>
+                </SelectContent>
+              </Select>
+              {discountType !== "none" && (
+                <Input type="number" min="0" step="0.01" value={discountValue} onChange={(e) => setDiscountValue(Number(e.target.value))} className="h-7 w-20 text-right text-xs" />
+              )}
+              {discountType === "percentage" && <span className="text-muted-foreground">%</span>}
+            </div>
+            {discountAmount > 0 && <span className="text-destructive">{formatCurrency(-discountAmount, currencyCode)}</span>}
+          </div>
+          {taxEnabled && (
+            <div className="flex items-center justify-between text-sm">
+              <div className="flex items-center gap-2">
+                <span className="text-muted-foreground">Tax</span>
+                <Input type="number" min="0" step="0.1" value={taxRate} onChange={(e) => setTaxRate(Number(e.target.value))} className="h-7 w-20 text-right text-xs" />
+                <span className="text-muted-foreground">%</span>
+              </div>
+              <span>{formatCurrency(taxAmount, currencyCode)}</span>
+            </div>
+          )}
+          <div className="flex items-center justify-between border-t pt-2 text-lg font-bold"><span>Total</span><span>{formatCurrency(totalAmount, currencyCode)}</span></div>
+        </div>
+      </div>
+
+      {/* Convert to Work Order */}
+      {quote.status !== "converted" && (
+        <div className="rounded-lg border p-3">
+          <Button type="button" variant="outline" size="sm" className="w-full" onClick={() => setShowConvertDialog(true)}>
+            <ArrowRight className="mr-1 h-3.5 w-3.5" /> Convert to Work Order
+          </Button>
+        </div>
+      )}
+    </div>
+  );
+
+  return (
+    <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
+      {/* Header */}
+      <div className="shrink-0 border-b bg-background px-4 py-2">
+        <div className="flex items-center justify-between">
+          <Link
+            href="/quotes"
+            className="flex min-w-0 items-center gap-3 text-foreground transition-colors hover:text-muted-foreground"
+          >
+            <ArrowLeft className="h-4 w-4 shrink-0" />
+            <div className="min-w-0">
+              <div className="flex items-center gap-2">
+                <Badge variant="outline" className={`shrink-0 text-xs capitalize ${statusColors[status] || ""}`}>
+                  {status}
+                </Badge>
+                <h1 className="truncate text-lg font-semibold leading-tight">{quote.title}</h1>
+              </div>
+              <p className="truncate text-xs text-muted-foreground">
+                {quote.quoteNumber || `Quote`}
+                {quote.customer ? ` · ${quote.customer.name}` : ""}
+              </p>
+            </div>
+          </Link>
+          <div className="flex shrink-0 items-center gap-2">
+            <Button type="submit" form="quote-form" size="sm" disabled={saving}>
+              {saving ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Save className="mr-1 h-3.5 w-3.5" />}
+              Save
+            </Button>
+            <ButtonGroup>
+              <Button variant="outline" size="sm" onClick={handleDownloadPDF} disabled={downloading}>
+                {downloading ? <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" /> : <Download className="mr-1 h-3.5 w-3.5" />}
+                PDF
+              </Button>
+              <Button variant="outline" size="sm" onClick={() => setShowEmailDialog(true)}>
+                <Mail className="mr-1 h-3.5 w-3.5" />
+                Email
+              </Button>
+              <Button variant="outline" size="sm" onClick={() => setShowShareDialog(true)}>
+                <Globe className="mr-1 h-3.5 w-3.5" />
+                Share
+              </Button>
+              <Button variant="outline" size="sm" className="text-destructive hover:bg-destructive/10" onClick={handleDelete}>
+                <Trash2 className="mr-1 h-3.5 w-3.5" />
+                Delete
+              </Button>
+            </ButtonGroup>
+          </div>
+        </div>
+      </div>
+
+      {/* Form */}
+      <form id="quote-form" onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col overflow-hidden">
+        {isLarge ? (
+          <ResizablePanelGroup orientation="horizontal" className="flex-1 overflow-hidden">
+            <ResizablePanel defaultSize={75} minSize={40}>
+              <div className="h-full overflow-y-auto overscroll-contain p-4 pr-2">
+                <div className="space-y-3 pb-40">{leftColumn}</div>
+              </div>
+            </ResizablePanel>
+            <ResizableHandle withHandle />
+            <ResizablePanel defaultSize={25} minSize={15}>
+              <div className="h-full overflow-y-auto overscroll-contain p-4 pl-2">
+                <div className="space-y-3 pb-40">{rightColumn}</div>
+              </div>
+            </ResizablePanel>
+          </ResizablePanelGroup>
+        ) : (
+          <div className="flex-1 overflow-y-auto overscroll-contain p-4">
+            <div className="space-y-3 pb-40">
+              {leftColumn}
+              {rightColumn}
+            </div>
+          </div>
+        )}
+      </form>
+
+      {/* Dialogs */}
+      <SendEmailDialog
+        open={showEmailDialog}
+        onOpenChange={setShowEmailDialog}
+        defaultEmail={quote.customer?.email || ""}
+        entityLabel="Quote"
+        onSend={async (email, message) => sendQuoteEmail({ quoteId: quote.id, recipientEmail: email, message })}
+      />
+
+      <QuoteShareDialog
+        open={showShareDialog}
+        onOpenChange={setShowShareDialog}
+        quoteId={quote.id}
+        organizationId={organizationId}
+        initialToken={quote.publicToken}
+      />
+
+      <Dialog open={showConvertDialog} onOpenChange={setShowConvertDialog}>
+        <DialogContent className="max-w-md">
+          <DialogHeader><DialogTitle>Convert Quote to Work Order</DialogTitle></DialogHeader>
+          <div className="space-y-4">
+            <p className="text-sm text-muted-foreground">This will create a new service record (work order) from this quote. Select a vehicle for the work order.</p>
+            <Select value={convertVehicleId} onValueChange={setConvertVehicleId}>
+              <SelectTrigger><SelectValue placeholder="Select vehicle..." /></SelectTrigger>
+              <SelectContent>
+                {vehicles.map((v) => (
+                  <SelectItem key={v.id} value={v.id}>{v.year} {v.make} {v.model}{v.licensePlate ? ` (${v.licensePlate})` : ""}</SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+            <div className="flex gap-2">
+              <Button type="button" onClick={handleConvert} disabled={converting || !convertVehicleId}>
+                {converting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                Convert
+              </Button>
+              <Button type="button" variant="ghost" onClick={() => setShowConvertDialog(false)}>Cancel</Button>
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+}

+ 98 - 0
src/features/quotes/Components/QuoteShareDialog.tsx

@@ -0,0 +1,98 @@
+"use client";
+
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import {
+  Dialog,
+  DialogContent,
+  DialogHeader,
+  DialogTitle,
+} from "@/components/ui/dialog";
+import { Check, Copy, Link2, Loader2 } from "lucide-react";
+import { generateQuotePublicLink, revokeQuotePublicLink } from "@/features/quotes/Actions/quoteShareActions";
+
+interface QuoteShareDialogProps {
+  open: boolean;
+  onOpenChange: (open: boolean) => void;
+  quoteId: string;
+  organizationId: string;
+  initialToken: string | null;
+}
+
+export function QuoteShareDialog({
+  open,
+  onOpenChange,
+  quoteId,
+  organizationId,
+  initialToken,
+}: QuoteShareDialogProps) {
+  const [publicToken, setPublicToken] = useState<string | null>(initialToken);
+  const [generatingLink, setGeneratingLink] = useState(false);
+  const [copied, setCopied] = useState(false);
+
+  const publicUrl = publicToken && organizationId
+    ? `${typeof window !== "undefined" ? window.location.origin : ""}/share/quote/${organizationId}/${publicToken}`
+    : null;
+
+  const handleGenerate = async () => {
+    setGeneratingLink(true);
+    const result = await generateQuotePublicLink(quoteId);
+    if (result.success && result.data) setPublicToken(result.data.token);
+    setGeneratingLink(false);
+  };
+
+  const handleRevoke = async () => {
+    await revokeQuotePublicLink(quoteId);
+    setPublicToken(null);
+  };
+
+  const handleCopy = () => {
+    if (publicUrl) {
+      navigator.clipboard.writeText(publicUrl);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    }
+  };
+
+  return (
+    <Dialog open={open} onOpenChange={onOpenChange}>
+      <DialogContent className="max-w-md">
+        <DialogHeader>
+          <DialogTitle className="flex items-center gap-2">
+            <Link2 className="h-5 w-5" />
+            Share Quote
+          </DialogTitle>
+        </DialogHeader>
+        <div className="space-y-4">
+          <p className="text-sm text-muted-foreground">
+            Generate a public link that allows anyone to view and download this quote without signing in.
+          </p>
+          {publicUrl ? (
+            <>
+              <div className="flex items-center gap-2">
+                <Input value={publicUrl} readOnly className="text-xs font-mono" />
+                <Button variant="outline" size="icon" onClick={handleCopy}>
+                  {copied ? <Check className="h-4 w-4 text-emerald-500" /> : <Copy className="h-4 w-4" />}
+                </Button>
+              </div>
+              <Button
+                variant="ghost"
+                size="sm"
+                className="text-destructive hover:text-destructive"
+                onClick={handleRevoke}
+              >
+                Revoke Link
+              </Button>
+            </>
+          ) : (
+            <Button onClick={handleGenerate} disabled={generatingLink}>
+              {generatingLink && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+              Generate Public Link
+            </Button>
+          )}
+        </div>
+      </DialogContent>
+    </Dialog>
+  );
+}