|
@@ -4,6 +4,7 @@ import { db } from "@/lib/db";
|
|
|
import { withAuth } from "@/lib/with-auth";
|
|
import { withAuth } from "@/lib/with-auth";
|
|
|
import { PermissionAction, PermissionSubject } from "@/lib/permissions";
|
|
import { PermissionAction, PermissionSubject } from "@/lib/permissions";
|
|
|
import { Prisma } from "@/generated/prisma/client";
|
|
import { Prisma } from "@/generated/prisma/client";
|
|
|
|
|
+import { effectiveDateSql } from "@/lib/date-sort";
|
|
|
|
|
|
|
|
interface BillingSummary {
|
|
interface BillingSummary {
|
|
|
totalRevenue: number;
|
|
totalRevenue: number;
|
|
@@ -64,6 +65,8 @@ export async function getBillingHistory(params: {
|
|
|
pageSize?: number;
|
|
pageSize?: number;
|
|
|
search?: string;
|
|
search?: string;
|
|
|
status?: string;
|
|
status?: string;
|
|
|
|
|
+ sortBy?: string;
|
|
|
|
|
+ sortOrder?: "asc" | "desc";
|
|
|
}) {
|
|
}) {
|
|
|
return withAuth(async ({ organizationId }) => {
|
|
return withAuth(async ({ organizationId }) => {
|
|
|
const page = params.page || 1;
|
|
const page = params.page || 1;
|
|
@@ -72,21 +75,10 @@ export async function getBillingHistory(params: {
|
|
|
|
|
|
|
|
const summary = await getBillingSummary(organizationId);
|
|
const summary = await getBillingSummary(organizationId);
|
|
|
|
|
|
|
|
- // Build where clause
|
|
|
|
|
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
- const where: any = { vehicle: { organizationId } };
|
|
|
|
|
-
|
|
|
|
|
- if (params.search) {
|
|
|
|
|
- where.OR = [
|
|
|
|
|
- { title: { contains: params.search, mode: "insensitive" } },
|
|
|
|
|
- { invoiceNumber: { contains: params.search, mode: "insensitive" } },
|
|
|
|
|
- { vehicle: { make: { contains: params.search, mode: "insensitive" } } },
|
|
|
|
|
- { vehicle: { model: { contains: params.search, mode: "insensitive" } } },
|
|
|
|
|
- ];
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // When filtering by payment status, use raw SQL for efficiency
|
|
|
|
|
- if (params.status === "paid" || params.status === "partial" || params.status === "unpaid") {
|
|
|
|
|
|
|
+ // Raw SQL for every path: payment status and effective total are computed
|
|
|
|
|
+ // values, and the displayed date is COALESCE(invoiceDate, startDateTime,
|
|
|
|
|
+ // serviceDate) — only SQL can filter and sort by them exactly.
|
|
|
|
|
+ {
|
|
|
const searchCondition = params.search
|
|
const searchCondition = params.search
|
|
|
? Prisma.sql`AND (
|
|
? Prisma.sql`AND (
|
|
|
sr.title ILIKE ${`%${params.search}%`}
|
|
sr.title ILIKE ${`%${params.search}%`}
|
|
@@ -101,8 +93,10 @@ export async function getBillingHistory(params: {
|
|
|
statusCondition = Prisma.sql`AND (manually_paid = true OR (paid_amount >= effective_total AND effective_total > 0))`;
|
|
statusCondition = Prisma.sql`AND (manually_paid = true OR (paid_amount >= effective_total AND effective_total > 0))`;
|
|
|
} else if (params.status === "partial") {
|
|
} else if (params.status === "partial") {
|
|
|
statusCondition = Prisma.sql`AND manually_paid = false AND paid_amount > 0 AND paid_amount < effective_total`;
|
|
statusCondition = Prisma.sql`AND manually_paid = false AND paid_amount > 0 AND paid_amount < effective_total`;
|
|
|
- } else {
|
|
|
|
|
|
|
+ } else if (params.status === "unpaid") {
|
|
|
statusCondition = Prisma.sql`AND manually_paid = false AND paid_amount = 0`;
|
|
statusCondition = Prisma.sql`AND manually_paid = false AND paid_amount = 0`;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ statusCondition = Prisma.empty;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const countRows = await db.$queryRaw<{ cnt: bigint }[]>(Prisma.sql`
|
|
const countRows = await db.$queryRaw<{ cnt: bigint }[]>(Prisma.sql`
|
|
@@ -190,7 +184,18 @@ export async function getBillingHistory(params: {
|
|
|
${searchCondition}
|
|
${searchCondition}
|
|
|
) sub
|
|
) sub
|
|
|
WHERE 1=1 ${statusCondition}
|
|
WHERE 1=1 ${statusCondition}
|
|
|
- ORDER BY COALESCE(sub."invoiceDate", sub."startDateTime", sub."serviceDate") DESC
|
|
|
|
|
|
|
+ ORDER BY ${(() => {
|
|
|
|
|
+ const dir = params.sortOrder === "asc" ? Prisma.sql`ASC` : Prisma.sql`DESC`;
|
|
|
|
|
+ switch (params.sortBy) {
|
|
|
|
|
+ case "invoiceNumber": return Prisma.sql`sub."invoiceNumber" ${dir} NULLS LAST`;
|
|
|
|
|
+ case "title": return Prisma.sql`sub.title ${dir}`;
|
|
|
|
|
+ case "vehicle": return Prisma.sql`sub.vehicle_make ${dir}, sub.vehicle_model ${dir}, sub.vehicle_year ${dir}`;
|
|
|
|
|
+ case "customer": return Prisma.sql`sub.customer_name ${dir} NULLS LAST`;
|
|
|
|
|
+ case "total": return Prisma.sql`sub.effective_total ${dir}`;
|
|
|
|
|
+ case "date": return Prisma.sql`${effectiveDateSql("sub")} ${dir}`;
|
|
|
|
|
+ default: return Prisma.sql`${effectiveDateSql("sub")} DESC`;
|
|
|
|
|
+ }
|
|
|
|
|
+ })()}
|
|
|
LIMIT ${pageSize} OFFSET ${skip}
|
|
LIMIT ${pageSize} OFFSET ${skip}
|
|
|
`);
|
|
`);
|
|
|
|
|
|
|
@@ -230,62 +235,5 @@ export async function getBillingHistory(params: {
|
|
|
summary,
|
|
summary,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- // No status filter — use Prisma for clean pagination
|
|
|
|
|
- const [records, total] = await Promise.all([
|
|
|
|
|
- db.serviceRecord.findMany({
|
|
|
|
|
- where,
|
|
|
|
|
- include: {
|
|
|
|
|
- payments: { select: { amount: true } },
|
|
|
|
|
- vehicle: {
|
|
|
|
|
- select: {
|
|
|
|
|
- id: true,
|
|
|
|
|
- make: true,
|
|
|
|
|
- model: true,
|
|
|
|
|
- year: true,
|
|
|
|
|
- licensePlate: true,
|
|
|
|
|
- customer: { select: { id: true, name: true } },
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- },
|
|
|
|
|
- orderBy: [
|
|
|
|
|
- { invoiceDate: { sort: "desc", nulls: "last" } },
|
|
|
|
|
- { startDateTime: { sort: "desc", nulls: "last" } },
|
|
|
|
|
- { serviceDate: "desc" },
|
|
|
|
|
- ],
|
|
|
|
|
- skip,
|
|
|
|
|
- take: pageSize,
|
|
|
|
|
- }),
|
|
|
|
|
- db.serviceRecord.count({ where }),
|
|
|
|
|
- ]);
|
|
|
|
|
-
|
|
|
|
|
- return {
|
|
|
|
|
- records: records.map((r) => {
|
|
|
|
|
- const effectiveTotal = r.totalAmount > 0 ? r.totalAmount : r.cost;
|
|
|
|
|
- const paidFromPayments = r.payments.reduce((s, p) => s + p.amount, 0);
|
|
|
|
|
- const totalPaid = r.manuallyPaid ? effectiveTotal : paidFromPayments;
|
|
|
|
|
- const paymentStatus = r.manuallyPaid || (totalPaid >= effectiveTotal && effectiveTotal > 0)
|
|
|
|
|
- ? "paid"
|
|
|
|
|
- : totalPaid > 0
|
|
|
|
|
- ? "partial"
|
|
|
|
|
- : "unpaid";
|
|
|
|
|
- return {
|
|
|
|
|
- id: r.id,
|
|
|
|
|
- title: r.title,
|
|
|
|
|
- invoiceNumber: r.invoiceNumber,
|
|
|
|
|
- startDateTime: r.startDateTime,
|
|
|
|
|
- serviceDate: r.invoiceDate ?? r.startDateTime ?? r.serviceDate,
|
|
|
|
|
- totalAmount: effectiveTotal,
|
|
|
|
|
- totalPaid,
|
|
|
|
|
- status: paymentStatus,
|
|
|
|
|
- vehicle: r.vehicle,
|
|
|
|
|
- };
|
|
|
|
|
- }),
|
|
|
|
|
- total,
|
|
|
|
|
- page,
|
|
|
|
|
- pageSize,
|
|
|
|
|
- totalPages: Math.ceil(total / pageSize),
|
|
|
|
|
- summary,
|
|
|
|
|
- };
|
|
|
|
|
}, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.BILLING }] });
|
|
}, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.BILLING }] });
|
|
|
}
|
|
}
|