|
|
@@ -306,7 +306,7 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
if (activeTab === "financial") {
|
|
|
switch (financialSubTab) {
|
|
|
case "revenue":
|
|
|
- if (revenueData) exportRevenueCsv(revenueData, currencyCode, [h("month"), h("revenue"), h("collected"), h("count"), h("partsCost"), h("netProfit")]);
|
|
|
+ if (revenueData) exportRevenueCsv(revenueData, currencyCode, [h("month"), h("revenue"), h("collected"), h("count"), h("partsCost"), h("partsNetProfit"), h("laborRevenue"), h("netProfit")]);
|
|
|
break;
|
|
|
case "past-due-invoices":
|
|
|
if (pastDueData) exportPastDueInvoicesCsv(pastDueData, currencyCode, [h("customer"), h("company"), h("invoiceNumber"), h("totalAmount"), h("amountPaid"), h("amountDue"), h("dueDate"), h("daysPastDue")]);
|
|
|
@@ -330,7 +330,7 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
if (technicianData) exportTechniciansCsv(technicianData, currencyCode, [h("technician"), h("jobs"), h("totalRevenue"), h("avgRevenue"), h("totalHours"), h("avgHours")]);
|
|
|
break;
|
|
|
case "parts":
|
|
|
- if (partsData) exportPartsCsv(partsData, currencyCode, [h("partName"), h("partNumber"), h("usageCount"), h("totalQty"), h("totalRevenue")]);
|
|
|
+ if (partsData) exportPartsCsv(partsData, currencyCode, [h("partName"), h("partNumber"), h("usageCount"), h("totalQty"), h("totalRevenue"), h("partsCost"), h("netProfit")]);
|
|
|
break;
|
|
|
case "job-analytics":
|
|
|
if (jobAnalyticsData) exportJobAnalyticsCsv(jobAnalyticsData, currencyCode, [h("serviceType"), h("count"), h("avgValue"), h("avgHours")]);
|
|
|
@@ -380,6 +380,8 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
outstanding: t("pdf.outstanding"),
|
|
|
services: t("pdf.services"),
|
|
|
partsCost: t("pdf.partsCost"),
|
|
|
+ partsNetProfit: t("pdf.partsNetProfit"),
|
|
|
+ laborRevenue: t("pdf.laborRevenue"),
|
|
|
netProfit: t("pdf.netProfit"),
|
|
|
month: t("pdf.month"),
|
|
|
count: t("pdf.count"),
|
|
|
@@ -419,6 +421,8 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
usageCount: t("pdf.usageCount"),
|
|
|
totalQty: t("pdf.totalQty"),
|
|
|
totalPartsRevenue: t("pdf.totalPartsRevenue"),
|
|
|
+ totalPartsCost: t("pdf.totalPartsCost"),
|
|
|
+ totalPartsNetProfit: t("pdf.totalPartsNetProfit"),
|
|
|
totalPartsUsed: t("pdf.totalPartsUsed"),
|
|
|
serviceType: t("pdf.serviceType"),
|
|
|
avgValue: t("pdf.avgValue"),
|
|
|
@@ -681,92 +685,56 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
<TabsContent value="revenue">
|
|
|
{!loading && revenueData && (
|
|
|
<div className="space-y-4">
|
|
|
- <div className="grid gap-3 grid-cols-2 lg:grid-cols-3">
|
|
|
+ {/* Top-line metrics */}
|
|
|
+ <div className="grid gap-2 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">
|
|
|
- <DollarSign className="h-4 w-4 text-blue-500" />
|
|
|
- </div>
|
|
|
- <div className="min-w-0">
|
|
|
- <p className="text-xs text-muted-foreground">{t("revenue.revenue")}</p>
|
|
|
- <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 className="px-3 py-1.5">
|
|
|
+ <p className="text-[11px] text-muted-foreground">{t("revenue.revenue")}</p>
|
|
|
+ <p className="text-base font-semibold truncate">{fmtCurrency(revenueData.summary.totalRevenue)}</p>
|
|
|
</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">{t("revenue.collected")}</p>
|
|
|
- <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 className="px-3 py-1.5">
|
|
|
+ <p className="text-[11px] text-muted-foreground">{t("revenue.collected")}</p>
|
|
|
+ <p className="text-base font-semibold truncate">{fmtCurrency(revenueData.summary.totalCollected)}</p>
|
|
|
</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-amber-500/10">
|
|
|
- <AlertTriangle className="h-4 w-4 text-amber-500" />
|
|
|
- </div>
|
|
|
- <div className="min-w-0">
|
|
|
- <p className="text-xs text-muted-foreground">{t("revenue.outstanding")}</p>
|
|
|
- <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>
|
|
|
- <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">{t("revenue.services")}</p>
|
|
|
- <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 className="px-3 py-1.5">
|
|
|
+ <p className="text-[11px] text-muted-foreground">{t("revenue.outstanding")}</p>
|
|
|
+ <p className="text-base font-semibold truncate">{fmtCurrency(revenueData.summary.outstanding)}</p>
|
|
|
</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>
|
|
|
+
|
|
|
+ {/* Net Profit breakdown */}
|
|
|
+ <Card className="border-0 shadow-sm">
|
|
|
+ <CardHeader className="pb-2">
|
|
|
+ <CardTitle className="text-sm font-medium">{t("revenue.netProfit")}</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="space-y-3">
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <div className="h-2 w-2 rounded-full bg-green-500" />
|
|
|
+ <span className="text-sm text-muted-foreground">{t("revenue.partsNetProfit")}</span>
|
|
|
+ </div>
|
|
|
+ <span className="text-sm font-medium tabular-nums">{fmtCurrency(revenueData.summary.totalPartsNetProfit)}</span>
|
|
|
</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 className="flex items-center justify-between">
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
+ <div className="h-2 w-2 rounded-full bg-blue-500" />
|
|
|
+ <span className="text-sm text-muted-foreground">{t("revenue.laborRevenue")}</span>
|
|
|
+ </div>
|
|
|
+ <span className="text-sm font-medium tabular-nums">{fmtCurrency(revenueData.summary.totalLaborRevenue)}</span>
|
|
|
</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 className="border-t pt-3 flex items-center justify-between">
|
|
|
+ <span className="text-sm font-medium">{t("revenue.netProfit")}</span>
|
|
|
+ <span className="text-base font-semibold tabular-nums">{fmtCurrency(revenueData.summary.netProfit)}</span>
|
|
|
</div>
|
|
|
- </CardContent>
|
|
|
- </Card>
|
|
|
- </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
|
|
|
<div className="grid gap-4 lg:grid-cols-5">
|
|
|
<Card className="border-0 shadow-sm lg:col-span-3">
|
|
|
@@ -804,6 +772,8 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
<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.partsNetProfit")}</TableHead>
|
|
|
+ <TableHead className="text-right">{t("revenue.tableHeaders.laborRevenue")}</TableHead>
|
|
|
<TableHead className="text-right">{t("revenue.tableHeaders.netProfit")}</TableHead>
|
|
|
<TableHead className="text-right">{t("revenue.tableHeaders.count")}</TableHead>
|
|
|
</TableRow>
|
|
|
@@ -821,6 +791,12 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
<TableCell className="text-right text-sm">
|
|
|
{fmtCurrency(row.partsCost)}
|
|
|
</TableCell>
|
|
|
+ <TableCell className="text-right text-sm">
|
|
|
+ {fmtCurrency(row.partsNetProfit)}
|
|
|
+ </TableCell>
|
|
|
+ <TableCell className="text-right text-sm">
|
|
|
+ {fmtCurrency(row.laborRevenue)}
|
|
|
+ </TableCell>
|
|
|
<TableCell className="text-right text-sm">
|
|
|
{fmtCurrency(row.netProfit)}
|
|
|
</TableCell>
|
|
|
@@ -1450,7 +1426,7 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
<TabsContent value="parts">
|
|
|
{!loading && partsData && (
|
|
|
<div className="space-y-4">
|
|
|
- <div className="grid gap-3 grid-cols-2 max-w-lg">
|
|
|
+ <div className="grid gap-3 grid-cols-2 lg:grid-cols-4">
|
|
|
<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">
|
|
|
@@ -1475,6 +1451,32 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
</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("parts.partsCost")}</p>
|
|
|
+ <p className="text-lg font-semibold truncate">
|
|
|
+ {fmtCurrency(partsData.totalPartsCost)}
|
|
|
+ </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("parts.netProfit")}</p>
|
|
|
+ <p className="text-lg font-semibold truncate">
|
|
|
+ {fmtCurrency(partsData.totalPartsNetProfit)}
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
</div>
|
|
|
{partsData.parts.length > 0 && (
|
|
|
<Card className="border-0 shadow-sm">
|
|
|
@@ -1500,6 +1502,8 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
<TableHead className="text-right">{t("parts.tableHeaders.usageCount")}</TableHead>
|
|
|
<TableHead className="text-right">{t("parts.tableHeaders.totalQty")}</TableHead>
|
|
|
<TableHead className="text-right">{t("parts.tableHeaders.revenue")}</TableHead>
|
|
|
+ <TableHead className="text-right">{t("parts.tableHeaders.cost")}</TableHead>
|
|
|
+ <TableHead className="text-right">{t("parts.tableHeaders.netProfit")}</TableHead>
|
|
|
</TableRow>
|
|
|
</TableHeader>
|
|
|
<TableBody>
|
|
|
@@ -1510,6 +1514,8 @@ export default function ReportsClient({ currencyCode, primaryColor }: ReportsCli
|
|
|
<TableCell className="text-right text-sm">{row.usageCount}</TableCell>
|
|
|
<TableCell className="text-right text-sm">{row.totalQuantity}</TableCell>
|
|
|
<TableCell className="text-right text-sm">{fmtCurrency(row.totalRevenue)}</TableCell>
|
|
|
+ <TableCell className="text-right text-sm">{fmtCurrency(row.totalCost)}</TableCell>
|
|
|
+ <TableCell className="text-right text-sm">{fmtCurrency(row.netProfit)}</TableCell>
|
|
|
</TableRow>
|
|
|
))}
|
|
|
</TableBody>
|