mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 23:28:11 +00:00
mile
This commit is contained in:
@@ -307,6 +307,16 @@ export class BillingService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Invoices for a batch of source records (e.g. many last-mile legs), so a
|
||||||
|
* list can show which records already have an invoice without N+1 queries. */
|
||||||
|
findBySourceIds(source: string, sourceIds: string[]): Promise<Invoice[]> {
|
||||||
|
if (!sourceIds.length) return Promise.resolve([]);
|
||||||
|
return this.invoices.findAll({
|
||||||
|
where: { source, sourceId: In(sourceIds) },
|
||||||
|
order: { createdAt: "DESC" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Invoices for the signed-in customer; empty when they have no company. */
|
/** Invoices for the signed-in customer; empty when they have no company. */
|
||||||
async findForUser(
|
async findForUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { LastMile, LastMileStatus } from './entities/last-mile.entity';
|
|||||||
import { LastMileContainerAllocation } from './entities/last-mile-container-allocation.entity';
|
import { LastMileContainerAllocation } from './entities/last-mile-container-allocation.entity';
|
||||||
import { LastMileVehicleAssignment } from './entities/last-mile-vehicle-assignment.entity';
|
import { LastMileVehicleAssignment } from './entities/last-mile-vehicle-assignment.entity';
|
||||||
import { LastMileRepository } from './last-mile.repository';
|
import { LastMileRepository } from './last-mile.repository';
|
||||||
import { InvoiceEventPayload } from '../billing/billing.service';
|
import { BillingService, InvoiceEventPayload } from '../billing/billing.service';
|
||||||
import { OnEvent } from '@nestjs/event-emitter';
|
import { OnEvent } from '@nestjs/event-emitter';
|
||||||
import { FleetHistoryService } from '../fleet-history/fleet-history.service';
|
import { FleetHistoryService } from '../fleet-history/fleet-history.service';
|
||||||
import { FleetEventType } from '../fleet-history/entities/fleet-event.entity';
|
import { FleetEventType } from '../fleet-history/entities/fleet-event.entity';
|
||||||
@@ -46,8 +46,28 @@ export class LastMileService {
|
|||||||
private readonly smsClient: SmsClientService,
|
private readonly smsClient: SmsClientService,
|
||||||
private readonly dataSource: DataSource,
|
private readonly dataSource: DataSource,
|
||||||
private readonly history: FleetHistoryService,
|
private readonly history: FleetHistoryService,
|
||||||
|
private readonly billing: BillingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
/** Attach real invoice info (number/status) to records so the UI can show an
|
||||||
|
* invoice link only when one actually exists — NOT merely because distance
|
||||||
|
* was entered. Batched to avoid N+1. */
|
||||||
|
private async attachInvoices(records: LastMile[]): Promise<void> {
|
||||||
|
const invoices = await this.billing.findBySourceIds(
|
||||||
|
'last_mile',
|
||||||
|
records.map((r) => r.id),
|
||||||
|
);
|
||||||
|
const byId = new Map<string, { number: string; status: string }>();
|
||||||
|
for (const inv of invoices) {
|
||||||
|
if (!byId.has(inv.sourceId)) {
|
||||||
|
byId.set(inv.sourceId, { number: inv.invoiceNumber, status: String(inv.status) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const r of records) {
|
||||||
|
(r as LastMile & { invoice?: unknown }).invoice = byId.get(r.id) ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Resolve a vehicle's driver + human labels, for stamping mile events onto
|
/** Resolve a vehicle's driver + human labels, for stamping mile events onto
|
||||||
* the driver's timeline and naming the vehicle. Best-effort — never throws. */
|
* the driver's timeline and naming the vehicle. Best-effort — never throws. */
|
||||||
private async vehicleInfo(
|
private async vehicleInfo(
|
||||||
@@ -159,6 +179,8 @@ export class LastMileService {
|
|||||||
take: pageSize,
|
take: pageSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this.attachInvoices(data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
meta: {
|
meta: {
|
||||||
@@ -183,6 +205,8 @@ export class LastMileService {
|
|||||||
throw new NotFoundException(`Last-mile record ${id} not found`);
|
throw new NotFoundException(`Last-mile record ${id} not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.attachInvoices([record]);
|
||||||
|
|
||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1086,35 +1086,25 @@ const LastMilePage = () => {
|
|||||||
header: "Invoice",
|
header: "Invoice",
|
||||||
meta: { headerClassName, cellClassName },
|
meta: { headerClassName, cellClassName },
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const hasDistance = row.original.exactKm != null && row.original.exactKm > 0;
|
// Only show an invoice once it's actually been generated — NOT merely
|
||||||
const isPaid = (row.original as any).paid;
|
// because distance was entered.
|
||||||
if (!hasDistance) {
|
const invoice = row.original.invoice;
|
||||||
|
if (!invoice) {
|
||||||
return <Text c="dimmed">—</Text>;
|
return <Text c="dimmed">—</Text>;
|
||||||
}
|
}
|
||||||
if (isPaid) {
|
const isPaid = (row.original as any).paid || invoice.status === "Paid";
|
||||||
return (
|
|
||||||
<Group gap="xs" wrap="nowrap">
|
|
||||||
<UnstyledButton
|
|
||||||
onClick={() => openInvoice(row.original)}
|
|
||||||
c="blue"
|
|
||||||
fw={500}
|
|
||||||
style={{ textDecoration: "underline", cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
#345
|
|
||||||
</UnstyledButton>
|
|
||||||
<Badge color="green" variant="light" size="sm">Paid</Badge>
|
|
||||||
</Group>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<UnstyledButton
|
<Group gap="xs" wrap="nowrap">
|
||||||
onClick={() => openInvoice(row.original)}
|
<UnstyledButton
|
||||||
c="blue"
|
onClick={() => openInvoice(row.original)}
|
||||||
fw={500}
|
c="blue"
|
||||||
style={{ textDecoration: "underline", cursor: "pointer" }}
|
fw={500}
|
||||||
>
|
style={{ textDecoration: "underline", cursor: "pointer" }}
|
||||||
#345
|
>
|
||||||
</UnstyledButton>
|
{invoice.number}
|
||||||
|
</UnstyledButton>
|
||||||
|
{isPaid && <Badge color="green" variant="light" size="sm">Paid</Badge>}
|
||||||
|
</Group>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ export interface LastMileRecord {
|
|||||||
distanceKm?: number | null;
|
distanceKm?: number | null;
|
||||||
vehicle?: LastMileVehicle | null;
|
vehicle?: LastMileVehicle | null;
|
||||||
}>;
|
}>;
|
||||||
|
/** Present only when an invoice has actually been generated (not on distance). */
|
||||||
|
invoice?: { number: string; status: string } | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user