Răsfoiți Sursa

Add net profit tracking and reporting for service parts (#92)

* net profit

* description
Bernt Christian Egeland 7 luni în urmă
părinte
comite
7d5d25117f

+ 10 - 0
messages/de/reports.json

@@ -23,6 +23,14 @@
     "collected": "Eingezogen",
     "outstanding": "Ausstehend",
     "services": "Dienstleistungen",
+    "partsCost": "Teilekosten",
+    "netProfit": "Nettogewinn",
+    "revenueDesc": "Gesamtbetrag aller abgeschlossenen Aufträge",
+    "collectedDesc": "Erhaltene Zahlungen von Kunden",
+    "outstandingDesc": "In Rechnung gestellt, aber noch nicht bezahlt",
+    "servicesDesc": "Abgeschlossene Arbeitsaufträge gesamt",
+    "partsCostDesc": "Einkaufskosten der verwendeten Teile",
+    "netProfitDesc": "Umsatz abzüglich Teilekosten",
     "monthlyRevenue": "Monatlicher Umsatz",
     "revenueByType": "Umsatz nach Typ",
     "monthlyBreakdown": "Monatliche Aufschlüsselung",
@@ -30,6 +38,8 @@
       "month": "Monat",
       "revenue": "Umsatz",
       "collected": "Eingezogen",
+      "partsCost": "Teilekosten",
+      "netProfit": "Nettogewinn",
       "count": "Anzahl"
     }
   },

+ 10 - 0
messages/en/reports.json

@@ -23,6 +23,14 @@
     "collected": "Collected",
     "outstanding": "Outstanding",
     "services": "Services",
+    "partsCost": "Parts Cost",
+    "netProfit": "Net Profit",
+    "revenueDesc": "Total invoiced from all completed services",
+    "collectedDesc": "Payments received from customers",
+    "outstandingDesc": "Invoiced but not yet paid",
+    "servicesDesc": "Total completed work orders",
+    "partsCostDesc": "Purchase cost of parts used in services",
+    "netProfitDesc": "Revenue minus parts cost",
     "monthlyRevenue": "Monthly Revenue",
     "revenueByType": "Revenue by Type",
     "monthlyBreakdown": "Monthly Breakdown",
@@ -30,6 +38,8 @@
       "month": "Month",
       "revenue": "Revenue",
       "collected": "Collected",
+      "partsCost": "Parts Cost",
+      "netProfit": "Net Profit",
       "count": "Count"
     }
   },

+ 10 - 0
messages/es/reports.json

@@ -23,6 +23,14 @@
     "collected": "Cobrado",
     "outstanding": "Pendiente",
     "services": "Servicios",
+    "partsCost": "Coste de piezas",
+    "netProfit": "Beneficio neto",
+    "revenueDesc": "Total facturado de todos los servicios completados",
+    "collectedDesc": "Pagos recibidos de clientes",
+    "outstandingDesc": "Facturado pero aún no cobrado",
+    "servicesDesc": "Total de órdenes de trabajo completadas",
+    "partsCostDesc": "Coste de compra de piezas usadas en servicios",
+    "netProfitDesc": "Ingresos menos coste de piezas",
     "monthlyRevenue": "Ingresos Mensuales",
     "revenueByType": "Ingresos por Tipo",
     "monthlyBreakdown": "Desglose Mensual",
@@ -30,6 +38,8 @@
       "month": "Mes",
       "revenue": "Ingresos",
       "collected": "Cobrado",
+      "partsCost": "Coste de piezas",
+      "netProfit": "Beneficio neto",
       "count": "Cantidad"
     }
   },

+ 10 - 0
messages/fr/reports.json

@@ -23,6 +23,14 @@
     "collected": "Encaissé",
     "outstanding": "En attente",
     "services": "Services",
+    "partsCost": "Coût des pièces",
+    "netProfit": "Bénéfice net",
+    "revenueDesc": "Total facturé pour tous les services terminés",
+    "collectedDesc": "Paiements reçus des clients",
+    "outstandingDesc": "Facturé mais pas encore encaissé",
+    "servicesDesc": "Total des ordres de travail terminés",
+    "partsCostDesc": "Coût d'achat des pièces utilisées",
+    "netProfitDesc": "Chiffre d'affaires moins coût des pièces",
     "monthlyRevenue": "Revenus Mensuels",
     "revenueByType": "Revenus par Type",
     "monthlyBreakdown": "Répartition Mensuelle",
@@ -30,6 +38,8 @@
       "month": "Mois",
       "revenue": "Revenus",
       "collected": "Encaissé",
+      "partsCost": "Coût des pièces",
+      "netProfit": "Bénéfice net",
       "count": "Nombre"
     }
   },

+ 10 - 0
messages/it/reports.json

@@ -23,6 +23,14 @@
     "collected": "Incassato",
     "outstanding": "In sospeso",
     "services": "Servizi",
+    "partsCost": "Costo ricambi",
+    "netProfit": "Utile netto",
+    "revenueDesc": "Totale fatturato da tutti i servizi completati",
+    "collectedDesc": "Pagamenti ricevuti dai clienti",
+    "outstandingDesc": "Fatturato ma non ancora incassato",
+    "servicesDesc": "Totale ordini di lavoro completati",
+    "partsCostDesc": "Costo di acquisto dei ricambi utilizzati",
+    "netProfitDesc": "Ricavi meno costo ricambi",
     "monthlyRevenue": "Ricavi Mensili",
     "revenueByType": "Ricavi per Tipo",
     "monthlyBreakdown": "Riepilogo Mensile",
@@ -30,6 +38,8 @@
       "month": "Mese",
       "revenue": "Ricavi",
       "collected": "Incassato",
+      "partsCost": "Costo ricambi",
+      "netProfit": "Utile netto",
       "count": "Conteggio"
     }
   },

+ 10 - 0
messages/nb/reports.json

@@ -23,6 +23,14 @@
     "collected": "Innkrevd",
     "outstanding": "Utestående",
     "services": "Tjenester",
+    "partsCost": "Delekostnad",
+    "netProfit": "Nettoresultat",
+    "revenueDesc": "Totalt fakturert fra alle fullførte tjenester",
+    "collectedDesc": "Betalinger mottatt fra kunder",
+    "outstandingDesc": "Fakturert men ikke betalt ennå",
+    "servicesDesc": "Totalt fullførte arbeidsordrer",
+    "partsCostDesc": "Innkjøpskostnad for deler brukt i tjenester",
+    "netProfitDesc": "Inntekt minus delekostnad",
     "monthlyRevenue": "Månedlige Inntekter",
     "revenueByType": "Inntekter etter Type",
     "monthlyBreakdown": "Månedlig Oversikt",
@@ -30,6 +38,8 @@
       "month": "Måned",
       "revenue": "Inntekter",
       "collected": "Innkrevd",
+      "partsCost": "Delekostnad",
+      "netProfit": "Nettoresultat",
       "count": "Antall"
     }
   },

+ 10 - 0
messages/nl/reports.json

@@ -23,6 +23,14 @@
     "collected": "Geïnd",
     "outstanding": "Openstaand",
     "services": "Diensten",
+    "partsCost": "Onderdeelkosten",
+    "netProfit": "Nettowinst",
+    "revenueDesc": "Totaal gefactureerd van alle voltooide diensten",
+    "collectedDesc": "Ontvangen betalingen van klanten",
+    "outstandingDesc": "Gefactureerd maar nog niet betaald",
+    "servicesDesc": "Totaal voltooide werkorders",
+    "partsCostDesc": "Inkoopkosten van gebruikte onderdelen",
+    "netProfitDesc": "Omzet minus onderdeelkosten",
     "monthlyRevenue": "Maandelijkse Omzet",
     "revenueByType": "Omzet per Type",
     "monthlyBreakdown": "Maandelijks Overzicht",
@@ -30,6 +38,8 @@
       "month": "Maand",
       "revenue": "Omzet",
       "collected": "Geïnd",
+      "partsCost": "Onderdeelkosten",
+      "netProfit": "Nettowinst",
       "count": "Aantal"
     }
   },

+ 10 - 0
messages/pl/reports.json

@@ -23,6 +23,14 @@
     "collected": "Zebrano",
     "outstanding": "Zaległe",
     "services": "Usługi",
+    "partsCost": "Koszt części",
+    "netProfit": "Zysk netto",
+    "revenueDesc": "Łączna kwota faktur ze wszystkich usług",
+    "collectedDesc": "Płatności otrzymane od klientów",
+    "outstandingDesc": "Zafakturowane, ale jeszcze nieopłacone",
+    "servicesDesc": "Łączna liczba zrealizowanych zleceń",
+    "partsCostDesc": "Koszt zakupu części użytych w usługach",
+    "netProfitDesc": "Przychód minus koszt części",
     "monthlyRevenue": "Przychody Miesięczne",
     "revenueByType": "Przychody wg Typu",
     "monthlyBreakdown": "Zestawienie Miesięczne",
@@ -30,6 +38,8 @@
       "month": "Miesiąc",
       "revenue": "Przychody",
       "collected": "Zebrano",
+      "partsCost": "Koszt części",
+      "netProfit": "Zysk netto",
       "count": "Ilość"
     }
   },

+ 10 - 0
messages/pt-BR/reports.json

@@ -23,6 +23,14 @@
     "collected": "Recebido",
     "outstanding": "Pendente",
     "services": "Serviços",
+    "partsCost": "Custo de peças",
+    "netProfit": "Lucro líquido",
+    "revenueDesc": "Total faturado de todos os serviços concluídos",
+    "collectedDesc": "Pagamentos recebidos de clientes",
+    "outstandingDesc": "Faturado mas ainda não pago",
+    "servicesDesc": "Total de ordens de serviço concluídas",
+    "partsCostDesc": "Custo de compra das peças usadas nos serviços",
+    "netProfitDesc": "Receita menos custo de peças",
     "monthlyRevenue": "Receita Mensal",
     "revenueByType": "Receita por Tipo",
     "monthlyBreakdown": "Detalhamento Mensal",
@@ -30,6 +38,8 @@
       "month": "Mês",
       "revenue": "Receita",
       "collected": "Recebido",
+      "partsCost": "Custo de peças",
+      "netProfit": "Lucro líquido",
       "count": "Quantidade"
     }
   },

+ 10 - 0
messages/ru/reports.json

@@ -23,6 +23,14 @@
     "collected": "Собрано",
     "outstanding": "Задолженность",
     "services": "Обслуживания",
+    "partsCost": "Стоимость запчастей",
+    "netProfit": "Чистая прибыль",
+    "revenueDesc": "Общая сумма счетов по всем выполненным услугам",
+    "collectedDesc": "Полученные платежи от клиентов",
+    "outstandingDesc": "Выставлено, но ещё не оплачено",
+    "servicesDesc": "Всего выполненных заказ-нарядов",
+    "partsCostDesc": "Закупочная стоимость запчастей, использованных в услугах",
+    "netProfitDesc": "Выручка минус стоимость запчастей",
     "monthlyRevenue": "Ежемесячный доход",
     "revenueByType": "Доход по типам",
     "monthlyBreakdown": "Помесячная разбивка",
@@ -30,6 +38,8 @@
       "month": "Месяц",
       "revenue": "Доход",
       "collected": "Собрано",
+      "partsCost": "Стоимость запчастей",
+      "netProfit": "Чистая прибыль",
       "count": "Количество"
     }
   },

+ 10 - 0
messages/tr/reports.json

@@ -23,6 +23,14 @@
     "collected": "Tahsil Edilen",
     "outstanding": "Bekleyen",
     "services": "Hizmetler",
+    "partsCost": "Parça maliyeti",
+    "netProfit": "Net kâr",
+    "revenueDesc": "Tamamlanan tüm servislerden toplam fatura",
+    "collectedDesc": "Müşterilerden alınan ödemeler",
+    "outstandingDesc": "Faturalandırılmış ancak henüz ödenmemiş",
+    "servicesDesc": "Toplam tamamlanan iş emirleri",
+    "partsCostDesc": "Servislerde kullanılan parçaların alış maliyeti",
+    "netProfitDesc": "Gelir eksi parça maliyeti",
     "monthlyRevenue": "Aylık Gelir",
     "revenueByType": "Türe Göre Gelir",
     "monthlyBreakdown": "Aylık Döküm",
@@ -30,6 +38,8 @@
       "month": "Ay",
       "revenue": "Gelir",
       "collected": "Tahsil Edilen",
+      "partsCost": "Parça maliyeti",
+      "netProfit": "Net kâr",
       "count": "Adet"
     }
   },

+ 12 - 0
prisma/migrations/20260325204612_net_profit/migration.sql

@@ -0,0 +1,12 @@
+/*
+  Warnings:
+
+  - You are about to drop the column `images` on the `inventory_parts` table. All the data in the column will be lost.
+
+*/
+-- AlterTable
+ALTER TABLE "inventory_parts" DROP COLUMN "images";
+
+-- AlterTable
+ALTER TABLE "service_parts" ADD COLUMN     "inventoryPartId" TEXT,
+ADD COLUMN     "unitCost" DOUBLE PRECISION NOT NULL DEFAULT 0;

+ 8 - 6
prisma/schema.prisma

@@ -342,12 +342,14 @@ model ServiceAttachment {
 }
 
 model ServicePart {
-  id         String  @id @default(cuid())
-  partNumber String?
-  name       String
-  quantity   Float   @default(1)
-  unitPrice  Float   @default(0)
-  total      Float   @default(0)
+  id              String  @id @default(cuid())
+  partNumber      String?
+  name            String
+  quantity        Float   @default(1)
+  unitPrice       Float   @default(0)
+  total           Float   @default(0)
+  unitCost        Float   @default(0)
+  inventoryPartId String?
 
   serviceRecordId String
   serviceRecord   ServiceRecord @relation(fields: [serviceRecordId], references: [id], onDelete: Cascade)

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

@@ -203,7 +203,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
     const h = (key: string) => t(`csvHeaders.${key}`);
     switch (activeTab) {
       case "revenue":
-        if (revenueData) exportRevenueCsv(revenueData, currencyCode, [h("month"), h("revenue"), h("collected"), h("count")]);
+        if (revenueData) exportRevenueCsv(revenueData, currencyCode, [h("month"), h("revenue"), h("collected"), h("count"), h("partsCost"), h("netProfit")]);
         break;
       case "services":
         if (serviceData) exportServicesCsv(serviceData, [h("category"), h("label"), h("count")]);
@@ -332,7 +332,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
         {!loading && revenueData && (
           <div className="space-y-4">
             {/* Summary cards */}
-            <div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
+            <div className="grid gap-3 grid-cols-2 lg: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-blue-500/10">
@@ -343,6 +343,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
                     <p className="text-lg font-semibold truncate">
                       {fmtCurrency(revenueData.summary.totalRevenue)}
                     </p>
+                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.revenueDesc")}</p>
                   </div>
                 </CardContent>
               </Card>
@@ -356,6 +357,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
                     <p className="text-lg font-semibold truncate">
                       {fmtCurrency(revenueData.summary.totalCollected)}
                     </p>
+                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.collectedDesc")}</p>
                   </div>
                 </CardContent>
               </Card>
@@ -369,6 +371,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
                     <p className="text-lg font-semibold truncate">
                       {fmtCurrency(revenueData.summary.outstanding)}
                     </p>
+                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.outstandingDesc")}</p>
                   </div>
                 </CardContent>
               </Card>
@@ -382,6 +385,35 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
                     <p className="text-lg font-semibold">
                       {revenueData.summary.totalCount}
                     </p>
+                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.servicesDesc")}</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-orange-500/10">
+                    <Package className="h-4 w-4 text-orange-500" />
+                  </div>
+                  <div className="min-w-0">
+                    <p className="text-xs text-muted-foreground">{t("revenue.partsCost")}</p>
+                    <p className="text-lg font-semibold truncate">
+                      {fmtCurrency(revenueData.summary.totalPartsCost)}
+                    </p>
+                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.partsCostDesc")}</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-green-500/10">
+                    <TrendingUp className="h-4 w-4 text-green-500" />
+                  </div>
+                  <div className="min-w-0">
+                    <p className="text-xs text-muted-foreground">{t("revenue.netProfit")}</p>
+                    <p className="text-lg font-semibold truncate">
+                      {fmtCurrency(revenueData.summary.netProfit)}
+                    </p>
+                    <p className="text-[10px] leading-tight text-muted-foreground/70">{t("revenue.netProfitDesc")}</p>
                   </div>
                 </CardContent>
               </Card>
@@ -397,7 +429,7 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
                   <RevenueBarChart
                     data={revenueData.monthly}
                     formatCurrency={fmtCurrency}
-                    labels={{ revenue: t("charts.revenue"), collected: t("charts.collected") }}
+                    labels={{ revenue: t("charts.revenue"), collected: t("charts.collected"), netProfit: t("revenue.netProfit") }}
                   />
                 </CardContent>
               </Card>
@@ -424,6 +456,8 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
                         <TableHead>{t("revenue.tableHeaders.month")}</TableHead>
                         <TableHead className="text-right">{t("revenue.tableHeaders.revenue")}</TableHead>
                         <TableHead className="text-right">{t("revenue.tableHeaders.collected")}</TableHead>
+                        <TableHead className="text-right">{t("revenue.tableHeaders.partsCost")}</TableHead>
+                        <TableHead className="text-right">{t("revenue.tableHeaders.netProfit")}</TableHead>
                         <TableHead className="text-right">{t("revenue.tableHeaders.count")}</TableHead>
                       </TableRow>
                     </TableHeader>
@@ -437,6 +471,12 @@ export default function ReportsClient({ currencyCode }: ReportsClientProps) {
                           <TableCell className="text-right text-sm">
                             {fmtCurrency(row.collected)}
                           </TableCell>
+                          <TableCell className="text-right text-sm">
+                            {fmtCurrency(row.partsCost)}
+                          </TableCell>
+                          <TableCell className="text-right text-sm">
+                            {fmtCurrency(row.netProfit)}
+                          </TableCell>
                           <TableCell className="text-right text-sm">{row.count}</TableCell>
                         </TableRow>
                       ))}

+ 1 - 0
src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx

@@ -128,6 +128,7 @@ export default async function ServiceDetailPage({
       quantity: p.quantity,
       unitPrice: p.unitPrice,
       total: p.total,
+      unitCost: p.unitCost ?? 0,
     })),
     laborItems: record.laborItems.map((l) => ({
       description: l.description,

+ 20 - 4
src/features/reports/Actions/reportActions.ts

@@ -25,15 +25,17 @@ export async function getRevenueReport(params: {
         type: true,
         manuallyPaid: true,
         payments: { select: { amount: true } },
+        partItems: { select: { unitCost: true, quantity: true } },
       },
       orderBy: { serviceDate: "asc" },
     });
 
     // Monthly breakdown
-    const monthly: Record<string, { revenue: number; collected: number; count: number }> = {};
+    const monthly: Record<string, { revenue: number; collected: number; count: number; partsCost: number }> = {};
     let totalRevenue = 0;
     let totalCollected = 0;
     let totalCount = 0;
+    let totalPartsCost = 0;
 
     // Type breakdown
     const byType: Record<string, { revenue: number; count: number }> = {};
@@ -41,16 +43,19 @@ export async function getRevenueReport(params: {
     for (const r of records) {
       const total = r.totalAmount > 0 ? r.totalAmount : r.cost;
       const paid = r.manuallyPaid ? total : r.payments.reduce((s, p) => s + p.amount, 0);
+      const partsCost = r.partItems.reduce((s, p) => s + (p.unitCost * p.quantity), 0);
       const month = `${r.serviceDate.getFullYear()}-${String(r.serviceDate.getMonth() + 1).padStart(2, "0")}`;
 
-      if (!monthly[month]) monthly[month] = { revenue: 0, collected: 0, count: 0 };
+      if (!monthly[month]) monthly[month] = { revenue: 0, collected: 0, count: 0, partsCost: 0 };
       monthly[month].revenue += total;
       monthly[month].collected += paid;
       monthly[month].count += 1;
+      monthly[month].partsCost += partsCost;
 
       totalRevenue += total;
       totalCollected += paid;
       totalCount += 1;
+      totalPartsCost += partsCost;
 
       if (!byType[r.type]) byType[r.type] = { revenue: 0, count: 0 };
       byType[r.type].revenue += total;
@@ -58,9 +63,20 @@ export async function getRevenueReport(params: {
     }
 
     return {
-      monthly: Object.entries(monthly).map(([month, data]) => ({ month, ...data })),
+      monthly: Object.entries(monthly).map(([month, data]) => ({
+        month,
+        ...data,
+        netProfit: data.revenue - data.partsCost,
+      })),
       byType: Object.entries(byType).map(([type, data]) => ({ type, ...data })),
-      summary: { totalRevenue, totalCollected, outstanding: totalRevenue - totalCollected, totalCount },
+      summary: {
+        totalRevenue,
+        totalCollected,
+        outstanding: totalRevenue - totalCollected,
+        totalCount,
+        totalPartsCost,
+        netProfit: totalRevenue - totalPartsCost,
+      },
     };
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.REPORTS }] });
 }

+ 2 - 1
src/features/reports/Components/RevenueCharts.tsx

@@ -29,7 +29,7 @@ const CHART_COLORS = [
 interface RevenueBarChartProps {
   data: MonthlyRevenue[];
   formatCurrency: (value: number) => string;
-  labels?: { revenue: string; collected: string };
+  labels?: { revenue: string; collected: string; netProfit?: string };
 }
 
 export function RevenueBarChart({ data, formatCurrency, labels }: RevenueBarChartProps) {
@@ -60,6 +60,7 @@ export function RevenueBarChart({ data, formatCurrency, labels }: RevenueBarChar
         />
         <Bar dataKey="revenue" name={labels?.revenue ?? "Revenue"} fill="#3b82f6" radius={[4, 4, 0, 0]} />
         <Bar dataKey="collected" name={labels?.collected ?? "Collected"} fill="#10b981" radius={[4, 4, 0, 0]} />
+        <Bar dataKey="netProfit" name={labels?.netProfit ?? "Net Profit"} fill="#10b981" radius={[4, 4, 0, 0]} />
       </BarChart>
     </ResponsiveContainer>
   );

+ 4 - 2
src/features/reports/Components/csv-export.ts

@@ -41,7 +41,7 @@ function downloadCsv(
 export function exportRevenueCsv(
   data: RevenueReport,
   currencyCode: string,
-  headers: [string, string, string, string] = ["Month", "Revenue", "Collected", "Count"],
+  headers: [string, string, string, string, string, string] = ["Month", "Revenue", "Collected", "Count", "Parts Cost", "Net Profit"],
 ) {
   const fmt = (n: number) => formatCurrency(n, currencyCode);
   const rows = data.monthly.map((m) => ({
@@ -49,8 +49,10 @@ export function exportRevenueCsv(
     revenue: fmt(m.revenue),
     collected: fmt(m.collected),
     count: m.count,
+    partsCost: fmt(m.partsCost),
+    netProfit: fmt(m.netProfit),
   }));
-  downloadCsv("revenue-report.csv", headers, rows, ["month", "revenue", "collected", "count"]);
+  downloadCsv("revenue-report.csv", headers, rows, ["month", "revenue", "collected", "count", "partsCost", "netProfit"]);
 }
 
 export function exportServicesCsv(

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

@@ -3,6 +3,8 @@ export interface MonthlyRevenue {
   revenue: number;
   collected: number;
   count: number;
+  partsCost: number;
+  netProfit: number;
 }
 
 export interface RevenueByType {
@@ -16,6 +18,8 @@ export interface RevenueSummary {
   totalCollected: number;
   outstanding: number;
   totalCount: number;
+  totalPartsCost: number;
+  netProfit: number;
 }
 
 export interface RevenueReport {

+ 26 - 13
src/features/vehicles/Actions/serviceActions.ts

@@ -270,21 +270,16 @@ export async function createServiceRecord(input: unknown) {
       });
 
       if (partItems && partItems.length > 0) {
-        await tx.servicePart.createMany({
-          data: partItems.map((p) => {
-            // eslint-disable-next-line @typescript-eslint/no-unused-vars
-            const { inventoryPartId: _inv, ...partData } = p;
-            return { ...partData, serviceRecordId: created.id };
-          }),
-        });
-
-        // Deduct inventory stock for parts sourced from inventory
+        // Enrich parts with unitCost from inventory and deduct stock
+        const enrichedParts = [];
         for (const p of partItems) {
+          let resolvedUnitCost = p.unitCost ?? 0;
           if (p.inventoryPartId) {
             const invPart = await tx.inventoryPart.findFirst({
               where: { id: p.inventoryPartId, organizationId },
             });
             if (invPart) {
+              resolvedUnitCost = invPart.unitCost ?? resolvedUnitCost;
               const newQty = invPart.quantity - Math.ceil(p.quantity);
               await tx.inventoryPart.update({
                 where: { id: p.inventoryPartId },
@@ -292,7 +287,19 @@ export async function createServiceRecord(input: unknown) {
               });
             }
           }
+          enrichedParts.push({
+            partNumber: p.partNumber,
+            name: p.name,
+            quantity: p.quantity,
+            unitPrice: p.unitPrice,
+            total: p.total,
+            unitCost: resolvedUnitCost,
+            inventoryPartId: p.inventoryPartId || null,
+            serviceRecordId: created.id,
+          });
         }
+
+        await tx.servicePart.createMany({ data: enrichedParts });
       }
 
       if (laborItems && laborItems.length > 0) {
@@ -399,10 +406,16 @@ export async function updateServiceRecord(input: unknown) {
         await tx.servicePart.deleteMany({ where: { serviceRecordId: id } });
         if (partItems.length > 0) {
           await tx.servicePart.createMany({
-            data: partItems.map((p) => {
-              const { inventoryPartId: _inv, ...partData } = p;
-              return { ...partData, serviceRecordId: id };
-            }),
+            data: partItems.map((p) => ({
+              partNumber: p.partNumber,
+              name: p.name,
+              quantity: p.quantity,
+              unitPrice: p.unitPrice,
+              total: p.total,
+              unitCost: p.unitCost ?? 0,
+              inventoryPartId: p.inventoryPartId || null,
+              serviceRecordId: id,
+            })),
           });
         }
       }

+ 1 - 0
src/features/vehicles/Components/service-edit/InventoryPickerDialog.tsx

@@ -95,6 +95,7 @@ export function InventoryPickerDialog({
                   quantity: 1,
                   unitPrice: price,
                   total: price,
+                  unitCost: ip.unitCost,
                   inventoryPartId: ip.id,
                 })
                 onOpenChange(false)

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

@@ -72,6 +72,7 @@ export const emptyPart = (): ServicePartInput => ({
   quantity: 1,
   unitPrice: 0,
   total: 0,
+  unitCost: 0,
 })
 
 export const makeEmptyLabor = (defaultRate: number): ServiceLaborInput => ({

+ 1 - 0
src/features/vehicles/Components/service-page/ServicePageClient.tsx

@@ -76,6 +76,7 @@ export function ServicePageClient({
         quantity: 1,
         unitPrice: price,
         total: price,
+        unitCost: part.unitCost,
         inventoryPartId: part.id,
       }])
       toast.success(t('parts.partFound', { name: part.name }))

+ 1 - 0
src/features/vehicles/Schema/serviceSchema.ts

@@ -6,6 +6,7 @@ export const servicePartSchema = z.object({
   quantity: z.coerce.number().min(0).default(1),
   unitPrice: z.coerce.number().min(0).default(0),
   total: z.coerce.number().min(0).default(0),
+  unitCost: z.coerce.number().min(0).default(0),
   inventoryPartId: z.string().optional(),
 });