Bernt Christian Egeland пре 7 месеци
родитељ
комит
0622fcb227

+ 2 - 0
prisma/migrations/20260219121039_sell_price/migration.sql

@@ -0,0 +1,2 @@
+-- AlterTable
+ALTER TABLE "inventory_parts" ADD COLUMN     "sellPrice" DOUBLE PRECISION NOT NULL DEFAULT 0;

+ 1 - 0
prisma/schema.prisma

@@ -339,6 +339,7 @@ model InventoryPart {
   quantity      Int      @default(0)
   minQuantity   Int      @default(0)
   unitCost      Float    @default(0)
+  sellPrice     Float    @default(0)
   supplier      String?
   supplierPhone String?
   supplierEmail String?

+ 108 - 6
src/app/(authenticated)/inventory/inventory-client.tsx

@@ -30,12 +30,22 @@ import { DataTablePagination } from "@/components/data-table-pagination";
 import { useGlassModal } from "@/components/glass-modal";
 import { useConfirm } from "@/components/confirm-dialog";
 import { InventoryPartForm } from "@/features/inventory/Components/InventoryPartForm";
-import { deleteInventoryPart } from "@/features/inventory/Actions/inventoryActions";
+import { deleteInventoryPart, applyMarkupToAll } from "@/features/inventory/Actions/inventoryActions";
+import { setSetting } from "@/features/settings/Actions/settingsActions";
+import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
+import {
+  Dialog as MarkupDialog,
+  DialogContent as MarkupDialogContent,
+  DialogHeader as MarkupDialogHeader,
+  DialogTitle as MarkupDialogTitle,
+} from "@/components/ui/dialog";
+import { Label } from "@/components/ui/label";
 import {
   ExternalLink,
   Loader2,
   MoreVertical,
   Pencil,
+  Percent,
   Plus,
   Search,
   Trash2,
@@ -51,6 +61,7 @@ interface InventoryPart {
   quantity: number;
   minQuantity: number;
   unitCost: number;
+  sellPrice: number;
   supplier: string | null;
   supplierPhone: string | null;
   supplierEmail: string | null;
@@ -73,12 +84,14 @@ export function InventoryClient({
   category,
   categories,
   currencyCode = "USD",
+  markupMultiplier: initialMarkup = 1.0,
 }: {
   data: PaginatedData;
   search: string;
   category: string;
   categories: string[];
   currencyCode?: string;
+  markupMultiplier?: number;
 }) {
   const router = useRouter();
   const pathname = usePathname();
@@ -87,6 +100,9 @@ export function InventoryClient({
   const [searchInput, setSearchInput] = useState(search);
   const [showForm, setShowForm] = useState(false);
   const [editPart, setEditPart] = useState<InventoryPart | null>(null);
+  const [showMarkup, setShowMarkup] = useState(false);
+  const [markupValue, setMarkupValue] = useState(String(initialMarkup));
+  const [applyingMarkup, setApplyingMarkup] = useState(false);
   const modal = useGlassModal();
   const confirm = useConfirm();
 
@@ -134,6 +150,31 @@ export function InventoryClient({
     }
   };
 
+  const handleApplyMarkup = async () => {
+    const multiplier = Number(markupValue);
+    if (!multiplier || multiplier <= 0) {
+      modal.open("error", "Error", "Multiplier must be greater than 0");
+      return;
+    }
+    setApplyingMarkup(true);
+    try {
+      const [result] = await Promise.all([
+        applyMarkupToAll({ multiplier }),
+        setSetting(SETTING_KEYS.INVENTORY_MARKUP_MULTIPLIER, String(multiplier)),
+      ]);
+      if (result.success) {
+        setShowMarkup(false);
+        router.refresh();
+      } else {
+        modal.open("error", "Error", result.error || "Failed to apply markup");
+      }
+    } catch {
+      modal.open("error", "Error", "Failed to apply markup");
+    } finally {
+      setApplyingMarkup(false);
+    }
+  };
+
   return (
     <div className="space-y-4">
       {/* Toolbar */}
@@ -166,10 +207,16 @@ export function InventoryClient({
           </Select>
           {isPending && <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />}
         </div>
-        <Button size="sm" onClick={() => setShowForm(true)}>
-          <Plus className="mr-1 h-3.5 w-3.5" />
-          Add Part
-        </Button>
+        <div className="flex items-center gap-2">
+          <Button size="sm" variant="outline" onClick={() => setShowMarkup(true)}>
+            <Percent className="mr-1 h-3.5 w-3.5" />
+            Apply Markup
+          </Button>
+          <Button size="sm" onClick={() => setShowForm(true)}>
+            <Plus className="mr-1 h-3.5 w-3.5" />
+            Add Part
+          </Button>
+        </div>
       </div>
 
       {/* Table */}
@@ -182,6 +229,7 @@ export function InventoryClient({
               <TableHead className="hidden sm:table-cell">Category</TableHead>
               <TableHead>In Stock</TableHead>
               <TableHead className="text-right">Unit Cost</TableHead>
+              <TableHead className="text-right">Sell Price</TableHead>
               <TableHead className="hidden md:table-cell">Supplier</TableHead>
               <TableHead className="hidden lg:table-cell">Location</TableHead>
               <TableHead className="w-[50px]" />
@@ -190,7 +238,7 @@ export function InventoryClient({
           <TableBody>
             {data.parts.length === 0 ? (
               <TableRow>
-                <TableCell colSpan={8} className="h-32 text-center text-muted-foreground">
+                <TableCell colSpan={9} className="h-32 text-center text-muted-foreground">
                   {search || category ? "No parts match your search." : "No parts in inventory yet."}
                 </TableCell>
               </TableRow>
@@ -241,6 +289,24 @@ export function InventoryClient({
                     <TableCell className="text-right">
                       {formatCurrency(part.unitCost, currencyCode)}
                     </TableCell>
+                    <TableCell className="text-right">
+                      {(() => {
+                        const effective = part.sellPrice > 0 ? part.sellPrice : part.unitCost;
+                        const margin = effective > 0 && part.unitCost > 0 && effective !== part.unitCost
+                          ? Math.round(((effective - part.unitCost) / part.unitCost) * 100)
+                          : null;
+                        return (
+                          <div>
+                            {formatCurrency(effective, currencyCode)}
+                            {margin !== null && (
+                              <span className="block text-[10px] text-muted-foreground">
+                                {margin}%
+                              </span>
+                            )}
+                          </div>
+                        );
+                      })()}
+                    </TableCell>
                     <TableCell className="hidden md:table-cell text-muted-foreground">
                       <span className="inline-flex items-center gap-1">
                         {part.supplier || "-"}
@@ -310,7 +376,43 @@ export function InventoryClient({
           if (!open) setEditPart(null);
         }}
         part={editPart ?? undefined}
+        markupMultiplier={initialMarkup}
       />
+
+      {/* Bulk Markup Dialog */}
+      <MarkupDialog open={showMarkup} onOpenChange={setShowMarkup}>
+        <MarkupDialogContent className="sm:max-w-sm">
+          <MarkupDialogHeader>
+            <MarkupDialogTitle>Apply Markup to All Parts</MarkupDialogTitle>
+          </MarkupDialogHeader>
+          <div className="space-y-4">
+            <div className="space-y-2">
+              <Label htmlFor="markupMultiplier">Markup Multiplier</Label>
+              <Input
+                id="markupMultiplier"
+                type="number"
+                min="0.01"
+                step="0.01"
+                value={markupValue}
+                onChange={(e) => setMarkupValue(e.target.value)}
+              />
+              <p className="text-xs text-muted-foreground">
+                This will set sell price = cost x {markupValue || "?"} for all {data.total} parts.
+                {Number(markupValue) > 1 && ` (${Math.round((Number(markupValue) - 1) * 100)}% markup)`}
+              </p>
+            </div>
+            <div className="flex justify-end gap-2">
+              <Button variant="outline" size="sm" onClick={() => setShowMarkup(false)}>
+                Cancel
+              </Button>
+              <Button size="sm" onClick={handleApplyMarkup} disabled={applyingMarkup}>
+                {applyingMarkup && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
+                Apply
+              </Button>
+            </div>
+          </div>
+        </MarkupDialogContent>
+      </MarkupDialog>
     </div>
   );
 }

+ 3 - 1
src/app/(authenticated)/inventory/page.tsx

@@ -18,7 +18,7 @@ export default async function InventoryPage({
       category: params.category,
     }),
     getInventoryCategories(),
-    getSettings([SETTING_KEYS.CURRENCY_CODE]),
+    getSettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.INVENTORY_MARKUP_MULTIPLIER]),
   ]);
 
   if (!result.success || !result.data) {
@@ -37,6 +37,7 @@ export default async function InventoryPage({
   const categories = categoriesResult.success && categoriesResult.data ? categoriesResult.data : [];
   const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
   const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
+  const markupMultiplier = Number(settings[SETTING_KEYS.INVENTORY_MARKUP_MULTIPLIER]) || 1.0;
 
   return (
     <>
@@ -48,6 +49,7 @@ export default async function InventoryPage({
           category={params.category || ""}
           categories={categories}
           currencyCode={currencyCode}
+          markupMultiplier={markupMultiplier}
         />
       </div>
     </>

+ 172 - 3
src/app/(authenticated)/reports/reports-client.tsx

@@ -32,6 +32,7 @@ import {
   Cog,
   CalendarDays,
   UserCheck,
+  Receipt,
 } from "lucide-react";
 import {
   getRevenueReport,
@@ -42,6 +43,7 @@ import {
   getPartsUsageReport,
   getJobAnalyticsReport,
   getCustomerRetentionReport,
+  getTaxReport,
 } from "@/features/reports/Actions/reportActions";
 import { formatCurrency } from "@/lib/format";
 import type {
@@ -53,6 +55,7 @@ import type {
   PartsUsageReport,
   JobAnalyticsReport,
   CustomerRetentionReport,
+  TaxReport,
 } from "@/features/reports/Schema/reportTypes";
 import { RevenueBarChart, RevenueTypeDonut } from "@/features/reports/Components/RevenueCharts";
 import { ServiceStatusChart, ServiceTypeDonut } from "@/features/reports/Components/ServiceCharts";
@@ -61,6 +64,7 @@ import { TechnicianBarChart } from "@/features/reports/Components/TechnicianChar
 import { PartsDonut } from "@/features/reports/Components/PartsCharts";
 import { DayOfWeekChart, ServiceTypeAnalyticsDonut, MonthlyTrendChart } from "@/features/reports/Components/JobAnalyticsCharts";
 import { RetentionBarChart } from "@/features/reports/Components/RetentionCharts";
+import { TaxBarChart } from "@/features/reports/Components/TaxCharts";
 import {
   exportRevenueCsv,
   exportServicesCsv,
@@ -70,9 +74,10 @@ import {
   exportPartsCsv,
   exportJobAnalyticsCsv,
   exportRetentionCsv,
+  exportTaxCsv,
 } from "@/features/reports/Components/csv-export";
 
-type ReportTab = "revenue" | "services" | "customers" | "inventory" | "technicians" | "parts" | "job-analytics" | "retention";
+type ReportTab = "revenue" | "services" | "customers" | "inventory" | "technicians" | "parts" | "job-analytics" | "retention" | "tax";
 
 interface ReportsClientProps {
   currencyCode: string;
@@ -96,6 +101,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
   const [partsData, setPartsData] = useState<PartsUsageReport | null>(null);
   const [jobAnalyticsData, setJobAnalyticsData] = useState<JobAnalyticsReport | null>(null);
   const [retentionData, setRetentionData] = useState<CustomerRetentionReport | null>(null);
+  const [taxData, setTaxData] = useState<TaxReport | null>(null);
 
   const fmtCurrency = useCallback(
     (value: number) => formatCurrency(value, currencyCode),
@@ -148,6 +154,11 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
             if (result.success && result.data) setRetentionData(result.data);
             break;
           }
+          case "tax": {
+            const result = await getTaxReport(dateParams);
+            if (result.success && result.data) setTaxData(result.data);
+            break;
+          }
         }
       } catch (error) {
         console.error("Failed to fetch report:", error);
@@ -175,6 +186,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
       parts: partsData,
       "job-analytics": jobAnalyticsData,
       retention: retentionData,
+      tax: taxData,
     };
     if (!dataMap[tab]) {
       fetchReport(tab);
@@ -211,6 +223,9 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
       case "retention":
         if (retentionData) exportRetentionCsv(retentionData, currencyCode);
         break;
+      case "tax":
+        if (taxData) exportTaxCsv(taxData, currencyCode);
+        break;
     }
   };
 
@@ -223,6 +238,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
     parts: partsData,
     "job-analytics": jobAnalyticsData,
     retention: retentionData,
+    tax: taxData,
   };
   const hasData = !!dataMap[activeTab];
 
@@ -265,6 +281,10 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
             <UserCheck className="h-4 w-4" />
             Retention
           </TabsTrigger>
+          <TabsTrigger value="tax" className="gap-1.5">
+            <Receipt className="h-4 w-4" />
+            Tax
+          </TabsTrigger>
         </TabsList>
 
         <div className="flex items-center gap-2">
@@ -545,7 +565,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
       <TabsContent value="inventory">
         {!loading && inventoryData && (
           <div className="space-y-4">
-            <div className="grid gap-3 grid-cols-3">
+            <div className="grid gap-3 grid-cols-2 lg:grid-cols-5">
               <Card className="border-0 shadow-sm">
                 <CardContent className="flex items-center gap-3 p-4">
                   <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
@@ -574,13 +594,39 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
                     <DollarSign className="h-4 w-4 text-amber-500" />
                   </div>
                   <div className="min-w-0">
-                    <p className="text-xs text-muted-foreground">Value</p>
+                    <p className="text-xs text-muted-foreground">Cost Value</p>
                     <p className="text-lg font-semibold truncate">
                       {fmtCurrency(inventoryData.totalValue)}
                     </p>
                   </div>
                 </CardContent>
               </Card>
+              <Card className="border-0 shadow-sm">
+                <CardContent className="flex items-center gap-3 p-4">
+                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
+                    <DollarSign className="h-4 w-4 text-violet-500" />
+                  </div>
+                  <div className="min-w-0">
+                    <p className="text-xs text-muted-foreground">Sell Value</p>
+                    <p className="text-lg font-semibold truncate">
+                      {fmtCurrency(inventoryData.totalSellValue)}
+                    </p>
+                  </div>
+                </CardContent>
+              </Card>
+              <Card className="border-0 shadow-sm">
+                <CardContent className="flex items-center gap-3 p-4">
+                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-emerald-500/10">
+                    <TrendingUp className="h-4 w-4 text-emerald-500" />
+                  </div>
+                  <div className="min-w-0">
+                    <p className="text-xs text-muted-foreground">Potential Margin</p>
+                    <p className="text-lg font-semibold truncate">
+                      {fmtCurrency(inventoryData.totalSellValue - inventoryData.totalValue)}
+                    </p>
+                  </div>
+                </CardContent>
+              </Card>
             </div>
             {inventoryData.lowStock.length > 0 && (
               <Card className="border-0 shadow-sm">
@@ -973,6 +1019,129 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
         )}
         {!loading && !retentionData && <EmptyState />}
       </TabsContent>
+
+      {/* Tax Tab */}
+      <TabsContent value="tax">
+        {!loading && taxData && (
+          <div className="space-y-4">
+            <div className="grid gap-3 grid-cols-1 sm:grid-cols-3">
+              <Card className="border-0 shadow-sm">
+                <CardContent className="flex items-center gap-3 p-4">
+                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-amber-500/10">
+                    <Receipt className="h-4 w-4 text-amber-500" />
+                  </div>
+                  <div className="min-w-0">
+                    <p className="text-xs text-muted-foreground">Total Tax Collected</p>
+                    <p className="text-lg font-semibold truncate">
+                      {fmtCurrency(taxData.summary.totalTaxCollected)}
+                    </p>
+                  </div>
+                </CardContent>
+              </Card>
+              <Card className="border-0 shadow-sm">
+                <CardContent className="flex items-center gap-3 p-4">
+                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-blue-500/10">
+                    <DollarSign className="h-4 w-4 text-blue-500" />
+                  </div>
+                  <div className="min-w-0">
+                    <p className="text-xs text-muted-foreground">Taxable Revenue</p>
+                    <p className="text-lg font-semibold truncate">
+                      {fmtCurrency(taxData.summary.totalTaxableAmount)}
+                    </p>
+                  </div>
+                </CardContent>
+              </Card>
+              <Card className="border-0 shadow-sm">
+                <CardContent className="flex items-center gap-3 p-4">
+                  <div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-violet-500/10">
+                    <BarChart3 className="h-4 w-4 text-violet-500" />
+                  </div>
+                  <div className="min-w-0">
+                    <p className="text-xs text-muted-foreground">Invoices with Tax</p>
+                    <p className="text-lg font-semibold">{taxData.summary.totalInvoices}</p>
+                  </div>
+                </CardContent>
+              </Card>
+            </div>
+
+            {taxData.monthly.length > 0 && (
+              <Card className="border-0 shadow-sm">
+                <CardHeader className="pb-2">
+                  <CardTitle className="text-sm font-medium">Monthly Tax Collected</CardTitle>
+                </CardHeader>
+                <CardContent>
+                  <TaxBarChart data={taxData.monthly} formatCurrency={fmtCurrency} />
+                </CardContent>
+              </Card>
+            )}
+
+            {taxData.byRate.length > 0 && (
+              <Card className="border-0 shadow-sm">
+                <CardHeader className="pb-2">
+                  <CardTitle className="text-sm font-medium">Tax by Rate</CardTitle>
+                </CardHeader>
+                <CardContent>
+                  <Table>
+                    <TableHeader>
+                      <TableRow>
+                        <TableHead>Tax Rate</TableHead>
+                        <TableHead className="text-right">Tax Collected</TableHead>
+                        <TableHead className="text-right">Invoices</TableHead>
+                      </TableRow>
+                    </TableHeader>
+                    <TableBody>
+                      {taxData.byRate.map((row) => (
+                        <TableRow key={row.taxRate}>
+                          <TableCell className="text-sm font-medium">{row.taxRate}%</TableCell>
+                          <TableCell className="text-right text-sm">
+                            {fmtCurrency(row.taxCollected)}
+                          </TableCell>
+                          <TableCell className="text-right text-sm">{row.invoiceCount}</TableCell>
+                        </TableRow>
+                      ))}
+                    </TableBody>
+                  </Table>
+                </CardContent>
+              </Card>
+            )}
+
+            {taxData.monthly.length > 0 && (
+              <Card className="border-0 shadow-sm">
+                <CardHeader className="pb-2">
+                  <CardTitle className="text-sm font-medium">Monthly Breakdown</CardTitle>
+                </CardHeader>
+                <CardContent>
+                  <Table>
+                    <TableHeader>
+                      <TableRow>
+                        <TableHead>Month</TableHead>
+                        <TableHead className="text-right">Tax Collected</TableHead>
+                        <TableHead className="text-right">Taxable Amount</TableHead>
+                        <TableHead className="text-right">Invoices</TableHead>
+                      </TableRow>
+                    </TableHeader>
+                    <TableBody>
+                      {taxData.monthly.map((row) => (
+                        <TableRow key={row.month}>
+                          <TableCell className="text-sm">{row.month}</TableCell>
+                          <TableCell className="text-right text-sm">
+                            {fmtCurrency(row.taxCollected)}
+                          </TableCell>
+                          <TableCell className="text-right text-sm">
+                            {fmtCurrency(row.taxableAmount)}
+                          </TableCell>
+                          <TableCell className="text-right text-sm">{row.invoiceCount}</TableCell>
+                        </TableRow>
+                      ))}
+                    </TableBody>
+                  </Table>
+                </CardContent>
+              </Card>
+            )}
+          </div>
+        )}
+        {!loading && !taxData && <EmptyState />}
+      </TabsContent>
     </Tabs>
   );
 }

+ 29 - 0
src/features/inventory/Actions/inventoryActions.ts

@@ -5,6 +5,7 @@ import { withAuth } from "@/lib/with-auth";
 import { createInventoryPartSchema, updateInventoryPartSchema, adjustStockSchema } from "../Schema/inventorySchema";
 import { revalidatePath } from "next/cache";
 import { Prisma } from "@prisma/client";
+import { z } from "zod";
 import { unlink } from "fs/promises";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
@@ -189,6 +190,7 @@ export async function getInventoryPartsList() {
         partNumber: true,
         name: true,
         unitCost: true,
+        sellPrice: true,
         quantity: true,
         category: true,
       },
@@ -196,3 +198,30 @@ export async function getInventoryPartsList() {
     });
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.INVENTORY }] });
 }
+
+const applyMarkupSchema = z.object({
+  multiplier: z.number().positive("Multiplier must be greater than 0"),
+});
+
+export async function applyMarkupToAll(input: unknown) {
+  return withAuth(async ({ userId, organizationId }) => {
+    const { multiplier } = applyMarkupSchema.parse(input);
+
+    const parts = await db.inventoryPart.findMany({
+      where: { organizationId, isArchived: false },
+      select: { id: true, unitCost: true },
+    });
+
+    await db.$transaction(
+      parts.map((p) =>
+        db.inventoryPart.update({
+          where: { id: p.id },
+          data: { sellPrice: Math.round(p.unitCost * multiplier * 100) / 100 },
+        })
+      )
+    );
+
+    revalidatePath("/inventory");
+    return { updated: parts.length };
+  }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INVENTORY }] });
+}

+ 25 - 3
src/features/inventory/Components/InventoryPartForm.tsx

@@ -21,6 +21,7 @@ import { compressImage } from "@/lib/compress-image";
 interface InventoryPartFormProps {
   open: boolean;
   onOpenChange: (open: boolean) => void;
+  markupMultiplier?: number;
   part?: {
     id: string;
     partNumber: string | null;
@@ -30,6 +31,7 @@ interface InventoryPartFormProps {
     quantity: number;
     minQuantity: number;
     unitCost: number;
+    sellPrice: number;
     supplier: string | null;
     supplierPhone: string | null;
     supplierEmail: string | null;
@@ -39,7 +41,7 @@ interface InventoryPartFormProps {
   };
 }
 
-export function InventoryPartForm({ open, onOpenChange, part }: InventoryPartFormProps) {
+export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier }: InventoryPartFormProps) {
   const router = useRouter();
   const modal = useGlassModal();
   const [loading, setLoading] = useState(false);
@@ -176,6 +178,7 @@ export function InventoryPartForm({ open, onOpenChange, part }: InventoryPartFor
       quantity: Number(formData.get("quantity")) || 0,
       minQuantity: Number(formData.get("minQuantity")) || 0,
       unitCost: Number(formData.get("unitCost")) || 0,
+      sellPrice: Number(formData.get("sellPrice")) || 0,
       supplier: (formData.get("supplier") as string) || undefined,
       supplierPhone: (formData.get("supplierPhone") as string) || undefined,
       supplierEmail: (formData.get("supplierEmail") as string) || undefined,
@@ -380,7 +383,7 @@ export function InventoryPartForm({ open, onOpenChange, part }: InventoryPartFor
                 </div>
               </div>
 
-              <div className="grid grid-cols-3 gap-4">
+              <div className="grid grid-cols-4 gap-4">
                 <div className="space-y-2">
                   <Label htmlFor="quantity">Quantity</Label>
                   <Input
@@ -393,7 +396,7 @@ export function InventoryPartForm({ open, onOpenChange, part }: InventoryPartFor
                   />
                 </div>
                 <div className="space-y-2">
-                  <Label htmlFor="minQuantity">Min Quantity</Label>
+                  <Label htmlFor="minQuantity">Min Qty</Label>
                   <Input
                     id="minQuantity"
                     name="minQuantity"
@@ -412,6 +415,25 @@ export function InventoryPartForm({ open, onOpenChange, part }: InventoryPartFor
                     min="0"
                     step="0.01"
                     defaultValue={part?.unitCost ?? 0}
+                    onChange={(e) => {
+                      if (!markupMultiplier || markupMultiplier <= 0) return;
+                      const cost = Number(e.target.value) || 0;
+                      const sellPriceInput = document.getElementById("sellPrice") as HTMLInputElement | null;
+                      if (sellPriceInput && (!sellPriceInput.value || Number(sellPriceInput.value) === 0)) {
+                        sellPriceInput.value = String(Math.round(cost * markupMultiplier * 100) / 100);
+                      }
+                    }}
+                  />
+                </div>
+                <div className="space-y-2">
+                  <Label htmlFor="sellPrice">Sell Price</Label>
+                  <Input
+                    id="sellPrice"
+                    name="sellPrice"
+                    type="number"
+                    min="0"
+                    step="0.01"
+                    defaultValue={part?.sellPrice ?? 0}
                   />
                 </div>
               </div>

+ 1 - 0
src/features/inventory/Schema/inventorySchema.ts

@@ -8,6 +8,7 @@ export const createInventoryPartSchema = z.object({
   quantity: z.coerce.number().int().min(0, "Quantity must be 0 or more").default(0),
   minQuantity: z.coerce.number().int().min(0, "Min quantity must be 0 or more").default(0),
   unitCost: z.coerce.number().min(0, "Unit cost must be 0 or more").default(0),
+  sellPrice: z.coerce.number().min(0, "Sell price must be 0 or more").default(0),
   supplier: z.string().optional(),
   supplierPhone: z.string().optional(),
   supplierEmail: z.string().optional(),

+ 61 - 0
src/features/reports/Actions/reportActions.ts

@@ -438,17 +438,20 @@ export async function getInventoryReport() {
         partNumber: true,
         quantity: true,
         unitCost: true,
+        sellPrice: true,
         minQuantity: true,
       },
       orderBy: { name: "asc" },
     });
 
     let totalValue = 0;
+    let totalSellValue = 0;
     let totalItems = 0;
     const lowStock: typeof parts = [];
 
     for (const p of parts) {
       totalValue += (p.unitCost || 0) * p.quantity;
+      totalSellValue += (p.sellPrice > 0 ? p.sellPrice : p.unitCost || 0) * p.quantity;
       totalItems += p.quantity;
       if (p.minQuantity && p.quantity <= p.minQuantity) {
         lowStock.push(p);
@@ -459,7 +462,65 @@ export async function getInventoryReport() {
       totalParts: parts.length,
       totalItems,
       totalValue,
+      totalSellValue,
       lowStock,
     };
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.REPORTS }] });
 }
+
+export async function getTaxReport(params: {
+  startDate?: string;
+  endDate?: string;
+}) {
+  return withAuth(async ({ organizationId }) => {
+    const start = params.startDate ? new Date(params.startDate) : new Date(new Date().getFullYear(), 0, 1);
+    const end = params.endDate ? new Date(params.endDate) : new Date();
+    end.setHours(23, 59, 59, 999);
+
+    const records = await db.serviceRecord.findMany({
+      where: {
+        vehicle: { organizationId },
+        serviceDate: { gte: start, lte: end },
+      },
+      select: {
+        serviceDate: true,
+        subtotal: true,
+        taxRate: true,
+        taxAmount: true,
+        totalAmount: true,
+      },
+      orderBy: { serviceDate: "asc" },
+    });
+
+    const monthly: Record<string, { taxCollected: number; invoiceCount: number; taxableAmount: number }> = {};
+    const byRate: Record<number, { taxCollected: number; invoiceCount: number }> = {};
+    let totalTaxCollected = 0;
+    let totalTaxableAmount = 0;
+    let totalInvoices = 0;
+
+    for (const r of records) {
+      if (r.taxAmount <= 0) continue;
+
+      const month = `${r.serviceDate.getFullYear()}-${String(r.serviceDate.getMonth() + 1).padStart(2, "0")}`;
+
+      if (!monthly[month]) monthly[month] = { taxCollected: 0, invoiceCount: 0, taxableAmount: 0 };
+      monthly[month].taxCollected += r.taxAmount;
+      monthly[month].invoiceCount += 1;
+      monthly[month].taxableAmount += r.subtotal;
+
+      if (!byRate[r.taxRate]) byRate[r.taxRate] = { taxCollected: 0, invoiceCount: 0 };
+      byRate[r.taxRate].taxCollected += r.taxAmount;
+      byRate[r.taxRate].invoiceCount += 1;
+
+      totalTaxCollected += r.taxAmount;
+      totalTaxableAmount += r.subtotal;
+      totalInvoices += 1;
+    }
+
+    return {
+      monthly: Object.entries(monthly).map(([month, data]) => ({ month, ...data })),
+      byRate: Object.entries(byRate).map(([rate, data]) => ({ taxRate: Number(rate), ...data })),
+      summary: { totalTaxCollected, totalTaxableAmount, totalInvoices },
+    };
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.REPORTS }] });
+}

+ 49 - 0
src/features/reports/Components/TaxCharts.tsx

@@ -0,0 +1,49 @@
+"use client";
+
+import {
+  BarChart,
+  Bar,
+  XAxis,
+  YAxis,
+  CartesianGrid,
+  Tooltip,
+  ResponsiveContainer,
+} from "recharts";
+import type { MonthlyTax } from "../Schema/reportTypes";
+
+interface TaxBarChartProps {
+  data: MonthlyTax[];
+  formatCurrency: (value: number) => string;
+}
+
+export function TaxBarChart({ data, formatCurrency }: TaxBarChartProps) {
+  if (data.length === 0) return null;
+
+  return (
+    <ResponsiveContainer width="100%" height={300}>
+      <BarChart data={data} margin={{ top: 5, right: 20, left: 10, bottom: 5 }}>
+        <CartesianGrid strokeDasharray="3 3" className="stroke-border" />
+        <XAxis
+          dataKey="month"
+          className="text-xs fill-muted-foreground"
+          tick={{ fontSize: 12 }}
+        />
+        <YAxis
+          className="text-xs fill-muted-foreground"
+          tick={{ fontSize: 12 }}
+          tickFormatter={(v) => formatCurrency(v)}
+        />
+        <Tooltip
+          formatter={(value) => formatCurrency(Number(value))}
+          contentStyle={{
+            backgroundColor: "hsl(var(--popover))",
+            border: "1px solid hsl(var(--border))",
+            borderRadius: "8px",
+            color: "hsl(var(--popover-foreground))",
+          }}
+        />
+        <Bar dataKey="taxCollected" name="Tax Collected" fill="#f59e0b" radius={[4, 4, 0, 0]} />
+      </BarChart>
+    </ResponsiveContainer>
+  );
+}

+ 17 - 0
src/features/reports/Components/csv-export.ts

@@ -7,6 +7,7 @@ import type {
   PartsUsageReport,
   JobAnalyticsReport,
   CustomerRetentionReport,
+  TaxReport,
 } from "../Schema/reportTypes";
 import { formatCurrency } from "@/lib/format";
 
@@ -166,3 +167,19 @@ export function exportRetentionCsv(data: CustomerRetentionReport, currencyCode:
     ["name", "company", "visitCount", "totalSpent", "avgDaysBetweenVisits"],
   );
 }
+
+export function exportTaxCsv(data: TaxReport, currencyCode: string) {
+  const fmt = (n: number) => formatCurrency(n, currencyCode);
+  const rows = data.monthly.map((m) => ({
+    month: m.month,
+    taxCollected: fmt(m.taxCollected),
+    taxableAmount: fmt(m.taxableAmount),
+    invoiceCount: m.invoiceCount,
+  }));
+  downloadCsv(
+    "tax-report.csv",
+    ["Month", "Tax Collected", "Taxable Amount", "Invoice Count"],
+    rows,
+    ["month", "taxCollected", "taxableAmount", "invoiceCount"],
+  );
+}

+ 25 - 0
src/features/reports/Schema/reportTypes.ts

@@ -60,6 +60,7 @@ export interface LowStockPart {
   partNumber: string | null;
   quantity: number;
   unitCost: number | null;
+  sellPrice: number | null;
   minQuantity: number | null;
 }
 
@@ -67,6 +68,7 @@ export interface InventoryReport {
   totalParts: number;
   totalItems: number;
   totalValue: number;
+  totalSellValue: number;
   lowStock: LowStockPart[];
 }
 
@@ -143,3 +145,26 @@ export interface CustomerRetentionReport {
   avgTimeBetweenVisits: number | null;
   topReturning: RetentionCustomer[];
 }
+
+export interface MonthlyTax {
+  month: string;
+  taxCollected: number;
+  invoiceCount: number;
+  taxableAmount: number;
+}
+
+export interface TaxByRate {
+  taxRate: number;
+  taxCollected: number;
+  invoiceCount: number;
+}
+
+export interface TaxReport {
+  monthly: MonthlyTax[];
+  byRate: TaxByRate[];
+  summary: {
+    totalTaxCollected: number;
+    totalTaxableAmount: number;
+    totalInvoices: number;
+  };
+}

+ 1 - 1
src/features/settings/Components/license-settings.tsx

@@ -66,7 +66,7 @@ export function LicenseSettings({
             </CardDescription>
           </div>
           {!licenseValid && (
-            <Button asChild variant="outline" size="sm" className="shrink-0">
+            <Button asChild variant="default" size="sm" className="shrink-0">
               <a
                 href={`${process.env.NEXT_PUBLIC_TORQVOICE_COM_URL || 'https://torqvoice.com'}/subscriptions/white-label`}
                 target="_blank"

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

@@ -60,6 +60,7 @@ export const SETTING_KEYS = {
   PREDICTED_MAINTENANCE_ENABLED: "maintenance.enabled",
   MAINTENANCE_SERVICE_INTERVAL: "maintenance.serviceInterval",
   MAINTENANCE_APPROACHING_THRESHOLD: "maintenance.approachingThreshold",
+  INVENTORY_MARKUP_MULTIPLIER: "inventory.markupMultiplier",
 } as const;
 
 export type SettingKey = (typeof SETTING_KEYS)[keyof typeof SETTING_KEYS];

+ 11 - 3
src/features/vehicles/Components/service-edit/InventoryPickerDialog.tsx

@@ -63,12 +63,13 @@ export function InventoryPickerDialog({
               type="button"
               className="w-full text-left rounded-md p-2.5 hover:bg-accent transition-colors"
               onClick={() => {
+                const price = ip.sellPrice > 0 ? ip.sellPrice : ip.unitCost;
                 onSelectPart({
                   partNumber: ip.partNumber || '',
                   name: ip.name,
                   quantity: 1,
-                  unitPrice: ip.unitCost,
-                  total: ip.unitCost,
+                  unitPrice: price,
+                  total: price,
                   inventoryPartId: ip.id,
                 })
                 onOpenChange(false)
@@ -87,7 +88,14 @@ export function InventoryPickerDialog({
               </div>
               <div className="flex items-center gap-2 text-xs text-muted-foreground mt-0.5">
                 {ip.category && <span>{ip.category}</span>}
-                <span>{formatCurrency(ip.unitCost, currencyCode)}</span>
+                {ip.sellPrice > 0 ? (
+                  <>
+                    <span className="font-medium text-foreground">{formatCurrency(ip.sellPrice, currencyCode)}</span>
+                    <span className="line-through">{formatCurrency(ip.unitCost, currencyCode)}</span>
+                  </>
+                ) : (
+                  <span>{formatCurrency(ip.unitCost, currencyCode)}</span>
+                )}
               </div>
             </button>
           ))}

+ 1 - 0
src/features/vehicles/Components/service-edit/form-types.ts

@@ -9,6 +9,7 @@ export interface InventoryPartOption {
   partNumber: string | null
   name: string
   unitCost: number
+  sellPrice: number
   quantity: number
   category: string | null
 }