mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 13:05:44 +00:00
@@ -1,5 +1,17 @@
|
|||||||
# Copy to .env for local/docker compose (not committed).
|
# Copy to .env for local/docker compose (not committed).
|
||||||
PORT=3001
|
PORT=3001
|
||||||
|
# @tria-plc/auditlog's client interceptor stamps every AuditLog row's
|
||||||
|
# `application` from this env var directly, bypassing MezgebModule.forRoot's
|
||||||
|
# applicationName option (package quirk). audit.controller.ts reads the same
|
||||||
|
# var when filtering reads, so this can be anything as long as it's set.
|
||||||
|
APPLICATION_NAME=freight-api
|
||||||
|
# Also required for @tria-plc/auditlog: its producer (AuditClientModule)
|
||||||
|
# reads the RMQ URL at package IMPORT time, before MezgebModule.forRoot's
|
||||||
|
# rmqUrl option ever runs, so only an env var reaches it — an in-code
|
||||||
|
# override is too late. Without this, audit events are silently dropped
|
||||||
|
# (no error, nothing published). Point it at whatever broker/vhost your
|
||||||
|
# RabbitMQ actually has a user provisioned on.
|
||||||
|
RABBITMQ_URL=amqp://localhost:5672
|
||||||
# GT06 GPS tracker TCP listener port (raw TCP, must be reachable by tracker SIMs). 0 disables.
|
# GT06 GPS tracker TCP listener port (raw TCP, must be reachable by tracker SIMs). 0 disables.
|
||||||
GT06_TCP_PORT=5023
|
GT06_TCP_PORT=5023
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
"@nestjs/typeorm": "^11.0.1",
|
"@nestjs/typeorm": "^11.0.1",
|
||||||
"@nestjs/websockets": "^11.1.27",
|
"@nestjs/websockets": "^11.1.27",
|
||||||
"@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.6.0.tgz",
|
"@tria-plc/api-common": "file:../../local-packages/tria-plc-api-common-1.6.0.tgz",
|
||||||
|
"@tria-plc/auditlog": "file:../../local-packages/tria-plc-auditlog-1.1.2.tgz",
|
||||||
"@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz",
|
"@tria-plc/iamapi-common": "file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz",
|
||||||
"amqp-connection-manager": "^5.0.0",
|
"amqp-connection-manager": "^5.0.0",
|
||||||
"amqplib": "^2.0.1",
|
"amqplib": "^2.0.1",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
import { IamBaselineSeeder, IamSeedModule } from "@edr/iam-seed";
|
import { IamBaselineSeeder, IamSeedModule } from "@edr/iam-seed";
|
||||||
import { IamModule } from "@tria-plc/iamapi-common";
|
import { IamModule } from "@tria-plc/iamapi-common";
|
||||||
import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module";
|
import { SharedAuthModule } from "@tria-plc/api-common/modules/auth/shared-auth.module";
|
||||||
|
import { MezgebModule } from "@tria-plc/auditlog";
|
||||||
|
|
||||||
import appConfig from "./config/app.config";
|
import appConfig from "./config/app.config";
|
||||||
import databaseConfig from "./config/database.config";
|
import databaseConfig from "./config/database.config";
|
||||||
@@ -105,9 +106,14 @@ import { LastMileModule } from "./modules/last-mile/last-mile.module";
|
|||||||
import { InterchangeDocumentsModule } from "./modules/interchange-documents/interchange-documents.module";
|
import { InterchangeDocumentsModule } from "./modules/interchange-documents/interchange-documents.module";
|
||||||
import { ImportOperationsModule } from "./modules/import-operations/import-operations.module";
|
import { ImportOperationsModule } from "./modules/import-operations/import-operations.module";
|
||||||
import { AiModule } from "./modules/ai/ai.module";
|
import { AiModule } from "./modules/ai/ai.module";
|
||||||
|
import { AuditModule } from "./modules/audit/audit.module";
|
||||||
import { LoggerMiddleware } from "./logger.middleware";
|
import { LoggerMiddleware } from "./logger.middleware";
|
||||||
import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middleware";
|
import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middleware";
|
||||||
|
|
||||||
|
if (!process.env.APPLICATION_NAME) {
|
||||||
|
process.env.APPLICATION_NAME = "freight";
|
||||||
|
}
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
@@ -155,6 +161,19 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar
|
|||||||
return dataSource;
|
return dataSource;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
// Request + entity-level audit logging over RabbitMQ (@tria-plc/auditlog).
|
||||||
|
// Must come after TypeOrmModule above so it picks up this app's DataSource.
|
||||||
|
// rmqUrl falls back the same way notifications.module.ts's RABBITMQ_URL
|
||||||
|
// does: the dev broker only provisions the `edr` user on the `payment`
|
||||||
|
// vhost (docker-compose's RABBITMQ_DEFAULT_USER/VHOST), so an unset
|
||||||
|
// RABBITMQ_URL must land there too, not on guest@'/' (403 ACCESS_REFUSED).
|
||||||
|
MezgebModule.forRoot({
|
||||||
|
applicationName: "freight-api",
|
||||||
|
rmqUrl:
|
||||||
|
process.env.RABBITMQ_URL ??
|
||||||
|
process.env.PAYMENT_RABBITMQ_URL ??
|
||||||
|
"amqp://localhost:5672",
|
||||||
|
}),
|
||||||
SharedAuthModule,
|
SharedAuthModule,
|
||||||
IamModule.forRoot({
|
IamModule.forRoot({
|
||||||
applications: [EDR_FREIGHT_APPLICATION],
|
applications: [EDR_FREIGHT_APPLICATION],
|
||||||
@@ -227,6 +246,7 @@ import { LoginAudienceMiddleware } from "./modules/auth/login-audience.middlewar
|
|||||||
VerifaydaModule,
|
VerifaydaModule,
|
||||||
FleetHistoryModule,
|
FleetHistoryModule,
|
||||||
AiModule,
|
AiModule,
|
||||||
|
AuditModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
EdrOrgSeeder,
|
EdrOrgSeeder,
|
||||||
|
|||||||
@@ -56,6 +56,11 @@ import { EmployeePositionActivePeriod } from "@tria-plc/iamapi-common/entities/i
|
|||||||
import { UnitConfiguration } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-configuration.entity";
|
import { UnitConfiguration } from "@tria-plc/iamapi-common/entities/iam/organization-structure/unit-configuration.entity";
|
||||||
import { Site } from "@tria-plc/iamapi-common/entities/iam/site/site.entity";
|
import { Site } from "@tria-plc/iamapi-common/entities/iam/site/site.entity";
|
||||||
import { SiteSetting } from "@tria-plc/iamapi-common/entities/iam/site/site-setting.entity";
|
import { SiteSetting } from "@tria-plc/iamapi-common/entities/iam/site/site-setting.entity";
|
||||||
|
import { AuditLog, AuditLogCommand } from "@tria-plc/auditlog";
|
||||||
|
|
||||||
|
// @tria-plc/auditlog's entities live in node_modules, same as the iam ones —
|
||||||
|
// the glob below only matches this app's own src/**/*.entity.ts.
|
||||||
|
const auditEntities = [AuditLog, AuditLogCommand];
|
||||||
|
|
||||||
const iamEntities = [
|
const iamEntities = [
|
||||||
UnitSetting,
|
UnitSetting,
|
||||||
@@ -177,7 +182,11 @@ export function buildDataSourceOptions(): DataSourceOptions {
|
|||||||
return {
|
return {
|
||||||
...buildConnectionOptions(),
|
...buildConnectionOptions(),
|
||||||
schema: "public",
|
schema: "public",
|
||||||
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
|
entities: [
|
||||||
|
__dirname + "/../**/*.entity.{ts,js}",
|
||||||
|
...iamEntities,
|
||||||
|
...auditEntities,
|
||||||
|
],
|
||||||
migrations: [],
|
migrations: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
ResponseTransformInterceptor,
|
ResponseTransformInterceptor,
|
||||||
createValidationPipe,
|
createValidationPipe,
|
||||||
} from "@edr/api-common";
|
} from "@edr/api-common";
|
||||||
|
import { getAuditLoggerConfig } from "@tria-plc/auditlog";
|
||||||
|
|
||||||
import { AppModule } from "./app.module";
|
import { AppModule } from "./app.module";
|
||||||
|
|
||||||
@@ -160,6 +161,13 @@ export async function createFreightApp(): Promise<NestExpressApplication> {
|
|||||||
app.useGlobalFilters(new HttpExceptionFilter());
|
app.useGlobalFilters(new HttpExceptionFilter());
|
||||||
app.useGlobalInterceptors(new ResponseTransformInterceptor());
|
app.useGlobalInterceptors(new ResponseTransformInterceptor());
|
||||||
|
|
||||||
|
// Audit listener: consumes the RMQ events MezgebModule's client interceptor
|
||||||
|
// (app.module.ts) emits and persists them via the AuditLogController /
|
||||||
|
// AuditLogCommandController @EventPattern handlers. Same queue config the
|
||||||
|
// client side uses, reused from the package so the two never drift apart.
|
||||||
|
app.connectMicroservice(getAuditLoggerConfig());
|
||||||
|
await app.startAllMicroservices();
|
||||||
|
|
||||||
const config = new DocumentBuilder()
|
const config = new DocumentBuilder()
|
||||||
.setTitle("EDR Freight API")
|
.setTitle("EDR Freight API")
|
||||||
.setDescription("API for the EDR Freight Management application")
|
.setDescription("API for the EDR Freight Management application")
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds PAYMENT_PROCESSING to the invoice status enum: the customer completed
|
||||||
|
* provider checkout (success redirect) and settlement is awaiting the
|
||||||
|
* provider webhook.
|
||||||
|
*/
|
||||||
|
export class InvoicePaymentProcessingStatus3260000000000
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = "InvoicePaymentProcessingStatus3260000000000";
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TYPE freight.invoices_status_enum ADD VALUE IF NOT EXISTS 'PAYMENT_PROCESSING' AFTER 'PENDING'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(): Promise<void> {
|
||||||
|
// Postgres cannot drop an enum value; PAYMENT_PROCESSING stays. Harmless.
|
||||||
|
}
|
||||||
|
}
|
||||||
32
apps/edr-freight-api/src/modules/audit/audit.controller.ts
Normal file
32
apps/edr-freight-api/src/modules/audit/audit.controller.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Controller, Get, Query } from "@nestjs/common";
|
||||||
|
import { ApiOperation, ApiQuery, ApiTags } from "@nestjs/swagger";
|
||||||
|
|
||||||
|
import { BookingStaff } from "../../common/booking-guards";
|
||||||
|
import { FREIGHT_PERMS } from "../../seed/freight-permissions.registry";
|
||||||
|
import { AuditService } from "./audit.service";
|
||||||
|
|
||||||
|
@ApiTags("audit")
|
||||||
|
@Controller("audit")
|
||||||
|
@BookingStaff(FREIGHT_PERMS.audit.view)
|
||||||
|
export class AuditController {
|
||||||
|
constructor(private readonly auditService: AuditService) {}
|
||||||
|
|
||||||
|
@Get("logs")
|
||||||
|
@ApiOperation({ summary: "List freight-api audit log commands" })
|
||||||
|
@ApiQuery({ name: "skip", type: Number, required: false })
|
||||||
|
@ApiQuery({ name: "take", type: Number, required: false })
|
||||||
|
list(@Query("skip") skip?: string, @Query("take") take?: string) {
|
||||||
|
// Same fallback chain @tria-plc/auditlog's client interceptor uses to
|
||||||
|
// stamp AuditLog.application (mezgeb/client/client-audit.interceptor.js)
|
||||||
|
// — reading it here instead of a hardcoded literal means this can't
|
||||||
|
// silently drift out of sync with whatever APPLICATION_NAME/APP_NAME
|
||||||
|
// actually is at runtime.
|
||||||
|
const application =
|
||||||
|
process.env.APPLICATION_NAME ?? process.env.APP_NAME ?? "DEFAULT";
|
||||||
|
return this.auditService.list(
|
||||||
|
application,
|
||||||
|
skip !== undefined ? parseInt(skip, 10) : undefined,
|
||||||
|
take !== undefined ? parseInt(take, 10) : undefined,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
13
apps/edr-freight-api/src/modules/audit/audit.module.ts
Normal file
13
apps/edr-freight-api/src/modules/audit/audit.module.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from "@nestjs/common";
|
||||||
|
import { TypeOrmModule } from "@nestjs/typeorm";
|
||||||
|
import { AuditLogCommand } from "@tria-plc/auditlog";
|
||||||
|
|
||||||
|
import { AuditController } from "./audit.controller";
|
||||||
|
import { AuditService } from "./audit.service";
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([AuditLogCommand])],
|
||||||
|
controllers: [AuditController],
|
||||||
|
providers: [AuditService],
|
||||||
|
})
|
||||||
|
export class AuditModule {}
|
||||||
70
apps/edr-freight-api/src/modules/audit/audit.service.ts
Normal file
70
apps/edr-freight-api/src/modules/audit/audit.service.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from "@nestjs/common";
|
||||||
|
import { InjectRepository } from "@nestjs/typeorm";
|
||||||
|
import { Repository } from "typeorm";
|
||||||
|
import { AuditLogCommand } from "@tria-plc/auditlog";
|
||||||
|
|
||||||
|
import { CLIENT_APP_HEADER } from "../auth/login-audience.middleware";
|
||||||
|
|
||||||
|
export interface AuditLogListResult {
|
||||||
|
count: number;
|
||||||
|
items: AuditLogCommand[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Own read path onto @tria-plc/auditlog's tables, gated by AuditController's
|
||||||
|
* @BookingStaff — the package's own AuditLogCommandController (mounted at
|
||||||
|
* /api/audit-log-commands) ships with no guards at all, so it can't be used
|
||||||
|
* directly for a permission-gated UI. Query mirrors the package's
|
||||||
|
* AuditLogCommandService.buildAuditLogQuery/getAllAuditLogs exactly.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AuditService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(AuditLogCommand)
|
||||||
|
private readonly auditLogCommandRepository: Repository<AuditLogCommand>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async list(
|
||||||
|
application: string,
|
||||||
|
skip = 0,
|
||||||
|
take = 10,
|
||||||
|
): Promise<AuditLogListResult> {
|
||||||
|
const [items, count] = await this.auditLogCommandRepository
|
||||||
|
.createQueryBuilder("audit_log_commands")
|
||||||
|
.leftJoinAndSelect("audit_log_commands.auditLog", "auditLog")
|
||||||
|
.andWhere(
|
||||||
|
"(audit_log_commands.auditLogId IS NULL OR auditLog.application = :application)",
|
||||||
|
{ application },
|
||||||
|
)
|
||||||
|
.andWhere(
|
||||||
|
"(audit_log_commands.auditLogId IS NULL OR auditLog.status = :status)",
|
||||||
|
{ status: "Commit" },
|
||||||
|
)
|
||||||
|
// Backoffice-only view: portal (customer-facing) writes carry the same
|
||||||
|
// request-header set by every axios call from that app — see
|
||||||
|
// login-audience.middleware.ts. Rows with no linked auditLog (child/
|
||||||
|
// event commands with no request context) stay visible; they aren't
|
||||||
|
// attributable to any frontend, so they're not portal noise either.
|
||||||
|
.andWhere(
|
||||||
|
"(audit_log_commands.auditLogId IS NULL OR auditLog.requestHeader ->> :clientAppHeader = :clientApp)",
|
||||||
|
{ clientAppHeader: CLIENT_APP_HEADER, clientApp: "backoffice" },
|
||||||
|
)
|
||||||
|
.select([
|
||||||
|
"audit_log_commands.id",
|
||||||
|
"audit_log_commands.createdAt",
|
||||||
|
"audit_log_commands.deletedAt",
|
||||||
|
"audit_log_commands.entityName",
|
||||||
|
"audit_log_commands.queryMethod",
|
||||||
|
"audit_log_commands.changes",
|
||||||
|
"audit_log_commands.payload",
|
||||||
|
"auditLog.id",
|
||||||
|
"auditLog.user",
|
||||||
|
])
|
||||||
|
.addOrderBy("audit_log_commands.createdAt", "DESC")
|
||||||
|
.skip(skip)
|
||||||
|
.take(take)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
return { count, items };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import { EventEmitter2 } from "@nestjs/event-emitter";
|
import { EventEmitter2 } from "@nestjs/event-emitter";
|
||||||
import { DataSource, EntityManager, In } from "typeorm";
|
import { DataSource, EntityManager, In } from "typeorm";
|
||||||
|
|
||||||
|
import { Booking } from "../bookings/entities/booking.entity";
|
||||||
import { CompaniesService } from "../companies/companies.service";
|
import { CompaniesService } from "../companies/companies.service";
|
||||||
import { applyBookingRefDirectionScope } from "../user-trade-access/trade-scope.util";
|
import { applyBookingRefDirectionScope } from "../user-trade-access/trade-scope.util";
|
||||||
import { PaymentService } from "../payment/payment.service";
|
import { PaymentService } from "../payment/payment.service";
|
||||||
@@ -52,6 +53,8 @@ const DEFAULT_DUE_DAYS = 14;
|
|||||||
const OPEN_STATUSES: Freight.InvoiceStatus[] = [
|
const OPEN_STATUSES: Freight.InvoiceStatus[] = [
|
||||||
Freight.InvoiceStatus.Issued,
|
Freight.InvoiceStatus.Issued,
|
||||||
Freight.InvoiceStatus.Pending,
|
Freight.InvoiceStatus.Pending,
|
||||||
|
// Success-redirect ack; still unsettled, so it must stay payable/settleable.
|
||||||
|
Freight.InvoiceStatus.PaymentProcessing,
|
||||||
Freight.InvoiceStatus.PartiallyPaid,
|
Freight.InvoiceStatus.PartiallyPaid,
|
||||||
Freight.InvoiceStatus.Overdue,
|
Freight.InvoiceStatus.Overdue,
|
||||||
];
|
];
|
||||||
@@ -924,6 +927,26 @@ export class BillingService {
|
|||||||
});
|
});
|
||||||
if (!invoice) return null;
|
if (!invoice) return null;
|
||||||
|
|
||||||
|
// Reconcile-before-expire, caller-proof: an invoice with a payment intent may
|
||||||
|
// have settled at the gateway without the webhook landing yet. `paid` — leave
|
||||||
|
// it open, the (re-emitted) payment.succeeded settles it. `unverifiable` —
|
||||||
|
// never expire on unknown; the caller's next sweep retries. Invoices with no
|
||||||
|
// intent (`paymentId` null) were never payable at a gateway and expire directly.
|
||||||
|
if (invoice.paymentId) {
|
||||||
|
const { paid, unverifiable } = await this.reconcilePayable(
|
||||||
|
invoice.sourceId,
|
||||||
|
);
|
||||||
|
if (paid || unverifiable) {
|
||||||
|
this.logger.warn(
|
||||||
|
`expirePayable skipped for invoice ${invoice.invoiceNumber} (${invoice.id}) — ` +
|
||||||
|
(paid
|
||||||
|
? "gateway reconcile found a settled payment"
|
||||||
|
: "settlement unverifiable at the gateway"),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return this.transition(
|
return this.transition(
|
||||||
invoice.id,
|
invoice.id,
|
||||||
Freight.InvoiceStatus.Expired,
|
Freight.InvoiceStatus.Expired,
|
||||||
@@ -1028,6 +1051,40 @@ export class BillingService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success-redirect ack (see PaymentService.acknowledgeSuccessRedirect): move
|
||||||
|
* the invoice linked to a gateway intent to PAYMENT_PROCESSING. Only from
|
||||||
|
* ISSUED/PENDING — never overwrites a settlement (PAID/PARTIALLY_PAID) and
|
||||||
|
* is idempotent. Balance untouched: this is a display state, not a
|
||||||
|
* settlement; settleByPaymentId still performs the real transition.
|
||||||
|
*/
|
||||||
|
async markInvoicePaymentProcessing(paymentId: string): Promise<void> {
|
||||||
|
await this.dataSource.getRepository(Invoice).update(
|
||||||
|
{
|
||||||
|
paymentId,
|
||||||
|
status: In([
|
||||||
|
Freight.InvoiceStatus.Issued,
|
||||||
|
Freight.InvoiceStatus.Pending,
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
{ status: Freight.InvoiceStatus.PaymentProcessing },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Counterpart of {@link markInvoicePaymentProcessing} for a failed intent:
|
||||||
|
* PAYMENT_PROCESSING → PENDING so the invoice reads payable again for a
|
||||||
|
* retry. No-op from any other status.
|
||||||
|
*/
|
||||||
|
async revertInvoicePaymentProcessing(paymentId: string): Promise<void> {
|
||||||
|
await this.dataSource
|
||||||
|
.getRepository(Invoice)
|
||||||
|
.update(
|
||||||
|
{ paymentId, status: Freight.InvoiceStatus.PaymentProcessing },
|
||||||
|
{ status: Freight.InvoiceStatus.Pending },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Payment initiation & settlement (the gateway boundary) ───────────────────
|
// ── Payment initiation & settlement (the gateway boundary) ───────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1136,6 +1193,17 @@ export class BillingService {
|
|||||||
.getRepository(Invoice)
|
.getRepository(Invoice)
|
||||||
.update({ id: invoice.id }, { paymentId: result.intentId });
|
.update({ id: invoice.id }, { paymentId: result.intentId });
|
||||||
|
|
||||||
|
// CBE_BILL: the bill reference IS the booking's PNR — the number the customer pays against
|
||||||
|
// at any CBE channel. Persist it on the booking so it survives the initiate response and
|
||||||
|
// shows on the booking/contract everywhere. The payment service reissues the same reference
|
||||||
|
// while the bill stays open, so re-initiating overwrites with an identical value.
|
||||||
|
const billReference = result.response.clientAction?.billReference;
|
||||||
|
if (billReference && invoice.source === Freight.InvoiceSource.Booking) {
|
||||||
|
await this.dataSource
|
||||||
|
.getRepository(Booking)
|
||||||
|
.update({ id: invoice.sourceId }, { pnrCode: billReference });
|
||||||
|
}
|
||||||
|
|
||||||
// Settlement is driven by the payment API (webhook/outbox → payment.succeeded);
|
// Settlement is driven by the payment API (webhook/outbox → payment.succeeded);
|
||||||
// billing must not simulate it. Kept for local demos only.
|
// billing must not simulate it. Kept for local demos only.
|
||||||
// An OTP intent (CAC Bank) is NOT paid yet — the payer still has to enter the
|
// An OTP intent (CAC Bank) is NOT paid yet — the payer still has to enter the
|
||||||
|
|||||||
@@ -87,6 +87,33 @@ describe('BookingPricingService — domestic corridor', () => {
|
|||||||
expect(result.lineItems[0].amount).toBe(Math.round(35 * 120 * MOCK_CBE_RATE));
|
expect(result.lineItems[0].amount).toBe(Math.round(35 * 120 * MOCK_CBE_RATE));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the exchange rate decimals — ETB amounts round to cents, not whole birr', async () => {
|
||||||
|
exchangeService.getRate.mockResolvedValue(162.2132);
|
||||||
|
const booking = {
|
||||||
|
id: 'b-1-frac',
|
||||||
|
freightType: 'BULK',
|
||||||
|
tradeDirection: 'DOMESTIC',
|
||||||
|
paymentCurrency: 'ETB',
|
||||||
|
cargoTotalWeightVgm: 120,
|
||||||
|
originYardId: MOJO,
|
||||||
|
destinationYardId: DIRE,
|
||||||
|
bookingContainers: [],
|
||||||
|
} as unknown as Booking;
|
||||||
|
|
||||||
|
const result = await (
|
||||||
|
service as unknown as {
|
||||||
|
computeBaseRailLinesWithRates: (
|
||||||
|
b: Booking,
|
||||||
|
input: { containers: [] },
|
||||||
|
) => Promise<{ lineItems: Array<{ amount: number }> }>;
|
||||||
|
}
|
||||||
|
).computeBaseRailLinesWithRates(booking, { containers: [] });
|
||||||
|
|
||||||
|
// 35 × 120 × 162.2132 = 681,295.44 — the .44 must survive (whole-birr
|
||||||
|
// rounding here billed with the integer part of the rate, in effect).
|
||||||
|
expect(result.lineItems[0].amount).toBe(681295.44);
|
||||||
|
});
|
||||||
|
|
||||||
it('prices domestic bulk in USD using INTERCITY_BULK USD rate directly', async () => {
|
it('prices domestic bulk in USD using INTERCITY_BULK USD rate directly', async () => {
|
||||||
const booking = {
|
const booking = {
|
||||||
id: 'b-1-usd',
|
id: 'b-1-usd',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { RatesService } from '../rule-engine/services/rates.service';
|
|||||||
import { Rate } from '../rule-engine/entities/rate.entity';
|
import { Rate } from '../rule-engine/entities/rate.entity';
|
||||||
import { isBulkQuantityUnit } from '../rule-engine/entities/rate-unit.util';
|
import { isBulkQuantityUnit } from '../rule-engine/entities/rate-unit.util';
|
||||||
import { ContractRateSnapshot } from '../contracts/entities/contract-rate-snapshot.entity';
|
import { ContractRateSnapshot } from '../contracts/entities/contract-rate-snapshot.entity';
|
||||||
|
import { round2 } from '../billing/invoice-settlement.util';
|
||||||
import { ExchangeService } from '@edr/api-common';
|
import { ExchangeService } from '@edr/api-common';
|
||||||
import {
|
import {
|
||||||
AppliedCargoModifier,
|
AppliedCargoModifier,
|
||||||
@@ -205,14 +206,14 @@ export class BookingPricingService {
|
|||||||
const unitAmount = frozen
|
const unitAmount = frozen
|
||||||
? Number(frozen.unitPrice)
|
? Number(frozen.unitPrice)
|
||||||
: isEtbBooking
|
: isEtbBooking
|
||||||
? Math.round(unitUsd * usdToEtb)
|
? round2(unitUsd * usdToEtb)
|
||||||
: unitUsd;
|
: unitUsd;
|
||||||
const convertedAmount = frozen
|
const convertedAmount = frozen
|
||||||
? isEtbBooking
|
? isEtbBooking
|
||||||
? Math.round(unitAmount * quantity)
|
? round2(unitAmount * quantity)
|
||||||
: unitAmount * quantity
|
: unitAmount * quantity
|
||||||
: isEtbBooking
|
: isEtbBooking
|
||||||
? Math.round(usdAmount * usdToEtb)
|
? round2(usdAmount * usdToEtb)
|
||||||
: usdAmount;
|
: usdAmount;
|
||||||
|
|
||||||
const item: PriceLineItemDto = {
|
const item: PriceLineItemDto = {
|
||||||
@@ -583,8 +584,8 @@ export class BookingPricingService {
|
|||||||
} else {
|
} else {
|
||||||
const unitUsd = Number(rate!.rateValue);
|
const unitUsd = Number(rate!.rateValue);
|
||||||
const usdAmount = this.amountForRate(rate!, container.quantity, lineWagons);
|
const usdAmount = this.amountForRate(rate!, container.quantity, lineWagons);
|
||||||
amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount;
|
amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount;
|
||||||
unitAmount = isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd;
|
unitAmount = isEtbBooking ? round2(unitUsd * usdToEtb) : unitUsd;
|
||||||
}
|
}
|
||||||
if (rate) usedRatesMap.set(rate.id, rate);
|
if (rate) usedRatesMap.set(rate.id, rate);
|
||||||
lines.push({
|
lines.push({
|
||||||
@@ -652,8 +653,8 @@ export class BookingPricingService {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const usdAmount = this.amountForRate(fallback, quantity, wagonCount);
|
const usdAmount = this.amountForRate(fallback, quantity, wagonCount);
|
||||||
amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount;
|
amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount;
|
||||||
unitAmount = isEtbBooking ? Math.round(unitUsd * usdToEtb) : unitUsd;
|
unitAmount = isEtbBooking ? round2(unitUsd * usdToEtb) : unitUsd;
|
||||||
}
|
}
|
||||||
lines.push({
|
lines.push({
|
||||||
code: rateType,
|
code: rateType,
|
||||||
@@ -763,12 +764,12 @@ export class BookingPricingService {
|
|||||||
if (frozen) {
|
if (frozen) {
|
||||||
unitAmount = Number(frozen.unitPrice);
|
unitAmount = Number(frozen.unitPrice);
|
||||||
amount = isEtbBooking
|
amount = isEtbBooking
|
||||||
? Math.round(unitAmount * quantity)
|
? round2(unitAmount * quantity)
|
||||||
: unitAmount * quantity;
|
: unitAmount * quantity;
|
||||||
} else {
|
} else {
|
||||||
const usdAmount = value * quantity;
|
const usdAmount = value * quantity;
|
||||||
amount = isEtbBooking ? Math.round(usdAmount * usdToEtb) : usdAmount;
|
amount = isEtbBooking ? round2(usdAmount * usdToEtb) : usdAmount;
|
||||||
unitAmount = isEtbBooking ? Math.round(value * usdToEtb) : value;
|
unitAmount = isEtbBooking ? round2(value * usdToEtb) : value;
|
||||||
}
|
}
|
||||||
// Skip legs that resolve to nothing (zero rate, or zero km / count / tons).
|
// Skip legs that resolve to nothing (zero rate, or zero km / count / tons).
|
||||||
if (!(amount > 0)) continue;
|
if (!(amount > 0)) continue;
|
||||||
@@ -971,7 +972,7 @@ export class BookingPricingService {
|
|||||||
if (!(usdToEtb > 0)) return null;
|
if (!(usdToEtb > 0)) return null;
|
||||||
const converted =
|
const converted =
|
||||||
snap.currency === 'USD' && bookingCurrency === 'ETB'
|
snap.currency === 'USD' && bookingCurrency === 'ETB'
|
||||||
? Math.round(unitPrice * usdToEtb)
|
? round2(unitPrice * usdToEtb)
|
||||||
: snap.currency === 'ETB' && bookingCurrency === 'USD'
|
: snap.currency === 'ETB' && bookingCurrency === 'USD'
|
||||||
? unitPrice / usdToEtb
|
? unitPrice / usdToEtb
|
||||||
: null;
|
: null;
|
||||||
@@ -1027,7 +1028,7 @@ export class BookingPricingService {
|
|||||||
const currency = booking.paymentCurrency;
|
const currency = booking.paymentCurrency;
|
||||||
const isEtb = currency === 'ETB';
|
const isEtb = currency === 'ETB';
|
||||||
const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1;
|
const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1;
|
||||||
const convert = (usd: number): number => (isEtb ? Math.round(usd * usdToEtb) : usd);
|
const convert = (usd: number): number => (isEtb ? round2(usd * usdToEtb) : usd);
|
||||||
|
|
||||||
const onLeg = liveRates.filter(
|
const onLeg = liveRates.filter(
|
||||||
(r) =>
|
(r) =>
|
||||||
|
|||||||
@@ -280,15 +280,104 @@ describe("Fayda identity verification binds a person to the company", () => {
|
|||||||
expect(ctx.attributes.poaFaydaSub).toBe("new-sub");
|
expect(ctx.attributes.poaFaydaSub).toBe("new-sub");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refuses to rename a verified person by hand", async () => {
|
it("stages nothing for a verified field an approved company resubmits", async () => {
|
||||||
const { service } = makeService({
|
// Approving it could not move the live row — the verified value is written
|
||||||
|
// back over it — so it must never reach a reviewer as a pending change.
|
||||||
|
const { service, deps } = makeService({
|
||||||
|
status: CompanyStatus.Active,
|
||||||
|
attributes: { ...OWNER_VERIFIED, ownerEmail: "abebe@example.com" },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateProfile("user-1", {
|
||||||
|
companyEmail: "someone-else@example.com",
|
||||||
|
} as never),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
expect(deps.changeRequestRepo.create).not.toHaveBeenCalled();
|
||||||
|
expect(deps.changeRequestRepo.update).not.toHaveBeenCalled();
|
||||||
|
expect(deps.companiesRepo.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
// The verified value wins, and it wins by overwriting rather than by
|
||||||
|
// rejecting: nobody types these fields, so a submission that disagrees is a
|
||||||
|
// stale form echoing itself back, not an edit. Failing it would block a save
|
||||||
|
// the customer never made — and leave them no way through, since re-verifying
|
||||||
|
// returns the same value they are being 400'd for.
|
||||||
|
it("overwrites a hand-renamed verified person with the verified name", async () => {
|
||||||
|
const { service, ctx } = makeService({
|
||||||
attributes: { ...OWNER_VERIFIED, ...POA_VERIFIED },
|
attributes: { ...OWNER_VERIFIED, ...POA_VERIFIED },
|
||||||
files: [paper()],
|
files: [paper()],
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
service.updateProfile("user-1", { poaName: "Someone Else" } as never),
|
service.updateProfile("user-1", { poaName: "Someone Else" } as never),
|
||||||
).rejects.toBeInstanceOf(BadRequestException);
|
).resolves.toBeDefined();
|
||||||
|
expect(ctx.attributes.poaName).toBe(POA_VERIFIED.poaName);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fayda's email and phone claims are optional — a verification can prove the
|
||||||
|
// person and return neither. Holding the company mirrors to "the owner is
|
||||||
|
// verified" rather than to "the verification supplied this value" would
|
||||||
|
// clobber the fallbacks the portal is built to send (account email, eTrade's
|
||||||
|
// registered phone) with nothing at all. OWNER_VERIFIED is exactly that
|
||||||
|
// shape: a sub, no contact details.
|
||||||
|
it("keeps company contact details a Fayda verification never supplied", async () => {
|
||||||
|
const { service, deps } = makeService({
|
||||||
|
attributes: { ...OWNER_VERIFIED },
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateProfile("user-1", {
|
||||||
|
companyEmail: "account@example.com",
|
||||||
|
companyPhone: "+251911777777",
|
||||||
|
} as never),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
const [, patch] = deps.companiesRepo.update.mock.calls.at(-1)!;
|
||||||
|
expect(patch.email).toBe("account@example.com");
|
||||||
|
expect(patch.phone).toBe("+251911777777");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("overwrites company contact details the verification did supply", async () => {
|
||||||
|
const { service, deps } = makeService({
|
||||||
|
attributes: {
|
||||||
|
...OWNER_VERIFIED,
|
||||||
|
ownerEmail: "abebe@example.com",
|
||||||
|
ownerPhone: "+251911000000",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateProfile("user-1", {
|
||||||
|
companyEmail: "someone-else@example.com",
|
||||||
|
companyPhone: "+251911999999",
|
||||||
|
} as never),
|
||||||
|
).resolves.toBeDefined();
|
||||||
|
const [, patch] = deps.companiesRepo.update.mock.calls.at(-1)!;
|
||||||
|
expect(patch.email).toBe("abebe@example.com");
|
||||||
|
expect(patch.phone).toBe("+251911000000");
|
||||||
|
});
|
||||||
|
|
||||||
|
// "Same as owner" copies `ownerEmail ?? null` onto the GM while setting
|
||||||
|
// `gmFaydaSub`. Locking that null made generalManagerEmail required by
|
||||||
|
// onboarding, hidden by the portal's link card and unwritable at once.
|
||||||
|
it("lets the GM's details be typed when the copied owner identity carried none", async () => {
|
||||||
|
const { service } = makeService({
|
||||||
|
attributes: {
|
||||||
|
...OWNER_VERIFIED,
|
||||||
|
gmSameAsOwner: true,
|
||||||
|
gmFaydaSub: "owner-sub",
|
||||||
|
generalManagerName: "Abebe Bikila",
|
||||||
|
generalManagerEmail: null,
|
||||||
|
generalManagerPhone: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.updateProfile("user-1", {
|
||||||
|
generalManagerEmail: "gm@example.com",
|
||||||
|
generalManagerPhone: "+251911888888",
|
||||||
|
} as never),
|
||||||
|
).resolves.toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("never locks or gates the general manager — it is not the verified subject", async () => {
|
it("never locks or gates the general manager — it is not the verified subject", async () => {
|
||||||
|
|||||||
@@ -739,6 +739,39 @@ export class CompaniesService {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `UpdateProfileDto` keys this company's completed verifications own — the
|
||||||
|
* ones `mapProfileDtoToCompanyUpdates` overwrites with the verified value
|
||||||
|
* whatever a request submits for them.
|
||||||
|
*
|
||||||
|
* A key only lands here once there is a verified value to hold it to: Fayda's
|
||||||
|
* email and phone claims are optional, and a verification that returned
|
||||||
|
* neither owns nothing to overwrite with.
|
||||||
|
*
|
||||||
|
* The map is the enforcement; this is the list used to keep those keys out of
|
||||||
|
* a change request in the first place. If the two ever drift the map still
|
||||||
|
* wins — the cost is a staged field that approving turns out not to move.
|
||||||
|
*/
|
||||||
|
private faydaOwnedKeys(company: Company): string[] {
|
||||||
|
const attrs = company.attributes ?? {};
|
||||||
|
const held = (key: string) => {
|
||||||
|
const v = attrs[key];
|
||||||
|
return v !== null && v !== undefined && v !== "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const keys: string[] = [];
|
||||||
|
if (attrs.ownerFaydaSub) {
|
||||||
|
// The Company-column mirrors of the owner's verified contact details.
|
||||||
|
if (held("ownerEmail")) keys.push("companyEmail");
|
||||||
|
if (held("ownerPhone")) keys.push("companyPhone");
|
||||||
|
}
|
||||||
|
for (const subject of IDENTITY_SUBJECTS) {
|
||||||
|
if (!attrs[`${IDENTITY_PREFIX[subject]}FaydaSub`]) continue;
|
||||||
|
keys.push(...IDENTITY_OWNED_FIELDS[subject].filter(held));
|
||||||
|
}
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Translate an UpdateProfileDto (or a staged change-request snapshot) into a
|
* Translate an UpdateProfileDto (or a staged change-request snapshot) into a
|
||||||
* `Company` patch: scalar columns plus a merged `attributes` blob (contact/GM/
|
* `Company` patch: scalar columns plus a merged `attributes` blob (contact/GM/
|
||||||
@@ -829,49 +862,46 @@ export class CompaniesService {
|
|||||||
// lets the customer type them once verified) — lock them the same way
|
// lets the customer type them once verified) — lock them the same way
|
||||||
// ownerEmail/ownerPhone themselves are locked below, once there is a
|
// ownerEmail/ownerPhone themselves are locked below, once there is a
|
||||||
// verified owner to lock them to.
|
// verified owner to lock them to.
|
||||||
|
//
|
||||||
|
// Keyed on the verified VALUE, not on `ownerFaydaSub`: Fayda's email and
|
||||||
|
// phone claims are optional, so a verification can prove the person while
|
||||||
|
// supplying neither (see completeIdentityVerification's conditional
|
||||||
|
// spreads). The portal falls back to the account email / eTrade's
|
||||||
|
// registered phone in exactly that case and submits it on every save of
|
||||||
|
// the company step — locking against an absent value would 400 that
|
||||||
|
// forever, and re-verifying could never clear it because Fayda still has
|
||||||
|
// nothing to return.
|
||||||
if (attrUpdates.ownerFaydaSub) {
|
if (attrUpdates.ownerFaydaSub) {
|
||||||
if (
|
if (attrUpdates.ownerEmail && dto.companyEmail !== undefined)
|
||||||
dto.companyEmail !== undefined &&
|
companyUpdates.email = attrUpdates.ownerEmail;
|
||||||
dto.companyEmail !== attrUpdates.ownerEmail
|
if (attrUpdates.ownerPhone && dto.companyPhone !== undefined)
|
||||||
) {
|
companyUpdates.phone = normalizeE164(String(attrUpdates.ownerPhone));
|
||||||
throw new BadRequestException(
|
|
||||||
"companyEmail is set by the owner's Fayda verification and cannot be edited. Re-verify to change it.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
dto.companyPhone !== undefined &&
|
|
||||||
normalizeE164(dto.companyPhone) !==
|
|
||||||
normalizeE164(String(attrUpdates.ownerPhone ?? ""))
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
"companyPhone is set by the owner's Fayda verification and cannot be edited. Re-verify to change it.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Renaming a Fayda-verified person by hand would launder the guarantee
|
// Renaming a Fayda-verified person by hand would launder the guarantee
|
||||||
// away, so the fields the verification owns are refused once it exists.
|
// away, so the verification keeps these fields: a submission that disagrees
|
||||||
|
// is overwritten with the verified value rather than rejected — the same
|
||||||
|
// doctrine `applyEtradeSourcedFields` uses for eTrade's fields, and for the
|
||||||
|
// same reason. The customer never types these (the portal derives them, and
|
||||||
|
// a stale form or a re-render can echo back something else entirely), so a
|
||||||
|
// 400 punishes a save they never made while an overwrite lands the truth.
|
||||||
for (const subject of IDENTITY_SUBJECTS) {
|
for (const subject of IDENTITY_SUBJECTS) {
|
||||||
if (!attrUpdates[`${IDENTITY_PREFIX[subject]}FaydaSub`]) continue;
|
if (!attrUpdates[`${IDENTITY_PREFIX[subject]}FaydaSub`]) continue;
|
||||||
for (const field of IDENTITY_OWNED_FIELDS[subject]) {
|
for (const field of IDENTITY_OWNED_FIELDS[subject]) {
|
||||||
const incoming = (dto as Record<string, unknown>)[field];
|
if ((dto as Record<string, unknown>)[field] === undefined) continue;
|
||||||
if (incoming === undefined) continue;
|
// The verification itself is what writes them; it must not be undone by
|
||||||
// The verification itself is allowed to write them; anything else is
|
// the value this same call just copied into the patch.
|
||||||
// compared against what is already stored, not against the value this
|
|
||||||
// same call just copied into the patch. Phones are compared normalized:
|
|
||||||
// a form that re-renders +251911000000 as 0911000000 is echoing the
|
|
||||||
// stored value back, not trying to change it.
|
|
||||||
if (dto.faydaIdentity && field in dto.faydaIdentity) continue;
|
if (dto.faydaIdentity && field in dto.faydaIdentity) continue;
|
||||||
const stored = company.attributes?.[field];
|
const stored = company.attributes?.[field];
|
||||||
const same = field.endsWith("Phone")
|
// A verification that supplied nothing for this field left no guarantee
|
||||||
? normalizeE164(String(incoming)) ===
|
// to protect, so it stays typeable. Matters most for the GM —
|
||||||
normalizeE164(String(stored ?? ""))
|
// `setGmSameAsOwner` copies `ownerEmail ?? null` onto
|
||||||
: incoming === stored;
|
// `generalManagerEmail` while setting `gmFaydaSub`, and
|
||||||
if (!same) {
|
// REQUIRED_COMPANY_INFO still demands that email, so holding a null
|
||||||
throw new BadRequestException(
|
// here makes it required, hidden by the portal's "same as owner" card,
|
||||||
`${field} is set by the Fayda verification of this company's ${IDENTITY_LABEL[subject]} and cannot be edited. Re-verify to change it.`,
|
// and unwritable all at once.
|
||||||
);
|
if (stored === null || stored === undefined || stored === "") continue;
|
||||||
}
|
attrUpdates[field] = stored;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,6 +1007,11 @@ export class CompaniesService {
|
|||||||
// for review with the live row left intact.
|
// for review with the live row left intact.
|
||||||
await this.assertTinAvailable(company, dto.tin);
|
await this.assertTinAvailable(company, dto.tin);
|
||||||
const fields = this.pickDefined(dto);
|
const fields = this.pickDefined(dto);
|
||||||
|
// Drop what the verifications own before anything is staged. Approving one
|
||||||
|
// of these could not change the live row — mapProfileDtoToCompanyUpdates
|
||||||
|
// writes the verified value back over it — so showing it to a reviewer
|
||||||
|
// asks them to rule on a change that does not exist.
|
||||||
|
for (const key of this.faydaOwnedKeys(company)) delete fields[key];
|
||||||
const selfService: Record<string, any> = {};
|
const selfService: Record<string, any> = {};
|
||||||
const staged: Record<string, any> = {};
|
const staged: Record<string, any> = {};
|
||||||
for (const [key, value] of Object.entries(fields)) {
|
for (const [key, value] of Object.entries(fields)) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Injectable, UnprocessableEntityException } from '@nestjs/common';
|
|||||||
|
|
||||||
import { RatesService } from '../rule-engine/services/rates.service';
|
import { RatesService } from '../rule-engine/services/rates.service';
|
||||||
import { ContainerTypesService } from '../rule-engine/services/container-types.service';
|
import { ContainerTypesService } from '../rule-engine/services/container-types.service';
|
||||||
|
import { round2 } from '../billing/invoice-settlement.util';
|
||||||
import { ExchangeService } from '@edr/api-common';
|
import { ExchangeService } from '@edr/api-common';
|
||||||
import { ContractsRepository } from './contracts.repository';
|
import { ContractsRepository } from './contracts.repository';
|
||||||
import { Contract } from './entities/contract.entity';
|
import { Contract } from './entities/contract.entity';
|
||||||
@@ -79,7 +80,7 @@ export class ContractPricingService {
|
|||||||
const currency = contract.paymentCurrency;
|
const currency = contract.paymentCurrency;
|
||||||
const isEtb = currency === 'ETB';
|
const isEtb = currency === 'ETB';
|
||||||
const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1;
|
const usdToEtb = isEtb ? await this.exchangeService.getRate('USD', 'ETB') : 1;
|
||||||
const convert = (usd: number): number => (isEtb ? Math.round(usd * usdToEtb) : usd);
|
const convert = (usd: number): number => (isEtb ? round2(usd * usdToEtb) : usd);
|
||||||
|
|
||||||
const lineItems: ContractUnitRateLineItem[] = [];
|
const lineItems: ContractUnitRateLineItem[] = [];
|
||||||
const baseType = this.baseRateType(contract);
|
const baseType = this.baseRateType(contract);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
HttpStatus,
|
HttpStatus,
|
||||||
Param,
|
Param,
|
||||||
ParseUUIDPipe,
|
ParseUUIDPipe,
|
||||||
|
Post,
|
||||||
Query,
|
Query,
|
||||||
Res,
|
Res,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
@@ -84,6 +85,15 @@ export class PaymentController {
|
|||||||
return this.paymentService.getIntentByBookingId(bookingId);
|
return this.paymentService.getIntentByBookingId(bookingId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Post("redirect-success/:bookingId")
|
||||||
|
@ApiOperation({
|
||||||
|
summary:
|
||||||
|
"Success-redirect ack: mark payment processing + invoice PAYMENT_PROCESSING (webhook remains source of truth)",
|
||||||
|
})
|
||||||
|
acknowledgeSuccessRedirect(@Param("bookingId") bookingId: string) {
|
||||||
|
return this.paymentService.acknowledgeSuccessRedirect(bookingId);
|
||||||
|
}
|
||||||
|
|
||||||
@Get("receipt/:orderId")
|
@Get("receipt/:orderId")
|
||||||
@Public()
|
@Public()
|
||||||
@ApiOperation({ summary: "Generate a payment receipt HTML page" })
|
@ApiOperation({ summary: "Generate a payment receipt HTML page" })
|
||||||
|
|||||||
@@ -253,8 +253,10 @@ export class PaymentService {
|
|||||||
payerAccount: input.payerAccount,
|
payerAccount: input.payerAccount,
|
||||||
payerName: input.payerName,
|
payerName: input.payerName,
|
||||||
expiresAt: input.expiresAt,
|
expiresAt: input.expiresAt,
|
||||||
|
// bookingId lets the success page ack the redirect (→ PAYMENT_PROCESSING).
|
||||||
returnUrl:
|
returnUrl:
|
||||||
input.returnUrl ?? "https://edrfreight.triaplc.com/payment/success",
|
input.returnUrl ??
|
||||||
|
`https://edrfreight.triaplc.com/payment/success?bookingId=${encodeURIComponent(input.referenceId)}`,
|
||||||
failureUrl:
|
failureUrl:
|
||||||
input.failureUrl ?? "https://edrfreight.triaplc.com/payment/failure",
|
input.failureUrl ?? "https://edrfreight.triaplc.com/payment/failure",
|
||||||
});
|
});
|
||||||
@@ -492,6 +494,37 @@ export class PaymentService {
|
|||||||
return { alreadyFinalized: false };
|
return { alreadyFinalized: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success-redirect ack from the portal: the customer finished provider
|
||||||
|
* checkout, settlement webhook not (necessarily) in yet. Optimistic
|
||||||
|
* intermediate only — the webhook stays the source of truth. Never
|
||||||
|
* downgrades: only action-required → processing, and the invoice moves to
|
||||||
|
* PAYMENT_PROCESSING only from an open unpaid status. CBE_BILL is excluded
|
||||||
|
* (bank-counter flow, it has no redirect).
|
||||||
|
*/
|
||||||
|
async acknowledgeSuccessRedirect(
|
||||||
|
referenceId: string,
|
||||||
|
): Promise<{ acknowledged: boolean }> {
|
||||||
|
const intent = await this.paymentRepo.findOneBy({ refId: referenceId });
|
||||||
|
if (!intent || intent.method === "cbe-bill") {
|
||||||
|
return { acknowledged: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.status === "action-required") {
|
||||||
|
await this.paymentRepo.update(
|
||||||
|
{ id: intent.id, status: "action-required" },
|
||||||
|
{ status: "processing" },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Even if the intent already advanced (e.g. webhook raced the redirect to
|
||||||
|
// "processing"), the invoice ack is idempotent and status-guarded.
|
||||||
|
if (intent.status === "action-required" || intent.status === "processing") {
|
||||||
|
await this.billing.markInvoicePaymentProcessing(intent.id);
|
||||||
|
return { acknowledged: true };
|
||||||
|
}
|
||||||
|
return { acknowledged: false };
|
||||||
|
}
|
||||||
|
|
||||||
async markPaymentFailed(input: {
|
async markPaymentFailed(input: {
|
||||||
intentId: string;
|
intentId: string;
|
||||||
failureCode?: string;
|
failureCode?: string;
|
||||||
@@ -510,7 +543,9 @@ export class PaymentService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Invoice stays open for retry — nothing to settle. Logged only.
|
// Invoice stays open for retry — nothing to settle. A redirect-acked
|
||||||
|
// PAYMENT_PROCESSING invoice is put back to PENDING so it reads payable.
|
||||||
|
await this.billing.revertInvoicePaymentProcessing(intent.id);
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Payment ${intent.id} failed for ${intent.refId}` +
|
`Payment ${intent.id} failed for ${intent.refId}` +
|
||||||
(input.failureMessage ? `: ${input.failureMessage}` : ""),
|
(input.failureMessage ? `: ${input.failureMessage}` : ""),
|
||||||
|
|||||||
@@ -366,6 +366,7 @@ export const CONFIG_SETTINGS_PERMISSIONS: FreightPermissionSeed[] = [
|
|||||||
perm('b4a00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:file_upload:manage', 'Manage file-upload settings'),
|
perm('b4a00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:file_upload:manage', 'Manage file-upload settings'),
|
||||||
perm('b4b00001-0001-4000-8000-000000000001', 'edr_freight_app:settings:dropdown:view', 'View dropdown settings'),
|
perm('b4b00001-0001-4000-8000-000000000001', 'edr_freight_app:settings:dropdown:view', 'View dropdown settings'),
|
||||||
perm('b4b00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:dropdown:manage', 'Manage dropdown settings'),
|
perm('b4b00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:dropdown:manage', 'Manage dropdown settings'),
|
||||||
|
perm('b4c00001-0001-4000-8000-000000000001', 'edr_freight_app:audit:view', 'View audit logs'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// M. Staff / IAM admin — NEW keys only. The employee_registration / role_assignment
|
// M. Staff / IAM admin — NEW keys only. The employee_registration / role_assignment
|
||||||
@@ -700,6 +701,9 @@ export const FREIGHT_PERMS = {
|
|||||||
manage: 'edr_freight_app:settings:dropdown:manage',
|
manage: 'edr_freight_app:settings:dropdown:manage',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
audit: {
|
||||||
|
view: 'edr_freight_app:audit:view',
|
||||||
|
},
|
||||||
staff: {
|
staff: {
|
||||||
roles: {
|
roles: {
|
||||||
view: 'edr_freight_app:staff:roles:view',
|
view: 'edr_freight_app:staff:roles:view',
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
# Dev server port. Default: 5283.
|
||||||
|
PORT=5283
|
||||||
|
|
||||||
VITE_API_URL=http://localhost:3001
|
VITE_API_URL=http://localhost:3001
|
||||||
VITE_BASE_API_URL=http://localhost:3001
|
VITE_BASE_API_URL=http://localhost:3001
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 5183 --clearScreen false",
|
"dev": "vite --clearScreen false",
|
||||||
"prebuild": "node -e \"const fs=require('fs'); fs.rmSync('dist',{recursive:true,force:true});\"",
|
"prebuild": "node -e \"const fs=require('fs'); fs.rmSync('dist',{recursive:true,force:true});\"",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview --port 5183",
|
"preview": "vite preview --port 5183",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
FileSignature,
|
FileSignature,
|
||||||
FileText,
|
FileText,
|
||||||
Hammer,
|
Hammer,
|
||||||
|
History,
|
||||||
LayoutDashboard,
|
LayoutDashboard,
|
||||||
LayoutGrid,
|
LayoutGrid,
|
||||||
MapPin,
|
MapPin,
|
||||||
@@ -76,6 +77,7 @@ import ReportsHubPage from "./pages/reports/ReportsHubPage";
|
|||||||
import ReportPage from "./pages/reports/ReportPage";
|
import ReportPage from "./pages/reports/ReportPage";
|
||||||
import AiBookingMockTestPage from "./pages/ai/AiBookingMockTestPage";
|
import AiBookingMockTestPage from "./pages/ai/AiBookingMockTestPage";
|
||||||
import PaymentsPage from "./pages/payments/PaymentsPage";
|
import PaymentsPage from "./pages/payments/PaymentsPage";
|
||||||
|
import AuditLogsPage from "./pages/audit/AuditLogsPage";
|
||||||
//import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage";
|
//import EmployeesPage from "./pages/dashboard/user-management/EmployeesPage";
|
||||||
import { RequirePermission } from "./components/auth/RequirePermission";
|
import { RequirePermission } from "./components/auth/RequirePermission";
|
||||||
import {
|
import {
|
||||||
@@ -115,6 +117,7 @@ import TrainScheduleV2DetailPage from "./pages/trainScheduling/TrainScheduleV2De
|
|||||||
import TrainScheduleTrackPage from "./pages/trainScheduling/TrainScheduleTrackPage";
|
import TrainScheduleTrackPage from "./pages/trainScheduling/TrainScheduleTrackPage";
|
||||||
import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
|
import TrainSchedulingGlobalRulesPage from "./pages/trainScheduling/TrainSchedulingGlobalRulesPage";
|
||||||
import TradeAccessPage from "./pages/configuration/TradeAccessPage";
|
import TradeAccessPage from "./pages/configuration/TradeAccessPage";
|
||||||
|
import ExchangeRateSettingsCard from "./pages/settings/ExchangeRateSettingsCard";
|
||||||
import ContractValidityPeriodsPage from "./pages/configuration/ContractValidityPeriodsPage";
|
import ContractValidityPeriodsPage from "./pages/configuration/ContractValidityPeriodsPage";
|
||||||
import FirstMilePage from "./pages/operations/FirstMilePage";
|
import FirstMilePage from "./pages/operations/FirstMilePage";
|
||||||
import LastMilePage from "./pages/operations/LastMilePage";
|
import LastMilePage from "./pages/operations/LastMilePage";
|
||||||
@@ -581,6 +584,12 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
icon: <ScrollText />,
|
icon: <ScrollText />,
|
||||||
permission: FREIGHT_PERMS.admin,
|
permission: FREIGHT_PERMS.admin,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Audit logs",
|
||||||
|
href: "/dashboard/audit-logs",
|
||||||
|
icon: <History />,
|
||||||
|
permission: FREIGHT_PERMS.audit.view,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Configuration",
|
label: "Configuration",
|
||||||
href: "/dashboard/configuration",
|
href: "/dashboard/configuration",
|
||||||
@@ -597,6 +606,11 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
|||||||
href: "/dashboard/configuration/trade-access",
|
href: "/dashboard/configuration/trade-access",
|
||||||
permission: FREIGHT_PERMS.admin,
|
permission: FREIGHT_PERMS.admin,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Exchange rate",
|
||||||
|
href: "/dashboard/configuration/exchange-rate",
|
||||||
|
permission: FREIGHT_PERMS.admin,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1589,6 +1603,14 @@ const App = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="audit-logs"
|
||||||
|
element={
|
||||||
|
<RequirePermission permission={FREIGHT_PERMS.audit.view}>
|
||||||
|
<AuditLogsPage />
|
||||||
|
</RequirePermission>
|
||||||
|
}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="contract-templates"
|
path="contract-templates"
|
||||||
element={
|
element={
|
||||||
@@ -1628,6 +1650,16 @@ const App = () => {
|
|||||||
</RequirePermission>
|
</RequirePermission>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="configuration/exchange-rate"
|
||||||
|
element={
|
||||||
|
<RequirePermission permission={FREIGHT_PERMS.admin}>
|
||||||
|
<div className="p-4">
|
||||||
|
<ExchangeRateSettingsCard />
|
||||||
|
</div>
|
||||||
|
</RequirePermission>
|
||||||
|
}
|
||||||
|
/>
|
||||||
{/* <Route
|
{/* <Route
|
||||||
path="configuration/contract-validity-periods"
|
path="configuration/contract-validity-periods"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ const INVOICE_STATUS_COLOR: Record<Freight.InvoiceStatus, string> = {
|
|||||||
DRAFT: "gray",
|
DRAFT: "gray",
|
||||||
ISSUED: "cyan",
|
ISSUED: "cyan",
|
||||||
PENDING: "yellow",
|
PENDING: "yellow",
|
||||||
|
PAYMENT_PROCESSING: "indigo",
|
||||||
PARTIALLY_PAID: "orange",
|
PARTIALLY_PAID: "orange",
|
||||||
PAID: "edr-green",
|
PAID: "edr-green",
|
||||||
OVERDUE: "red",
|
OVERDUE: "red",
|
||||||
|
|||||||
@@ -184,6 +184,13 @@ const ROUTE_META: Array<{ prefix: string; meta: PageMeta }> = [
|
|||||||
subtitle: "Manage dropdown options used across the platform",
|
subtitle: "Manage dropdown options used across the platform",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
prefix: "/dashboard/audit-logs",
|
||||||
|
meta: {
|
||||||
|
title: "Audit Logs",
|
||||||
|
subtitle: "Request and entity-level activity recorded across the freight API",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
prefix: "/dashboard/configuration/contract-validity-periods",
|
prefix: "/dashboard/configuration/contract-validity-periods",
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { openPdfBlob } from './pdf';
|
|||||||
const INVOICE_STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
|
const INVOICE_STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
|
||||||
DRAFT: 'gray',
|
DRAFT: 'gray',
|
||||||
ISSUED: 'orange',
|
ISSUED: 'orange',
|
||||||
|
PAYMENT_PROCESSING: 'indigo',
|
||||||
PARTIALLY_PAID: 'yellow',
|
PARTIALLY_PAID: 'yellow',
|
||||||
PAID: 'edr-green',
|
PAID: 'edr-green',
|
||||||
CANCELLED: 'gray',
|
CANCELLED: 'gray',
|
||||||
|
|||||||
@@ -309,6 +309,10 @@ export const URL_CONSTANTS = {
|
|||||||
SUMMARY: "/payments/summary",
|
SUMMARY: "/payments/summary",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
AUDIT: {
|
||||||
|
LOGS: "/audit/logs",
|
||||||
|
},
|
||||||
|
|
||||||
LOCOMOTIVES: {
|
LOCOMOTIVES: {
|
||||||
BASE: "/locomotives",
|
BASE: "/locomotives",
|
||||||
BY_ID: (id: string) => `/locomotives/${id}`,
|
BY_ID: (id: string) => `/locomotives/${id}`,
|
||||||
|
|||||||
@@ -276,6 +276,9 @@ export const FREIGHT_PERMS = {
|
|||||||
manage: "edr_freight_app:settings:dropdown:manage",
|
manage: "edr_freight_app:settings:dropdown:manage",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
audit: {
|
||||||
|
view: "edr_freight_app:audit:view",
|
||||||
|
},
|
||||||
staff: {
|
staff: {
|
||||||
roles: {
|
roles: {
|
||||||
view: "edr_freight_app:staff:roles:view",
|
view: "edr_freight_app:staff:roles:view",
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/shared/common/ui/alert-dialog";
|
} from "@/shared/common/ui/alert-dialog";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import ExchangeRateSettingsCard from "./settings/ExchangeRateSettingsCard";
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
const [createDialogOpen, setCreateDialogOpen] = useState(false);
|
||||||
@@ -78,8 +77,6 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
<ExchangeRateSettingsCard />
|
|
||||||
|
|
||||||
<Card className="shadow-lg border-gray-200 dark:border-gray-700">
|
<Card className="shadow-lg border-gray-200 dark:border-gray-700">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-xl font-semibold">
|
<CardTitle className="text-xl font-semibold">
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { Badge, Box, Card, Stack, Text } from "@mantine/core";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
|
||||||
|
import { PageContainer, PageHeader } from "@/components/page";
|
||||||
|
import { api } from "@/services/api";
|
||||||
|
import type {
|
||||||
|
AuditLogRow,
|
||||||
|
AuditQueryMethod,
|
||||||
|
AuditUser,
|
||||||
|
LocalizedText,
|
||||||
|
} from "@/services/audit.service";
|
||||||
|
import {
|
||||||
|
DataTable,
|
||||||
|
DataTableFooter,
|
||||||
|
usePagination,
|
||||||
|
type ColumnDef,
|
||||||
|
} from "@edr/ui-common";
|
||||||
|
|
||||||
|
const ACTION_LABELS: Record<AuditQueryMethod, string> = {
|
||||||
|
INSERT: "Created",
|
||||||
|
UPDATE: "Updated",
|
||||||
|
DELETE: "Deleted",
|
||||||
|
INSERT_CHILD: "Linked child",
|
||||||
|
DELETE_CHILD: "Unlinked child",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ACTION_COLORS: Record<AuditQueryMethod, string> = {
|
||||||
|
INSERT: "edr-green",
|
||||||
|
UPDATE: "yellow",
|
||||||
|
DELETE: "red",
|
||||||
|
INSERT_CHILD: "indigo",
|
||||||
|
DELETE_CHILD: "gray",
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatDateTime(iso: string): string {
|
||||||
|
const d = new Date(iso);
|
||||||
|
return Number.isNaN(d.getTime())
|
||||||
|
? "—"
|
||||||
|
: d.toLocaleString(undefined, {
|
||||||
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// See LocalizedText: `name`/`title` lifted from a raw audited entity can be
|
||||||
|
// a plain string or IAM's { am, en } — never render either directly.
|
||||||
|
// "undefined undefined" is the producer's own broken template when no user
|
||||||
|
// was attached at all (unauthenticated/customer flows, e.g. Fayda
|
||||||
|
// verification) — filtered out here rather than shown as raw garbage.
|
||||||
|
function localize(value: LocalizedText | null | undefined): string | undefined {
|
||||||
|
if (!value) return undefined;
|
||||||
|
if (typeof value === "object") return value.en ?? value.am ?? undefined;
|
||||||
|
if (/^undefined(\s+undefined)?$/.test(value.trim())) return undefined;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatUser(user: AuditUser | null | undefined): string {
|
||||||
|
return localize(user?.name) ?? user?.id ?? "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarize(row: AuditLogRow): string {
|
||||||
|
if (row.changes?.length) {
|
||||||
|
return row.changes
|
||||||
|
.slice(0, 2)
|
||||||
|
.map((c) => c.field)
|
||||||
|
.join(", ") + (row.changes.length > 2 ? `, +${row.changes.length - 2} more` : "");
|
||||||
|
}
|
||||||
|
if (row.payload) {
|
||||||
|
return (
|
||||||
|
localize(row.payload.name) ?? localize(row.payload.title) ?? row.payload.id ?? "—"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return "—";
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableHeader =
|
||||||
|
"text-xs font-semibold uppercase tracking-wide text-muted-foreground";
|
||||||
|
|
||||||
|
export default function AuditLogsPage() {
|
||||||
|
const { pagination, setPagination } = usePagination({ pageSize: 20 });
|
||||||
|
|
||||||
|
const filter = {
|
||||||
|
skip: pagination.pageIndex * pagination.pageSize,
|
||||||
|
take: pagination.pageSize,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery(
|
||||||
|
api.audit.list.queryOptions({ input: { filter } }),
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = data?.items ?? [];
|
||||||
|
const total = data?.count ?? 0;
|
||||||
|
const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize));
|
||||||
|
|
||||||
|
const columns: ColumnDef<AuditLogRow>[] = [
|
||||||
|
{
|
||||||
|
id: "time",
|
||||||
|
header: () => <span className={tableHeader}>Time</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{formatDateTime(row.original.createdAt)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "action",
|
||||||
|
header: () => <span className={tableHeader}>Action</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Badge
|
||||||
|
color={ACTION_COLORS[row.original.queryMethod] ?? "gray"}
|
||||||
|
variant="light"
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
{ACTION_LABELS[row.original.queryMethod] ?? row.original.queryMethod}
|
||||||
|
</Badge>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "entity",
|
||||||
|
header: () => <span className={tableHeader}>Entity</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="font-mono text-sm text-foreground">
|
||||||
|
{row.original.entityName}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "user",
|
||||||
|
header: () => <span className={tableHeader}>User</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="text-sm text-foreground">
|
||||||
|
{formatUser(row.original.auditLog?.user)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "summary",
|
||||||
|
header: () => <span className={tableHeader}>Summary</span>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<span className="truncate text-sm text-muted-foreground">
|
||||||
|
{summarize(row.original)}
|
||||||
|
</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContainer>
|
||||||
|
<PageHeader
|
||||||
|
title="Audit Logs"
|
||||||
|
subtitle="Request and entity-level activity recorded across the freight API."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card p={0}>
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Box px="md" pt="md" pb="sm" w="100%">
|
||||||
|
<Text size="sm" c="dimmed">
|
||||||
|
{total} record{total !== 1 ? "s" : ""}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box style={{ overflowX: "auto" }} w="100%">
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
data={rows}
|
||||||
|
status={isLoading ? "loading" : isError ? "error" : "success"}
|
||||||
|
pagination={{
|
||||||
|
pageIndex: pagination.pageIndex,
|
||||||
|
pageSize: pagination.pageSize,
|
||||||
|
pageCount,
|
||||||
|
totalCount: total,
|
||||||
|
}}
|
||||||
|
tableOptions={{
|
||||||
|
state: { pagination },
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
manualPagination: true,
|
||||||
|
pageCount,
|
||||||
|
}}
|
||||||
|
containerClassName="border-0 shadow-none bg-transparent"
|
||||||
|
footer={DataTableFooter}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
</PageContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -183,6 +183,7 @@ export default function InvoicesPage() {
|
|||||||
data={[
|
data={[
|
||||||
{ label: "All", value: "all" },
|
{ label: "All", value: "all" },
|
||||||
{ label: "Pending", value: "PENDING" },
|
{ label: "Pending", value: "PENDING" },
|
||||||
|
{ label: "Payment processing", value: "PAYMENT_PROCESSING" },
|
||||||
{ label: "Paid", value: "PAID" },
|
{ label: "Paid", value: "PAID" },
|
||||||
{ label: "Overdue", value: "OVERDUE" },
|
{ label: "Overdue", value: "OVERDUE" },
|
||||||
]}
|
]}
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ const INVOICE_STATUS_META: Record<string, { label: string; color: string }> = {
|
|||||||
PAID: { label: "Paid", color: "green" },
|
PAID: { label: "Paid", color: "green" },
|
||||||
PARTIALLY_PAID: { label: "Partially Paid", color: "teal" },
|
PARTIALLY_PAID: { label: "Partially Paid", color: "teal" },
|
||||||
PENDING: { label: "Pending", color: "yellow" },
|
PENDING: { label: "Pending", color: "yellow" },
|
||||||
|
PAYMENT_PROCESSING: { label: "Payment Processing", color: "indigo" },
|
||||||
UNPAID: { label: "Unpaid", color: "yellow" },
|
UNPAID: { label: "Unpaid", color: "yellow" },
|
||||||
OPEN: { label: "Open", color: "yellow" },
|
OPEN: { label: "Open", color: "yellow" },
|
||||||
ISSUED: { label: "Issued", color: "blue" },
|
ISSUED: { label: "Issued", color: "blue" },
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ const INVOICE_STATUS_META: Record<string, { label: string; color: string }> = {
|
|||||||
PAID: { label: "Paid", color: "green" },
|
PAID: { label: "Paid", color: "green" },
|
||||||
PARTIALLY_PAID: { label: "Partially Paid", color: "teal" },
|
PARTIALLY_PAID: { label: "Partially Paid", color: "teal" },
|
||||||
PENDING: { label: "Pending", color: "yellow" },
|
PENDING: { label: "Pending", color: "yellow" },
|
||||||
|
PAYMENT_PROCESSING: { label: "Payment Processing", color: "indigo" },
|
||||||
UNPAID: { label: "Unpaid", color: "yellow" },
|
UNPAID: { label: "Unpaid", color: "yellow" },
|
||||||
OPEN: { label: "Open", color: "yellow" },
|
OPEN: { label: "Open", color: "yellow" },
|
||||||
ISSUED: { label: "Issued", color: "blue" },
|
ISSUED: { label: "Issued", color: "blue" },
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const CONTRACT_STATUSES = [
|
|||||||
const INVOICE_STATUSES = [
|
const INVOICE_STATUSES = [
|
||||||
"ISSUED",
|
"ISSUED",
|
||||||
"PENDING",
|
"PENDING",
|
||||||
|
"PAYMENT_PROCESSING",
|
||||||
"PARTIALLY_PAID",
|
"PARTIALLY_PAID",
|
||||||
"PAID",
|
"PAID",
|
||||||
"OVERDUE",
|
"OVERDUE",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import { extractErrorMessage } from '@/components/warehouses/options';
|
|||||||
const STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
|
const STATUS_COLOR: Record<WarehouseInvoiceStatus, string> = {
|
||||||
DRAFT: 'gray',
|
DRAFT: 'gray',
|
||||||
ISSUED: 'orange',
|
ISSUED: 'orange',
|
||||||
|
PAYMENT_PROCESSING: 'indigo',
|
||||||
PARTIALLY_PAID: 'yellow',
|
PARTIALLY_PAID: 'yellow',
|
||||||
PAID: 'edr-green',
|
PAID: 'edr-green',
|
||||||
CANCELLED: 'gray',
|
CANCELLED: 'gray',
|
||||||
|
|||||||
@@ -163,6 +163,11 @@ import {
|
|||||||
type SaveLocomotivePayload,
|
type SaveLocomotivePayload,
|
||||||
} from "./locomotives.service";
|
} from "./locomotives.service";
|
||||||
import { overviewService } from "./overview.service";
|
import { overviewService } from "./overview.service";
|
||||||
|
import {
|
||||||
|
auditService,
|
||||||
|
type AuditLogListFilter,
|
||||||
|
type PaginatedAuditLogs,
|
||||||
|
} from "./audit.service";
|
||||||
import { reportsService } from "./reports.service";
|
import { reportsService } from "./reports.service";
|
||||||
import type { ReportQueryInput, ReportResult } from "@/types/reports";
|
import type { ReportQueryInput, ReportResult } from "@/types/reports";
|
||||||
import {
|
import {
|
||||||
@@ -2136,6 +2141,15 @@ export const api = {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
audit: {
|
||||||
|
list: endpoint<{ filter?: AuditLogListFilter }, PaginatedAuditLogs>(
|
||||||
|
"audit",
|
||||||
|
"list",
|
||||||
|
({ filter }) => auditService.list(filter),
|
||||||
|
({ filter }) => ["audit", "list", filter ?? {}],
|
||||||
|
),
|
||||||
|
},
|
||||||
|
|
||||||
signatures: {
|
signatures: {
|
||||||
mySignature: endpoint<void, SavedSignature | null>(
|
mySignature: endpoint<void, SavedSignature | null>(
|
||||||
"me",
|
"me",
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { api as client } from "../auth/http";
|
||||||
|
import { unwrap } from "@/utils/endpoint";
|
||||||
|
import { URL_CONSTANTS } from "@/constants/URLS";
|
||||||
|
|
||||||
|
const A = URL_CONSTANTS.AUDIT;
|
||||||
|
|
||||||
|
// Shape from @tria-plc/auditlog's AuditLogCommandController — see
|
||||||
|
// local-packages/FRONTEND_GUIDE.md.
|
||||||
|
export type AuditQueryMethod =
|
||||||
|
| "INSERT"
|
||||||
|
| "UPDATE"
|
||||||
|
| "DELETE"
|
||||||
|
| "INSERT_CHILD"
|
||||||
|
| "DELETE_CHILD";
|
||||||
|
|
||||||
|
export interface AuditFieldChange {
|
||||||
|
field: string;
|
||||||
|
from: unknown;
|
||||||
|
to: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
// IAM entities (users, orgs, positions, ...) name themselves bilingually —
|
||||||
|
// see edr-org.seeder.ts. Any `name`/`title` field lifted from a raw audited
|
||||||
|
// entity (auditLog.user, payload) can come back as either a plain string or
|
||||||
|
// this shape; both `name` fields below reflect that.
|
||||||
|
export type LocalizedText = string | { am?: string; en?: string };
|
||||||
|
|
||||||
|
// The vendored interceptor's own broken template produces a plain string
|
||||||
|
// ("undefined undefined") when no user was attached at all (unauthenticated/
|
||||||
|
// customer flows) — that's the non-bilingual string case for `name` here.
|
||||||
|
export interface AuditUser {
|
||||||
|
id?: string;
|
||||||
|
name?: LocalizedText;
|
||||||
|
organizationId?: string;
|
||||||
|
organizationName?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogRow {
|
||||||
|
id?: string;
|
||||||
|
createdAt: string;
|
||||||
|
deletedAt?: string | null;
|
||||||
|
entityName: string;
|
||||||
|
queryMethod: AuditQueryMethod;
|
||||||
|
changes?: AuditFieldChange[] | null;
|
||||||
|
payload?: { name?: LocalizedText; title?: LocalizedText; id?: string } | null;
|
||||||
|
auditLog?: { id?: string; user?: AuditUser | null };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AuditLogListFilter {
|
||||||
|
skip?: number;
|
||||||
|
take?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PaginatedAuditLogs {
|
||||||
|
items: AuditLogRow[];
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auditService = {
|
||||||
|
list: async (filter?: AuditLogListFilter): Promise<PaginatedAuditLogs> => {
|
||||||
|
const params: Record<string, number | undefined> = {
|
||||||
|
skip: filter?.skip,
|
||||||
|
take: filter?.take,
|
||||||
|
};
|
||||||
|
const response = await client.get<PaginatedAuditLogs>(A.LOGS, { params });
|
||||||
|
const data = unwrap(response.data) as PaginatedAuditLogs;
|
||||||
|
return { items: data.items ?? [], count: data.count ?? 0 };
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -907,6 +907,7 @@ export interface AllocationCriteria {
|
|||||||
export const WAREHOUSE_INVOICE_STATUSES = [
|
export const WAREHOUSE_INVOICE_STATUSES = [
|
||||||
'DRAFT',
|
'DRAFT',
|
||||||
'ISSUED',
|
'ISSUED',
|
||||||
|
'PAYMENT_PROCESSING',
|
||||||
'PARTIALLY_PAID',
|
'PARTIALLY_PAID',
|
||||||
'PAID',
|
'PAID',
|
||||||
'CANCELLED',
|
'CANCELLED',
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import path from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
|
|
||||||
|
import { loadEnv } from "vite";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
@@ -10,7 +11,9 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
const require = createRequire(import.meta.url);
|
const require = createRequire(import.meta.url);
|
||||||
const streamBrowserifyPath = require.resolve("stream-browserify");
|
const streamBrowserifyPath = require.resolve("stream-browserify");
|
||||||
|
|
||||||
export default defineConfig(() => {
|
export default defineConfig(({ mode }) => {
|
||||||
|
const env = loadEnv(mode, __dirname, "");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
resolve: {
|
resolve: {
|
||||||
@@ -31,7 +34,7 @@ export default defineConfig(() => {
|
|||||||
dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"],
|
dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 5183,
|
port: Number(env.PORT) || 5283,
|
||||||
host: "0.0.0.0",
|
host: "0.0.0.0",
|
||||||
},
|
},
|
||||||
test: {
|
test: {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --port 3000 --clearScreen false",
|
"dev": "vite --clearScreen false",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"preview": "vite preview --port 5173",
|
"preview": "vite preview --port 5173",
|
||||||
"lint": "eslint src",
|
"lint": "eslint src",
|
||||||
|
|||||||
@@ -194,6 +194,8 @@ export const URL_CONSTANTS = {
|
|||||||
PAYMENTS: {
|
PAYMENTS: {
|
||||||
INITIATE: "/api/payments/initiate",
|
INITIATE: "/api/payments/initiate",
|
||||||
INTENT: (bookingId: string) => `/api/payments/intents/${bookingId}`,
|
INTENT: (bookingId: string) => `/api/payments/intents/${bookingId}`,
|
||||||
|
REDIRECT_SUCCESS: (bookingId: string) =>
|
||||||
|
`/api/payments/redirect-success/${bookingId}`,
|
||||||
CHECKOUT: "/api/payments/checkout",
|
CHECKOUT: "/api/payments/checkout",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ export const INVOICE_BADGE: Record<
|
|||||||
[Freight.InvoiceStatus.Issued]: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
|
[Freight.InvoiceStatus.Issued]: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
|
||||||
[Freight.InvoiceStatus.Pending]: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
|
[Freight.InvoiceStatus.Pending]: { label: "Due soon", bg: "edr-amber-soft", text: "edr-amber-text" },
|
||||||
[Freight.InvoiceStatus.PartiallyPaid]: { label: "Partially paid", bg: "edr-amber-soft", text: "edr-amber-text" },
|
[Freight.InvoiceStatus.PartiallyPaid]: { label: "Partially paid", bg: "edr-amber-soft", text: "edr-amber-text" },
|
||||||
|
[Freight.InvoiceStatus.PaymentProcessing]: { label: "Payment processing", bg: "edr-blue-soft", text: "edr-blue" },
|
||||||
[Freight.InvoiceStatus.Paid]: { label: "Paid", bg: "edr-soft", text: "edr-green.7" },
|
[Freight.InvoiceStatus.Paid]: { label: "Paid", bg: "edr-soft", text: "edr-green.7" },
|
||||||
[Freight.InvoiceStatus.Overdue]: { label: "Overdue", bg: "edr-red-soft", text: "edr-red" },
|
[Freight.InvoiceStatus.Overdue]: { label: "Overdue", bg: "edr-red-soft", text: "edr-red" },
|
||||||
[Freight.InvoiceStatus.Cancelled]: { label: "Cancelled", bg: "edr-slate-soft", text: "edr-slate" },
|
[Freight.InvoiceStatus.Cancelled]: { label: "Cancelled", bg: "edr-slate-soft", text: "edr-slate" },
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const STATUS_STYLE: Record<
|
|||||||
[Freight.InvoiceStatus.Draft]: { label: "Draft", bg: "#EEF2F6", fg: "#64748B" },
|
[Freight.InvoiceStatus.Draft]: { label: "Draft", bg: "#EEF2F6", fg: "#64748B" },
|
||||||
[Freight.InvoiceStatus.Issued]: { label: "Due", bg: "#FEF3E2", fg: "#B45309" },
|
[Freight.InvoiceStatus.Issued]: { label: "Due", bg: "#FEF3E2", fg: "#B45309" },
|
||||||
[Freight.InvoiceStatus.Pending]: { label: "Due", bg: "#FEF3E2", fg: "#B45309" },
|
[Freight.InvoiceStatus.Pending]: { label: "Due", bg: "#FEF3E2", fg: "#B45309" },
|
||||||
|
[Freight.InvoiceStatus.PaymentProcessing]: { label: "Payment processing", bg: "#EAF1FB", fg: "#2563EB" },
|
||||||
[Freight.InvoiceStatus.PartiallyPaid]: { label: "Partially paid", bg: "#FEF9E7", fg: "#A16207" },
|
[Freight.InvoiceStatus.PartiallyPaid]: { label: "Partially paid", bg: "#FEF9E7", fg: "#A16207" },
|
||||||
[Freight.InvoiceStatus.Paid]: { label: "Paid", bg: "#E6F7EF", fg: "#0A6F4D" },
|
[Freight.InvoiceStatus.Paid]: { label: "Paid", bg: "#E6F7EF", fg: "#0A6F4D" },
|
||||||
[Freight.InvoiceStatus.Overdue]: { label: "Overdue", bg: "#FDECEC", fg: "#C0392B" },
|
[Freight.InvoiceStatus.Overdue]: { label: "Overdue", bg: "#FDECEC", fg: "#C0392B" },
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const INVOICE_STATUS_LABELS: Record<string, string> = {
|
|||||||
DRAFT: "Draft",
|
DRAFT: "Draft",
|
||||||
ISSUED: "Issued",
|
ISSUED: "Issued",
|
||||||
PENDING: "Due",
|
PENDING: "Due",
|
||||||
|
PAYMENT_PROCESSING: "Payment processing",
|
||||||
PARTIALLY_PAID: "Partially paid",
|
PARTIALLY_PAID: "Partially paid",
|
||||||
PAID: "Paid",
|
PAID: "Paid",
|
||||||
OVERDUE: "Overdue",
|
OVERDUE: "Overdue",
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { Group, Tabs } from "@mantine/core";
|
import { Group, Tabs } from "@mantine/core";
|
||||||
import { Clock, CreditCard, FileText, LayoutGrid, Truck } from "lucide-react";
|
import {
|
||||||
|
Clock,
|
||||||
|
CreditCard,
|
||||||
|
FileText,
|
||||||
|
LayoutGrid,
|
||||||
|
Package,
|
||||||
|
Truck,
|
||||||
|
} from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { useFileViewer } from "@/hooks/useFileViewer";
|
import { useFileViewer } from "@/hooks/useFileViewer";
|
||||||
@@ -9,6 +16,7 @@ import { ApproveDeliveryButton } from "../delivery/ApproveDeliveryButton";
|
|||||||
import { ActivityCard } from "./components/ActivityCard";
|
import { ActivityCard } from "./components/ActivityCard";
|
||||||
import { ClearanceCard } from "./components/ClearanceCard";
|
import { ClearanceCard } from "./components/ClearanceCard";
|
||||||
import { BookingClearanceWorkflowBanner } from "@/pages/bookings/BookingClearanceWorkflowBanner";
|
import { BookingClearanceWorkflowBanner } from "@/pages/bookings/BookingClearanceWorkflowBanner";
|
||||||
|
import { CargoTab } from "./components/CargoTab";
|
||||||
import { DocumentsTab } from "./components/DocumentsTab";
|
import { DocumentsTab } from "./components/DocumentsTab";
|
||||||
import { CompanyInfoCard } from "./components/CompanyInfoCard";
|
import { CompanyInfoCard } from "./components/CompanyInfoCard";
|
||||||
import { ContainersCard } from "./components/ContainersCard";
|
import { ContainersCard } from "./components/ContainersCard";
|
||||||
@@ -199,6 +207,9 @@ export function ReadonlyBookingView({
|
|||||||
<Tabs.Tab value="overview" leftSection={<LayoutGrid size={15} />}>
|
<Tabs.Tab value="overview" leftSection={<LayoutGrid size={15} />}>
|
||||||
Overview
|
Overview
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
|
<Tabs.Tab value="cargo" leftSection={<Package size={15} />}>
|
||||||
|
Cargo
|
||||||
|
</Tabs.Tab>
|
||||||
<Tabs.Tab value="logistics" leftSection={<Truck size={15} />}>
|
<Tabs.Tab value="logistics" leftSection={<Truck size={15} />}>
|
||||||
Logistics
|
Logistics
|
||||||
</Tabs.Tab>
|
</Tabs.Tab>
|
||||||
@@ -254,6 +265,10 @@ export function ReadonlyBookingView({
|
|||||||
</div>
|
</div>
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
|
|
||||||
|
<Tabs.Panel value="cargo">
|
||||||
|
<CargoTab booking={booking} />
|
||||||
|
</Tabs.Panel>
|
||||||
|
|
||||||
<Tabs.Panel value="logistics">
|
<Tabs.Panel value="logistics">
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<BodyGrid
|
<BodyGrid
|
||||||
|
|||||||
@@ -0,0 +1,465 @@
|
|||||||
|
import { Box, Group, SimpleGrid, Table, Text } from "@mantine/core";
|
||||||
|
import {
|
||||||
|
AlertTriangle,
|
||||||
|
Box as BoxIcon,
|
||||||
|
Container,
|
||||||
|
Flame,
|
||||||
|
Package,
|
||||||
|
Scale,
|
||||||
|
Snowflake,
|
||||||
|
Undo2,
|
||||||
|
} from "lucide-react";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
BookingContainerLineDetail,
|
||||||
|
BookingContainerUnitDetail,
|
||||||
|
BookingDetail,
|
||||||
|
} from "../booking-detail-types";
|
||||||
|
import {
|
||||||
|
commodityLabel,
|
||||||
|
fmtDate,
|
||||||
|
fmtWeight,
|
||||||
|
shippingLineLabel,
|
||||||
|
totalVgmTons,
|
||||||
|
} from "../utils";
|
||||||
|
import { CardTitle, SectionCard } from "./layout";
|
||||||
|
|
||||||
|
// ─── Shared bits ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function Flag({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
tone = "grey",
|
||||||
|
}: {
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
tone?: "grey" | "amber" | "blue" | "green";
|
||||||
|
}) {
|
||||||
|
const palette = {
|
||||||
|
grey: { bg: "#F1F4F7", color: "#475569" },
|
||||||
|
amber: { bg: "#FFFBEB", color: "#92400E" },
|
||||||
|
blue: { bg: "#EAF1FE", color: "#1E40AF" },
|
||||||
|
green: { bg: "#E8F5EF", color: "#0A6F4D" },
|
||||||
|
}[tone];
|
||||||
|
return (
|
||||||
|
<Group
|
||||||
|
component="span"
|
||||||
|
gap={4}
|
||||||
|
align="center"
|
||||||
|
wrap="nowrap"
|
||||||
|
style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: palette.bg,
|
||||||
|
padding: "3px 9px",
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: 700,
|
||||||
|
color: palette.color,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatTile({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
sub,
|
||||||
|
}: {
|
||||||
|
icon: ReactNode;
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
sub?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
p={14}
|
||||||
|
style={{
|
||||||
|
borderRadius: 12,
|
||||||
|
border: "1px solid #E6ECF2",
|
||||||
|
backgroundColor: "#FAFCFE",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap={6} align="center" mb={6} c="#6B7C8E">
|
||||||
|
{icon}
|
||||||
|
<Text fz="11px" fw={700} tt="uppercase" style={{ letterSpacing: "0.05em" }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
<Text fz={18} fw={800} c="#10202F" truncate>
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
{sub && (
|
||||||
|
<Text fz={12} c="#9AA8B5" mt={2}>
|
||||||
|
{sub}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DetailRow({ label, value }: { label: string; value: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<Group justify="space-between" align="baseline" py={11} wrap="nowrap" style={{ borderBottom: "1px solid #F2F5F8" }}>
|
||||||
|
<Text fz={12.5} fw={600} c="#9AA8B5" style={{ flexShrink: 0 }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text fz={13.5} fw={700} c="#10202F" ta="right">
|
||||||
|
{value}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const th = { color: "#9AA8B5", fontSize: 11 } as const;
|
||||||
|
|
||||||
|
// ─── Containers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function lineTypeLabel(line: BookingContainerLineDetail): string {
|
||||||
|
const t = line.containerType;
|
||||||
|
if (t?.label) return t.label;
|
||||||
|
if (t?.sizeFt) return `${t.sizeFt}ft${t.isReefer ? " Reefer" : ""} container`;
|
||||||
|
return t?.code ?? "Container";
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnitRow({ unit }: { unit: BookingContainerUnitDetail }) {
|
||||||
|
return (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fz={13} fw={700} c="#10202F" style={{ fontFamily: "monospace" }}>
|
||||||
|
{unit.containerNumber}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fz={13} c="#475569">
|
||||||
|
{unit.sealNumber || "—"}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fz={13} c="#475569">
|
||||||
|
{Number(unit.vgmTons || 0) ? fmtWeight(Number(unit.vgmTons)) : "—"}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Group gap={4} wrap="wrap">
|
||||||
|
{unit.isHazardous && (
|
||||||
|
<Flag tone="amber" icon={<AlertTriangle size={11} />} label="Hazardous" />
|
||||||
|
)}
|
||||||
|
{unit.isReefer && (
|
||||||
|
<Flag tone="blue" icon={<Snowflake size={11} />} label="Reefer" />
|
||||||
|
)}
|
||||||
|
{unit.isReturn && <Flag icon={<Undo2 size={11} />} label="Return" />}
|
||||||
|
{!unit.isHazardous && !unit.isReefer && !unit.isReturn && (
|
||||||
|
<Text fz={12} c="#9AA8B5">
|
||||||
|
—
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Text fz={13} c="#475569">
|
||||||
|
{unit.grnNumber || "—"}
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
{unit.receivedToPort ? (
|
||||||
|
<Box>
|
||||||
|
<Text fz={12.5} fw={700} c="#0A6F4D">
|
||||||
|
Received
|
||||||
|
</Text>
|
||||||
|
{unit.receivedAt && (
|
||||||
|
<Text fz={11.5} c="#9AA8B5">
|
||||||
|
{fmtDate(unit.receivedAt)}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
) : (
|
||||||
|
<Text fz={12.5} fw={600} c="#9AA8B5">
|
||||||
|
Pending
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ContainerLineCard({ line, index }: { line: BookingContainerLineDetail; index: number }) {
|
||||||
|
const units = line.units ?? [];
|
||||||
|
return (
|
||||||
|
<SectionCard>
|
||||||
|
<Group justify="space-between" align="flex-start" mb={4} wrap="wrap">
|
||||||
|
<Group gap={10} align="center">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: "#E8F5EF",
|
||||||
|
color: "#0A6F4D",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container size={18} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz={15} fw={800} c="#10202F">
|
||||||
|
{lineTypeLabel(line)}
|
||||||
|
</Text>
|
||||||
|
<Text fz={12} c="#9AA8B5">
|
||||||
|
Line {index + 1} · {line.quantity} unit{line.quantity !== 1 ? "s" : ""}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
<Group gap={4} wrap="wrap" justify="flex-end">
|
||||||
|
{line.isOverweight && (
|
||||||
|
<Flag
|
||||||
|
tone="amber"
|
||||||
|
icon={<AlertTriangle size={11} />}
|
||||||
|
label={
|
||||||
|
Number(line.overweightExcessTons || 0)
|
||||||
|
? `Overweight +${Number(line.overweightExcessTons)} t`
|
||||||
|
: "Overweight"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!!line.hazardousQuantity && (
|
||||||
|
<Flag tone="amber" icon={<Flame size={11} />} label={`${line.hazardousQuantity} hazardous`} />
|
||||||
|
)}
|
||||||
|
{!!line.reeferQuantity && (
|
||||||
|
<Flag tone="blue" icon={<Snowflake size={11} />} label={`${line.reeferQuantity} reefer`} />
|
||||||
|
)}
|
||||||
|
{!!line.returnQuantity && (
|
||||||
|
<Flag icon={<Undo2 size={11} />} label={`${line.returnQuantity} return`} />
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: 3 }} spacing={10} my="md">
|
||||||
|
<StatTile
|
||||||
|
icon={<BoxIcon size={13} />}
|
||||||
|
label="Quantity"
|
||||||
|
value={`${line.quantity}`}
|
||||||
|
sub={`container${line.quantity !== 1 ? "s" : ""}`}
|
||||||
|
/>
|
||||||
|
<StatTile
|
||||||
|
icon={<Scale size={13} />}
|
||||||
|
label="VGM / unit"
|
||||||
|
value={fmtWeight(Number(line.vgmPerUnitTons || 0))}
|
||||||
|
/>
|
||||||
|
<StatTile
|
||||||
|
icon={<Scale size={13} />}
|
||||||
|
label="Line total VGM"
|
||||||
|
value={fmtWeight(Number(line.totalVgmTons || 0))}
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
{units.length > 0 && (
|
||||||
|
<Box style={{ overflowX: "auto" }}>
|
||||||
|
<Table verticalSpacing="sm" horizontalSpacing="sm" miw={640}>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th style={th}>Container no.</Table.Th>
|
||||||
|
<Table.Th style={th}>Seal no.</Table.Th>
|
||||||
|
<Table.Th style={th}>VGM</Table.Th>
|
||||||
|
<Table.Th style={th}>Flags</Table.Th>
|
||||||
|
<Table.Th style={th}>GRN</Table.Th>
|
||||||
|
<Table.Th style={th}>Port status</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{units.map((u) => (
|
||||||
|
<UnitRow key={u.id} unit={u} />
|
||||||
|
))}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{units.length === 0 && (
|
||||||
|
<Text fz={12.5} c="#9AA8B5">
|
||||||
|
Container numbers will appear here once the physical units are assigned.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</SectionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Bulk ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function BulkCargoCard({ booking }: { booking: BookingDetail }) {
|
||||||
|
const unit = booking.cargoType?.unitOfMeasure;
|
||||||
|
const isPerItem = unit === "PER_ITEM";
|
||||||
|
// Break-bulk (PER_ITEM): cargoTotalWeightVgm holds the ITEM COUNT and the
|
||||||
|
// real tonnage lives in bulkTotalWeightTons; PER_TON stores tons directly.
|
||||||
|
const quantity = Number(booking.cargoTotalWeightVgm || 0);
|
||||||
|
const tons = totalVgmTons(booking);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SectionCard>
|
||||||
|
<Group gap={10} align="center" mb="md">
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: "#E8F5EF",
|
||||||
|
color: "#0A6F4D",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Package size={18} />
|
||||||
|
</Box>
|
||||||
|
<Box>
|
||||||
|
<Text fz={15} fw={800} c="#10202F">
|
||||||
|
{commodityLabel(booking)}
|
||||||
|
</Text>
|
||||||
|
<Text fz={12} c="#9AA8B5">
|
||||||
|
Bulk cargo{booking.cargoType?.code ? ` · ${booking.cargoType.code}` : ""}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<SimpleGrid cols={{ base: 1, sm: isPerItem ? 3 : 2 }} spacing={10} mb="md">
|
||||||
|
{isPerItem && (
|
||||||
|
<StatTile
|
||||||
|
icon={<BoxIcon size={13} />}
|
||||||
|
label="Items"
|
||||||
|
value={quantity ? quantity.toLocaleString() : "—"}
|
||||||
|
sub="declared item count"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<StatTile
|
||||||
|
icon={<Scale size={13} />}
|
||||||
|
label="Total weight"
|
||||||
|
value={fmtWeight(tons)}
|
||||||
|
sub={isPerItem ? "actual tonnage" : "declared tonnage"}
|
||||||
|
/>
|
||||||
|
<StatTile
|
||||||
|
icon={<Package size={13} />}
|
||||||
|
label="Billing unit"
|
||||||
|
value={isPerItem ? "Per item" : "Per ton"}
|
||||||
|
/>
|
||||||
|
</SimpleGrid>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<DetailRow label="Commodity" value={commodityLabel(booking)} />
|
||||||
|
{booking.cargoType?.code && (
|
||||||
|
<DetailRow label="Cargo type code" value={booking.cargoType.code} />
|
||||||
|
)}
|
||||||
|
{booking.cargoFreeText && booking.cargoType?.cargoTypeName && (
|
||||||
|
<DetailRow label="Cargo description" value={booking.cargoFreeText} />
|
||||||
|
)}
|
||||||
|
<DetailRow
|
||||||
|
label="Hazardous"
|
||||||
|
value={
|
||||||
|
Number(booking.bulkHazardousQuantity || 0)
|
||||||
|
? `${Number(booking.bulkHazardousQuantity).toLocaleString()} ${isPerItem ? "items" : "t"}`
|
||||||
|
: booking.isHazardous
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="Refrigerated"
|
||||||
|
value={
|
||||||
|
Number(booking.bulkReeferQuantity || 0)
|
||||||
|
? `${Number(booking.bulkReeferQuantity).toLocaleString()} ${isPerItem ? "items" : "t"}`
|
||||||
|
: booking.isRefrigerated
|
||||||
|
? "Yes"
|
||||||
|
: "No"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<DetailRow
|
||||||
|
label="Equipment return"
|
||||||
|
value={booking.equipmentReturn === "WITH_RETURN" ? "With return" : "Without return"}
|
||||||
|
/>
|
||||||
|
<DetailRow label="Shipping line" value={shippingLineLabel(booking)} />
|
||||||
|
</Box>
|
||||||
|
</SectionCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tab ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dedicated cargo breakdown tab: bulk bookings get the full bulk declaration
|
||||||
|
* (commodity, unit of measure, item count vs tonnage, hazardous/reefer
|
||||||
|
* quantities); container bookings get one card per container line with its
|
||||||
|
* per-unit numbers, seals, VGM, GRN and port-arrival status.
|
||||||
|
*/
|
||||||
|
export function CargoTab({ booking }: { booking: BookingDetail }) {
|
||||||
|
const isBulk = booking.freightType === "BULK";
|
||||||
|
const lines = booking.bookingContainers ?? [];
|
||||||
|
const totalUnits = lines.reduce((s, c) => s + Number(c.quantity || 0), 0);
|
||||||
|
const totalVgm = totalVgmTons(booking);
|
||||||
|
const receivedCount = lines
|
||||||
|
.flatMap((l) => l.units ?? [])
|
||||||
|
.filter((u) => u.receivedToPort).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-6" style={{ maxWidth: 980 }}>
|
||||||
|
<SectionCard>
|
||||||
|
<CardTitle>Cargo summary</CardTitle>
|
||||||
|
<SimpleGrid cols={{ base: 2, sm: 4 }} spacing={10} mt="md">
|
||||||
|
<StatTile
|
||||||
|
icon={isBulk ? <Package size={13} /> : <Container size={13} />}
|
||||||
|
label="Freight type"
|
||||||
|
value={isBulk ? "Bulk" : "Container"}
|
||||||
|
/>
|
||||||
|
<StatTile
|
||||||
|
icon={<BoxIcon size={13} />}
|
||||||
|
label="Commodity"
|
||||||
|
value={commodityLabel(booking)}
|
||||||
|
/>
|
||||||
|
<StatTile
|
||||||
|
icon={<Scale size={13} />}
|
||||||
|
label="Total weight"
|
||||||
|
value={fmtWeight(totalVgm)}
|
||||||
|
/>
|
||||||
|
{isBulk ? (
|
||||||
|
<StatTile
|
||||||
|
icon={<Flame size={13} />}
|
||||||
|
label="Special handling"
|
||||||
|
value={
|
||||||
|
[
|
||||||
|
booking.isHazardous && "Hazardous",
|
||||||
|
booking.isRefrigerated && "Reefer",
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(" · ") || "None"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<StatTile
|
||||||
|
icon={<Container size={13} />}
|
||||||
|
label="Containers"
|
||||||
|
value={`${totalUnits}`}
|
||||||
|
sub={`${receivedCount} received at port`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</SimpleGrid>
|
||||||
|
</SectionCard>
|
||||||
|
|
||||||
|
{isBulk ? (
|
||||||
|
<BulkCargoCard booking={booking} />
|
||||||
|
) : lines.length > 0 ? (
|
||||||
|
lines.map((line, i) => (
|
||||||
|
<ContainerLineCard key={line.id ?? i} line={line} index={i} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<SectionCard>
|
||||||
|
<Text fz={13.5} c="#9AA8B5">
|
||||||
|
No container details recorded for this booking yet.
|
||||||
|
</Text>
|
||||||
|
</SectionCard>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -292,7 +292,10 @@ export default function NewBookingPage() {
|
|||||||
// physically carry the selected cargo/container type. Quantity is NOT part
|
// physically carry the selected cargo/container type. Quantity is NOT part
|
||||||
// of this gate — an oversized booking is accepted and gets a partial split
|
// of this gate — an oversized booking is accepted and gets a partial split
|
||||||
// offer later. Only selectable days reach the UI; no capacity counts.
|
// offer later. Only selectable days reach the UI; no capacity counts.
|
||||||
const gateContainerTypeIds = useMemo(() => {
|
// Computed per render on purpose — NOT useMemo. react-hook-form mutates the
|
||||||
|
// watched containers array in place on nested edits (containers.0.containerType),
|
||||||
|
// so a reference-based dep list never sees per-line changes.
|
||||||
|
const gateContainerTypeIds = (() => {
|
||||||
if (watchedCargoKind !== "container") return [];
|
if (watchedCargoKind !== "container") return [];
|
||||||
const groups = referenceData?.containers ?? [];
|
const groups = referenceData?.containers ?? [];
|
||||||
const ids = new Set<string>();
|
const ids = new Set<string>();
|
||||||
@@ -304,7 +307,7 @@ export default function NewBookingPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return [...ids];
|
return [...ids];
|
||||||
}, [watchedCargoKind, watchedContainers, referenceData]);
|
})();
|
||||||
const gateCargoTypeId =
|
const gateCargoTypeId =
|
||||||
watchedCargoKind === "bulk" ? watchedCargoTypePath?.[1] : undefined;
|
watchedCargoKind === "bulk" ? watchedCargoTypePath?.[1] : undefined;
|
||||||
const gateReady =
|
const gateReady =
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ const PAYMENT_COLORS: Record<string, string> = {
|
|||||||
// Backend emits the long form on some flows; keep the short alias too.
|
// Backend emits the long form on some flows; keep the short alias too.
|
||||||
VERIFICATION_IN_PROGRESS: "yellow",
|
VERIFICATION_IN_PROGRESS: "yellow",
|
||||||
PAYMENT_VERIFICATION_IN_PROGRESS: "yellow",
|
PAYMENT_VERIFICATION_IN_PROGRESS: "yellow",
|
||||||
|
PAYMENT_PROCESSING: "yellow",
|
||||||
OVERDUE: "red",
|
OVERDUE: "red",
|
||||||
REFUNDED: "blue",
|
REFUNDED: "blue",
|
||||||
CANCELLED: "gray",
|
CANCELLED: "gray",
|
||||||
@@ -132,6 +133,7 @@ const PAYMENT_LABELS: Record<string, string> = {
|
|||||||
PNR_GENERATED: "PNR generated",
|
PNR_GENERATED: "PNR generated",
|
||||||
VERIFICATION_IN_PROGRESS: "Verifying",
|
VERIFICATION_IN_PROGRESS: "Verifying",
|
||||||
PAYMENT_VERIFICATION_IN_PROGRESS: "Verifying",
|
PAYMENT_VERIFICATION_IN_PROGRESS: "Verifying",
|
||||||
|
PAYMENT_PROCESSING: "Payment processing",
|
||||||
OVERDUE: "Overdue",
|
OVERDUE: "Overdue",
|
||||||
REFUNDED: "Refunded",
|
REFUNDED: "Refunded",
|
||||||
CANCELLED: "Cancelled",
|
CANCELLED: "Cancelled",
|
||||||
|
|||||||
@@ -1006,9 +1006,11 @@ function ScheduleStep({
|
|||||||
const isContainer = contract.freightType === "CONTAINER";
|
const isContainer = contract.freightType === "CONTAINER";
|
||||||
const containerLines = form.watch("containers");
|
const containerLines = form.watch("containers");
|
||||||
const cargoWeightTons = form.watch("cargoWeightTons");
|
const cargoWeightTons = form.watch("cargoWeightTons");
|
||||||
const itemCount = form.watch("itemCount");
|
|
||||||
|
|
||||||
const cargoQuery = useMemo<Freight.AvailableDaysForCargoQuery | null>(() => {
|
// Computed per render on purpose — NOT useMemo. react-hook-form mutates the
|
||||||
|
// watched containers array in place on nested edits (containers.0.quantity),
|
||||||
|
// so a reference-based dep list never sees manual quantity changes.
|
||||||
|
const cargoQuery = ((): Freight.AvailableDaysForCargoQuery | null => {
|
||||||
if (!route?.originYardId || !route?.destinationYardId) return null;
|
if (!route?.originYardId || !route?.destinationYardId) return null;
|
||||||
if (isContainer) {
|
if (isContainer) {
|
||||||
const containers = (containerLines ?? [])
|
const containers = (containerLines ?? [])
|
||||||
@@ -1036,17 +1038,7 @@ function ScheduleStep({
|
|||||||
?.cargoTypeCode ?? undefined,
|
?.cargoTypeCode ?? undefined,
|
||||||
totalWeightTons: tons,
|
totalWeightTons: tons,
|
||||||
};
|
};
|
||||||
// Tonnage is the sizing input the day-feasibility endpoint takes, and
|
})();
|
||||||
// PER_ITEM cargo now captures it too — itemCount stays in the deps so the
|
|
||||||
// query still refreshes when only the item count changes.
|
|
||||||
}, [
|
|
||||||
route,
|
|
||||||
isContainer,
|
|
||||||
containerLines,
|
|
||||||
cargoWeightTons,
|
|
||||||
itemCount,
|
|
||||||
contract.pricingBreakdown,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const isIntercity = contract.tradeDirection === "DOMESTIC";
|
const isIntercity = contract.tradeDirection === "DOMESTIC";
|
||||||
const { data: availableDays, isLoading } = useQuery({
|
const { data: availableDays, isLoading } = useQuery({
|
||||||
|
|||||||
@@ -8,10 +8,22 @@ import {
|
|||||||
ThemeIcon,
|
ThemeIcon,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { CheckCircle2, FileText, Home } from "lucide-react";
|
import { CheckCircle2, FileText, Home } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useEffect } from "react";
|
||||||
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
import { paymentsService } from "@/services/payments.service";
|
||||||
|
|
||||||
export default function PaymentSuccessPage() {
|
export default function PaymentSuccessPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
const bookingId = searchParams.get("bookingId");
|
||||||
|
|
||||||
|
// Fire-and-forget ack: payment → processing, invoice → PAYMENT_PROCESSING.
|
||||||
|
// The provider webhook remains the source of truth for the final PAID state.
|
||||||
|
useEffect(() => {
|
||||||
|
if (bookingId) {
|
||||||
|
paymentsService.acknowledgeSuccessRedirect(bookingId).catch(() => {});
|
||||||
|
}
|
||||||
|
}, [bookingId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ export const paymentsService = {
|
|||||||
return data.data ?? data;
|
return data.data ?? data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Success-redirect ack: payment → processing, invoice → PAYMENT_PROCESSING. */
|
||||||
|
acknowledgeSuccessRedirect: async (bookingId: string): Promise<void> => {
|
||||||
|
await client.post(P.REDIRECT_SUCCESS(bookingId));
|
||||||
|
},
|
||||||
|
|
||||||
checkoutUrl: buildCheckoutUrl,
|
checkoutUrl: buildCheckoutUrl,
|
||||||
checkoutUrlForInvoice: buildCheckoutUrlForInvoice,
|
checkoutUrlForInvoice: buildCheckoutUrlForInvoice,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
import { loadEnv } from "vite";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
@@ -13,26 +14,30 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|||||||
const mantineCore = path.resolve(__dirname, "node_modules/@mantine/core");
|
const mantineCore = path.resolve(__dirname, "node_modules/@mantine/core");
|
||||||
const mantineHooks = path.resolve(__dirname, "node_modules/@mantine/hooks");
|
const mantineHooks = path.resolve(__dirname, "node_modules/@mantine/hooks");
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => {
|
||||||
plugins: [react(), tailwindcss()],
|
const env = loadEnv(mode, __dirname, "");
|
||||||
resolve: {
|
|
||||||
alias: {
|
return {
|
||||||
"@": path.resolve(__dirname, "./src"),
|
plugins: [react(), tailwindcss()],
|
||||||
// Resolve from TS source so Vite gets ESM named exports (dist is CommonJS).
|
resolve: {
|
||||||
"@edr/types": path.resolve(__dirname, "../../../packages/types/src/index.ts"),
|
alias: {
|
||||||
"@mantine/core": mantineCore,
|
"@": path.resolve(__dirname, "./src"),
|
||||||
"@mantine/hooks": mantineHooks,
|
// Resolve from TS source so Vite gets ESM named exports (dist is CommonJS).
|
||||||
|
"@edr/types": path.resolve(__dirname, "../../../packages/types/src/index.ts"),
|
||||||
|
"@mantine/core": mantineCore,
|
||||||
|
"@mantine/hooks": mantineHooks,
|
||||||
|
},
|
||||||
|
dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"],
|
||||||
},
|
},
|
||||||
dedupe: ["react", "react-dom", "@mantine/core", "@mantine/hooks"],
|
optimizeDeps: {
|
||||||
},
|
include: ["@mantine/core", "@mantine/hooks", "@edr/ui-common"],
|
||||||
optimizeDeps: {
|
},
|
||||||
include: ["@mantine/core", "@mantine/hooks", "@edr/ui-common"],
|
server: {
|
||||||
},
|
port: Number(env.PORT) || 5273,
|
||||||
server: {
|
host: "0.0.0.0",
|
||||||
port: 5173,
|
},
|
||||||
host: "0.0.0.0",
|
test: {
|
||||||
},
|
environment: "node",
|
||||||
test: {
|
},
|
||||||
environment: "node",
|
};
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ export class CbeBillService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ query */
|
/* ------------------------------------------------------------------ query */
|
||||||
|
|
||||||
async query(dto: CbeQueryRequestDto): Promise<CbeQueryResponseDto> {
|
async query(dto: CbeQueryRequestDto): Promise<CbeQueryResponseDto> {
|
||||||
@@ -168,6 +169,14 @@ export class CbeBillService {
|
|||||||
async pay(dto: CbePaymentRequestDto): Promise<CbePaymentResponseDto> {
|
async pay(dto: CbePaymentRequestDto): Promise<CbePaymentResponseDto> {
|
||||||
this.assertEnabled();
|
this.assertEnabled();
|
||||||
|
|
||||||
|
this.logger.log({
|
||||||
|
msg: "cbe.payment.request",
|
||||||
|
billId: dto.Bill_Id,
|
||||||
|
endToEndTxnId: dto.End_To_End_Txn_Id,
|
||||||
|
cbeTxnRef: dto.Cbe_Txn_Ref,
|
||||||
|
request: dto,
|
||||||
|
});
|
||||||
|
|
||||||
// §6.5 idempotency on CBE's per-attempt id, in order.
|
// §6.5 idempotency on CBE's per-attempt id, in order.
|
||||||
const prior = await this.cbeBillRepository.findByEndToEndTxnId(
|
const prior = await this.cbeBillRepository.findByEndToEndTxnId(
|
||||||
dto.End_To_End_Txn_Id,
|
dto.End_To_End_Txn_Id,
|
||||||
|
|||||||
@@ -24,7 +24,11 @@ describe("IntentsService CBE_BILL", () => {
|
|||||||
let repository: jest.Mocked<
|
let repository: jest.Mocked<
|
||||||
Pick<
|
Pick<
|
||||||
IntentsRepository,
|
IntentsRepository,
|
||||||
"create" | "findById" | "findByIdempotencyKey" | "update"
|
| "create"
|
||||||
|
| "findById"
|
||||||
|
| "findByIdempotencyKey"
|
||||||
|
| "findAllByReference"
|
||||||
|
| "update"
|
||||||
>
|
>
|
||||||
>;
|
>;
|
||||||
let billReferenceService: { generate: jest.Mock };
|
let billReferenceService: { generate: jest.Mock };
|
||||||
@@ -46,6 +50,7 @@ describe("IntentsService CBE_BILL", () => {
|
|||||||
create: jest.fn(async (data) => ({ id: "intent-1", ...data })),
|
create: jest.fn(async (data) => ({ id: "intent-1", ...data })),
|
||||||
findById: jest.fn(),
|
findById: jest.fn(),
|
||||||
findByIdempotencyKey: jest.fn().mockResolvedValue(null),
|
findByIdempotencyKey: jest.fn().mockResolvedValue(null),
|
||||||
|
findAllByReference: jest.fn().mockResolvedValue([]),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
} as never;
|
} as never;
|
||||||
billReferenceService = {
|
billReferenceService = {
|
||||||
@@ -79,6 +84,47 @@ describe("IntentsService CBE_BILL", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("reuses the open bill instead of minting a second reference", async () => {
|
||||||
|
repository.findAllByReference.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "intent-1",
|
||||||
|
provider: ProviderMethod.CBE_BILL,
|
||||||
|
status: ProviderPaymentStatus.REQUIRES_ACTION,
|
||||||
|
billReference: "000100000015",
|
||||||
|
amountMinor: 1500,
|
||||||
|
currency: "ETB",
|
||||||
|
clientAction: {
|
||||||
|
type: "SHOW_BILL_REFERENCE",
|
||||||
|
billReference: "000100000015",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
|
||||||
|
const snapshot = await service.initiate(request);
|
||||||
|
|
||||||
|
expect(snapshot.billReference).toBe("000100000015");
|
||||||
|
expect(billReferenceService.generate).not.toHaveBeenCalled();
|
||||||
|
expect(repository.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mints a new bill when the amount changed", async () => {
|
||||||
|
repository.findAllByReference.mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: "intent-1",
|
||||||
|
provider: ProviderMethod.CBE_BILL,
|
||||||
|
status: ProviderPaymentStatus.REQUIRES_ACTION,
|
||||||
|
billReference: "000100000015",
|
||||||
|
amountMinor: 900,
|
||||||
|
currency: "ETB",
|
||||||
|
},
|
||||||
|
] as never);
|
||||||
|
billReferenceService.generate.mockResolvedValue("000100000023");
|
||||||
|
|
||||||
|
const snapshot = await service.initiate(request);
|
||||||
|
|
||||||
|
expect(snapshot.billReference).toBe("000100000023");
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects non-ETB currency (plan D8)", async () => {
|
it("rejects non-ETB currency (plan D8)", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
service.initiate({ ...request, currency: "DJF" }),
|
service.initiate({ ...request, currency: "DJF" }),
|
||||||
|
|||||||
@@ -163,6 +163,32 @@ export class IntentsService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The bill reference is issued ONCE per order: the domain app persists it (freight stores it
|
||||||
|
// as the booking's PNR) and the payer may already have written it down, so re-initiating the
|
||||||
|
// same open bill must hand back the same number. A different amount/currency means a
|
||||||
|
// different debt — /cbe/payment verifies the debited amount against the intent — so that
|
||||||
|
// case mints a fresh bill instead of silently repricing an outstanding one.
|
||||||
|
const open = (
|
||||||
|
await this.intentsRepository.findAllByReference(
|
||||||
|
request.service,
|
||||||
|
request.referenceType,
|
||||||
|
request.referenceId,
|
||||||
|
)
|
||||||
|
).find(
|
||||||
|
(i) =>
|
||||||
|
i.provider === ProviderMethod.CBE_BILL &&
|
||||||
|
i.status === ProviderPaymentStatus.REQUIRES_ACTION &&
|
||||||
|
!!i.billReference &&
|
||||||
|
i.amountMinor === request.amountMinor &&
|
||||||
|
i.currency === request.currency,
|
||||||
|
);
|
||||||
|
if (open) {
|
||||||
|
this.logger.log(
|
||||||
|
`intent ${open.id} reused for ${request.service}/${request.referenceType}/${request.referenceId} via CBE_BILL (bill ${open.billReference})`,
|
||||||
|
);
|
||||||
|
return this.toSnapshot(open);
|
||||||
|
}
|
||||||
|
|
||||||
const merchantOrderId = createMerchantOrderId();
|
const merchantOrderId = createMerchantOrderId();
|
||||||
const billReference = await this.billReferenceService.generate();
|
const billReference = await this.billReferenceService.generate();
|
||||||
// expiresAt is the BOOKING's payment deadline passed by the domain app — never a provider
|
// expiresAt is the BOOKING's payment deadline passed by the domain app — never a provider
|
||||||
|
|||||||
BIN
local-packages/tria-plc-auditlog-1.1.2.tgz
Normal file
BIN
local-packages/tria-plc-auditlog-1.1.2.tgz
Normal file
Binary file not shown.
@@ -161,6 +161,8 @@ export enum InvoiceStatus {
|
|||||||
/** Issued and awaiting payment (alias of PENDING for fee invoices). */
|
/** Issued and awaiting payment (alias of PENDING for fee invoices). */
|
||||||
Issued = "ISSUED",
|
Issued = "ISSUED",
|
||||||
Pending = "PENDING",
|
Pending = "PENDING",
|
||||||
|
/** Customer completed provider checkout (success redirect); awaiting webhook confirmation. */
|
||||||
|
PaymentProcessing = "PAYMENT_PROCESSING",
|
||||||
/** Some, but not all, of the balance has been settled. */
|
/** Some, but not all, of the balance has been settled. */
|
||||||
PartiallyPaid = "PARTIALLY_PAID",
|
PartiallyPaid = "PARTIALLY_PAID",
|
||||||
Paid = "PAID",
|
Paid = "PAID",
|
||||||
|
|||||||
313
pnpm-lock.yaml
generated
313
pnpm-lock.yaml
generated
@@ -99,6 +99,9 @@ importers:
|
|||||||
'@tria-plc/api-common':
|
'@tria-plc/api-common':
|
||||||
specifier: file:../../local-packages/tria-plc-api-common-1.6.0.tgz
|
specifier: file:../../local-packages/tria-plc-api-common-1.6.0.tgz
|
||||||
version: file:local-packages/tria-plc-api-common-1.6.0.tgz(ae9a56cc1c6d93629dd85f7605ccd5b6)
|
version: file:local-packages/tria-plc-api-common-1.6.0.tgz(ae9a56cc1c6d93629dd85f7605ccd5b6)
|
||||||
|
'@tria-plc/auditlog':
|
||||||
|
specifier: file:../../local-packages/tria-plc-auditlog-1.1.2.tgz
|
||||||
|
version: file:local-packages/tria-plc-auditlog-1.1.2.tgz(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/microservices@11.1.24)(@nestjs/swagger@11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2))(@nestjs/typeorm@11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))))(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
||||||
'@tria-plc/iamapi-common':
|
'@tria-plc/iamapi-common':
|
||||||
specifier: file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz
|
specifier: file:../../local-packages/tria-plc-iamapi-common-1.0.0.tgz
|
||||||
version: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(d0be280d95adfc1b38e59bdc80c5dec5)
|
version: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(d0be280d95adfc1b38e59bdc80c5dec5)
|
||||||
@@ -595,7 +598,7 @@ importers:
|
|||||||
version: 5.101.0(react@19.2.6)
|
version: 5.101.0(react@19.2.6)
|
||||||
'@tria-plc/iamui':
|
'@tria-plc/iamui':
|
||||||
specifier: file:../../../local-packages/tria-plc-iamui-0.1.1.tgz
|
specifier: file:../../../local-packages/tria-plc-iamui-0.1.1.tgz
|
||||||
version: file:local-packages/tria-plc-iamui-0.1.1.tgz(a5d0bdee55164ae56064fb273b530e00)
|
version: file:local-packages/tria-plc-iamui-0.1.1.tgz(0ce39b7e349029277dcd938d06eeb0f7)
|
||||||
'@vis.gl/react-google-maps':
|
'@vis.gl/react-google-maps':
|
||||||
specifier: ^1.8.3
|
specifier: ^1.8.3
|
||||||
version: 1.8.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
version: 1.8.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
||||||
@@ -4647,6 +4650,19 @@ packages:
|
|||||||
rxjs: ^7.8.0
|
rxjs: ^7.8.0
|
||||||
typeorm: ^0.3.0
|
typeorm: ^0.3.0
|
||||||
|
|
||||||
|
'@tria-plc/auditlog@file:local-packages/tria-plc-auditlog-1.1.2.tgz':
|
||||||
|
resolution: {integrity: sha512-3kRaAtETvM9wVRSbwd2jyrts9DCcSV9yFUDoEpxJfVQDsIGY2d9K0c+h1LAjfNNaogZ8eeBcroyCrnWjwBmkYA==, tarball: file:local-packages/tria-plc-auditlog-1.1.2.tgz}
|
||||||
|
version: 1.1.2
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@nestjs/common': ^10.0.0 || ^11.0.0
|
||||||
|
'@nestjs/core': ^10.0.0 || ^11.0.0
|
||||||
|
'@nestjs/microservices': ^10.0.0 || ^11.0.0
|
||||||
|
'@nestjs/swagger': ^10.0.0 || ^11.0.0
|
||||||
|
'@nestjs/typeorm': ^10.0.0 || ^11.0.0
|
||||||
|
rxjs: ^7.0.0
|
||||||
|
typeorm: ^0.3.0
|
||||||
|
|
||||||
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz':
|
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz':
|
||||||
resolution: {integrity: sha512-rfHSXOm/0VUMTj7HrvYysrEAqxItqfPs4Rd35qWJyaAk+snF1wTKFRVz6cRGPoQNj8fhplkVAefnvuKBuHT+xQ==, tarball: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz}
|
resolution: {integrity: sha512-rfHSXOm/0VUMTj7HrvYysrEAqxItqfPs4Rd35qWJyaAk+snF1wTKFRVz6cRGPoQNj8fhplkVAefnvuKBuHT+xQ==, tarball: file:local-packages/tria-plc-iamapi-common-1.0.0.tgz}
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -12298,11 +12314,11 @@ snapshots:
|
|||||||
'@babel/helpers': 7.29.7
|
'@babel/helpers': 7.29.7
|
||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/template': 7.29.7
|
'@babel/template': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
'@jridgewell/remapping': 2.3.5
|
'@jridgewell/remapping': 2.3.5
|
||||||
convert-source-map: 2.0.0
|
convert-source-map: 2.0.0
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
gensync: 1.0.0-beta.2
|
gensync: 1.0.0-beta.2
|
||||||
json5: 2.2.3
|
json5: 2.2.3
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
@@ -12337,7 +12353,7 @@ snapshots:
|
|||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
|
'@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7)
|
||||||
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
'@babel/helper-skip-transparent-expression-wrappers': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
semver: 6.3.1
|
semver: 6.3.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -12346,14 +12362,7 @@ snapshots:
|
|||||||
|
|
||||||
'@babel/helper-member-expression-to-functions@7.29.7':
|
'@babel/helper-member-expression-to-functions@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/types': 7.29.7
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/helper-module-imports@7.29.7':
|
|
||||||
dependencies:
|
|
||||||
'@babel/traverse': 7.29.7
|
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -12368,9 +12377,9 @@ snapshots:
|
|||||||
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
'@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-module-imports': 7.29.7
|
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/helper-validator-identifier': 7.29.7
|
'@babel/helper-validator-identifier': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -12385,13 +12394,13 @@ snapshots:
|
|||||||
'@babel/core': 7.29.7
|
'@babel/core': 7.29.7
|
||||||
'@babel/helper-member-expression-to-functions': 7.29.7
|
'@babel/helper-member-expression-to-functions': 7.29.7
|
||||||
'@babel/helper-optimise-call-expression': 7.29.7
|
'@babel/helper-optimise-call-expression': 7.29.7
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
'@babel/helper-skip-transparent-expression-wrappers@7.29.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/traverse': 7.29.7
|
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -12544,18 +12553,6 @@ snapshots:
|
|||||||
'@babel/parser': 7.29.7
|
'@babel/parser': 7.29.7
|
||||||
'@babel/types': 7.29.7
|
'@babel/types': 7.29.7
|
||||||
|
|
||||||
'@babel/traverse@7.29.7':
|
|
||||||
dependencies:
|
|
||||||
'@babel/code-frame': 7.29.7
|
|
||||||
'@babel/generator': 7.29.7
|
|
||||||
'@babel/helper-globals': 7.29.7
|
|
||||||
'@babel/parser': 7.29.7
|
|
||||||
'@babel/template': 7.29.7
|
|
||||||
'@babel/types': 7.29.7
|
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@babel/traverse@7.29.7(supports-color@5.5.0)':
|
'@babel/traverse@7.29.7(supports-color@5.5.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.29.7
|
'@babel/code-frame': 7.29.7
|
||||||
@@ -12781,7 +12778,7 @@ snapshots:
|
|||||||
|
|
||||||
'@emotion/babel-plugin@11.13.5':
|
'@emotion/babel-plugin@11.13.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/helper-module-imports': 7.29.7
|
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
|
||||||
'@babel/runtime': 7.29.7
|
'@babel/runtime': 7.29.7
|
||||||
'@emotion/hash': 0.9.2
|
'@emotion/hash': 0.9.2
|
||||||
'@emotion/memoize': 0.9.0
|
'@emotion/memoize': 0.9.0
|
||||||
@@ -12947,7 +12944,7 @@ snapshots:
|
|||||||
'@eslint/eslintrc@2.1.4':
|
'@eslint/eslintrc@2.1.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 6.15.0
|
ajv: 6.15.0
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
espree: 9.6.1
|
espree: 9.6.1
|
||||||
globals: 13.24.0
|
globals: 13.24.0
|
||||||
ignore: 5.3.2
|
ignore: 5.3.2
|
||||||
@@ -13107,7 +13104,7 @@ snapshots:
|
|||||||
'@humanwhocodes/config-array@0.13.0':
|
'@humanwhocodes/config-array@0.13.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@humanwhocodes/object-schema': 2.0.3
|
'@humanwhocodes/object-schema': 2.0.3
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
minimatch: 3.1.5
|
minimatch: 3.1.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -14304,7 +14301,7 @@ snapshots:
|
|||||||
|
|
||||||
'@puppeteer/browsers@2.13.2':
|
'@puppeteer/browsers@2.13.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
extract-zip: 2.0.1
|
extract-zip: 2.0.1
|
||||||
progress: 2.0.3
|
progress: 2.0.3
|
||||||
proxy-agent: 6.5.0
|
proxy-agent: 6.5.0
|
||||||
@@ -16372,7 +16369,7 @@ snapshots:
|
|||||||
|
|
||||||
'@tokenizer/inflate@0.4.1':
|
'@tokenizer/inflate@0.4.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
token-types: 6.1.2
|
token-types: 6.1.2
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -16467,6 +16464,18 @@ snapshots:
|
|||||||
- debug
|
- debug
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
'@tria-plc/auditlog@file:local-packages/tria-plc-auditlog-1.1.2.tgz(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/microservices@11.1.24)(@nestjs/swagger@11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2))(@nestjs/typeorm@11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))))(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))':
|
||||||
|
dependencies:
|
||||||
|
'@nestjs/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/core': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/microservices@11.1.24)(@nestjs/platform-express@11.1.24)(@nestjs/websockets@11.1.27)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/microservices': 11.1.24(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(@nestjs/websockets@11.1.27)(amqp-connection-manager@5.0.0(amqplib@2.0.1))(amqplib@2.0.1)(reflect-metadata@0.2.2)(rxjs@7.8.2)
|
||||||
|
'@nestjs/swagger': 11.4.4(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)
|
||||||
|
'@nestjs/typeorm': 11.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.24)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3)))
|
||||||
|
amqp-connection-manager: 5.0.0(amqplib@0.10.9)
|
||||||
|
amqplib: 0.10.9
|
||||||
|
rxjs: 7.8.2
|
||||||
|
typeorm: 0.3.30(babel-plugin-macros@3.1.0)(pg@8.21.0)(ts-node@10.9.2(@types/node@20.19.42)(typescript@5.9.3))
|
||||||
|
|
||||||
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(cc085a020c559b355f168432c579a024)':
|
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-1.0.0.tgz(cc085a020c559b355f168432c579a024)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
'@nestjs/axios': 4.0.1(@nestjs/common@11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2))(axios@1.17.0)(rxjs@7.8.2)
|
||||||
@@ -16659,130 +16668,6 @@ snapshots:
|
|||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
- vite
|
- vite
|
||||||
|
|
||||||
'@tria-plc/iamui@file:local-packages/tria-plc-iamui-0.1.1.tgz(a5d0bdee55164ae56064fb273b530e00)':
|
|
||||||
dependencies:
|
|
||||||
'@emotion/react': 11.14.0(@types/react@18.3.31)(react@19.2.6)
|
|
||||||
'@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@19.2.6))(@types/react@18.3.31)(react@19.2.6)
|
|
||||||
'@hookform/resolvers': 5.4.0(react-hook-form@7.77.0(react@19.2.6))
|
|
||||||
'@lottiefiles/react-lottie-player': 3.6.0(react@19.2.6)
|
|
||||||
'@mantine/charts': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(recharts@3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1))
|
|
||||||
'@mantine/core': 7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@mantine/dates': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@mantine/hooks': 7.17.8(react@19.2.6)
|
|
||||||
'@mantine/notifications': 7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-accordion': 1.2.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-alert-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-avatar': 1.1.12(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-checkbox': 1.3.4(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-collapsible': 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-context-menu': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-dialog': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-dropdown-menu': 2.1.17(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-hover-card': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-label': 2.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-navigation-menu': 1.2.15(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-popover': 1.1.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-progress': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-radio-group': 1.4.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-scroll-area': 1.2.11(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-select': 2.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-separator': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-slot': 1.2.5(@types/react@18.3.31)(react@19.2.6)
|
|
||||||
'@radix-ui/react-switch': 1.3.0(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-tabs': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-toast': 1.2.16(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@radix-ui/react-tooltip': 1.2.9(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@react-pdf-viewer/core': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@react-pdf-viewer/default-layout': 3.12.0(pdfjs-dist@5.4.296)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@react-pdf/renderer': 4.5.1(react@19.2.6)
|
|
||||||
'@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1))(react@19.2.6)
|
|
||||||
'@tabler/icons-react': 3.44.0(react@19.2.6)
|
|
||||||
'@tailwindcss/vite': 4.3.0(vite@5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0))
|
|
||||||
'@tanstack/react-query': 5.101.0(react@19.2.6)
|
|
||||||
'@tanstack/react-query-devtools': 5.101.0(@tanstack/react-query@5.101.0(react@19.2.6))(react@19.2.6)
|
|
||||||
'@tanstack/react-table': 8.21.3(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
'@tinymce/tinymce-react': 6.3.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tinymce@7.9.3)
|
|
||||||
'@types/dompurify': 3.2.0
|
|
||||||
'@types/node': 24.13.1
|
|
||||||
'@types/tinymce': 4.6.9
|
|
||||||
axios: 1.17.0
|
|
||||||
class-variance-authority: 0.7.1
|
|
||||||
clsx: 2.1.1
|
|
||||||
cmdk: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
date-fns: 3.6.0
|
|
||||||
dayjs: 1.11.21
|
|
||||||
dompurify: 3.4.8
|
|
||||||
ethiopian-calendar-date-converter: 2.1.6
|
|
||||||
ethiopian-calendar-new: 1.1.0
|
|
||||||
file-type: 18.7.0
|
|
||||||
framer-motion: 12.40.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
html2canvas: 1.4.1
|
|
||||||
i18next: 25.10.10(typescript@5.9.3)
|
|
||||||
i18next-browser-languagedetector: 8.2.1
|
|
||||||
jquery: 3.7.1
|
|
||||||
js-cookie: 3.0.8
|
|
||||||
jspdf: 3.0.4
|
|
||||||
lodash: 4.18.1
|
|
||||||
lucide-react: 0.513.0(react@19.2.6)
|
|
||||||
mantine-react-table: 2.0.0-beta.9(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/dates@7.17.8(@mantine/core@7.17.8(@mantine/hooks@7.17.8(react@19.2.6))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@mantine/hooks@7.17.8(react@19.2.6))(@tabler/icons-react@3.44.0(react@19.2.6))(clsx@2.1.1)(dayjs@1.11.21)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
mui-ethiopian-datepicker: 0.3.2(4b3af212eafdf0059f009b005d7e343d)
|
|
||||||
next-themes: 0.4.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
path: 0.12.7
|
|
||||||
pdf-lib: 1.17.1
|
|
||||||
qs: 6.15.2
|
|
||||||
react: 19.2.6
|
|
||||||
react-cookie: 8.1.2(@types/react@18.3.31)(react@19.2.6)
|
|
||||||
react-css-nocode-editor: 1.0.13(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
|
|
||||||
react-day-picker: 8.10.2(date-fns@3.6.0)(react@19.2.6)
|
|
||||||
react-dom: 19.2.6(react@19.2.6)
|
|
||||||
react-dropzone: 14.4.1(react@19.2.6)
|
|
||||||
react-hook-form: 7.77.0(react@19.2.6)
|
|
||||||
react-i18next: 15.7.4(i18next@25.10.10(typescript@5.9.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(typescript@5.9.3)
|
|
||||||
react-icons: 5.6.0(react@19.2.6)
|
|
||||||
react-image-crop: 11.0.10(react@19.2.6)
|
|
||||||
react-intersection-observer: 9.16.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
react-pdf: 10.4.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
react-pdf-html: 2.1.5(@react-pdf/renderer@4.5.1(react@19.2.6))(react@19.2.6)
|
|
||||||
react-redux: 9.3.0(@types/react@18.3.31)(react@19.2.6)(redux@5.0.1)
|
|
||||||
react-resizable-panels: 3.0.6(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
react-router-dom: 7.17.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
react-signature-canvas: 1.1.0-alpha.2(@types/prop-types@15.7.15)(@types/react@18.3.31)(prop-types@15.8.1)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
recharts: 3.8.1(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)(redux@5.0.1)
|
|
||||||
rollup-plugin-visualizer: 7.0.1(rollup@4.61.1)
|
|
||||||
socket.io-client: 4.8.3
|
|
||||||
sonner: 2.0.7(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
tailwind-merge: 3.6.0
|
|
||||||
tailwind-scrollbar-hide: 4.0.0(tailwindcss@4.3.0)
|
|
||||||
tailwindcss: 4.3.0
|
|
||||||
tailwindcss-animate: 1.0.7(tailwindcss@4.3.0)
|
|
||||||
tinymce: 7.9.3
|
|
||||||
url: 0.11.4
|
|
||||||
vaul: 1.1.2(@types/react-dom@18.3.7(@types/react@18.3.31))(@types/react@18.3.31)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
|
|
||||||
xlsx: 0.18.5
|
|
||||||
zod: 3.25.76
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
- '@emotion/is-prop-valid'
|
|
||||||
- '@mui/icons-material'
|
|
||||||
- '@mui/material'
|
|
||||||
- '@mui/x-date-pickers'
|
|
||||||
- '@types/prop-types'
|
|
||||||
- '@types/react'
|
|
||||||
- '@types/react-dom'
|
|
||||||
- bufferutil
|
|
||||||
- debug
|
|
||||||
- pdfjs-dist
|
|
||||||
- prop-types
|
|
||||||
- react-is
|
|
||||||
- react-native
|
|
||||||
- redux
|
|
||||||
- rolldown
|
|
||||||
- rollup
|
|
||||||
- supports-color
|
|
||||||
- typescript
|
|
||||||
- utf-8-validate
|
|
||||||
- vite
|
|
||||||
|
|
||||||
'@ts-morph/common@0.27.0':
|
'@ts-morph/common@0.27.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob: 3.3.3
|
fast-glob: 3.3.3
|
||||||
@@ -17161,7 +17046,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/visitor-keys': 8.60.1
|
'@typescript-eslint/visitor-keys': 8.60.1
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -17171,7 +17056,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -17190,7 +17075,7 @@ snapshots:
|
|||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/typescript-estree': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/utils': 8.60.1(eslint@8.57.1)(typescript@5.9.3)
|
'@typescript-eslint/utils': 8.60.1(eslint@8.57.1)(typescript@5.9.3)
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
ts-api-utils: 2.5.0(typescript@5.9.3)
|
ts-api-utils: 2.5.0(typescript@5.9.3)
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -17205,7 +17090,7 @@ snapshots:
|
|||||||
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
'@typescript-eslint/tsconfig-utils': 8.60.1(typescript@5.9.3)
|
||||||
'@typescript-eslint/types': 8.60.1
|
'@typescript-eslint/types': 8.60.1
|
||||||
'@typescript-eslint/visitor-keys': 8.60.1
|
'@typescript-eslint/visitor-keys': 8.60.1
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
minimatch: 10.2.5
|
minimatch: 10.2.5
|
||||||
semver: 7.8.2
|
semver: 7.8.2
|
||||||
tinyglobby: 0.2.17
|
tinyglobby: 0.2.17
|
||||||
@@ -17494,7 +17379,7 @@ snapshots:
|
|||||||
|
|
||||||
agent-base@6.0.2:
|
agent-base@6.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -17547,6 +17432,11 @@ snapshots:
|
|||||||
amqplib: 0.10.9
|
amqplib: 0.10.9
|
||||||
promise-breaker: 6.0.0
|
promise-breaker: 6.0.0
|
||||||
|
|
||||||
|
amqp-connection-manager@5.0.0(amqplib@0.10.9):
|
||||||
|
dependencies:
|
||||||
|
amqplib: 0.10.9
|
||||||
|
promise-breaker: 6.0.0
|
||||||
|
|
||||||
amqp-connection-manager@5.0.0(amqplib@2.0.1):
|
amqp-connection-manager@5.0.0(amqplib@2.0.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
amqplib: 2.0.1
|
amqplib: 2.0.1
|
||||||
@@ -18003,16 +17893,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
babel-plugin-styled-components@2.3.0(styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6))(supports-color@5.5.0):
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-annotate-as-pure': 7.29.7
|
|
||||||
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
|
|
||||||
'@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7)
|
|
||||||
picomatch: 4.0.4
|
|
||||||
styled-components: 5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
babel-polyfill@6.26.0:
|
babel-polyfill@6.26.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
babel-runtime: 6.26.0
|
babel-runtime: 6.26.0
|
||||||
@@ -18166,7 +18046,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
http-errors: 2.0.1
|
http-errors: 2.0.1
|
||||||
iconv-lite: 0.7.2
|
iconv-lite: 0.7.2
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
@@ -19163,7 +19043,7 @@ snapshots:
|
|||||||
engine.io-client@6.6.5:
|
engine.io-client@6.6.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@socket.io/component-emitter': 3.1.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
engine.io-parser: 5.2.3
|
engine.io-parser: 5.2.3
|
||||||
ws: 8.20.1
|
ws: 8.20.1
|
||||||
xmlhttprequest-ssl: 2.1.2
|
xmlhttprequest-ssl: 2.1.2
|
||||||
@@ -19183,7 +19063,7 @@ snapshots:
|
|||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
cors: 2.8.6
|
cors: 2.8.6
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
engine.io-parser: 5.2.3
|
engine.io-parser: 5.2.3
|
||||||
ws: 8.21.0
|
ws: 8.21.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -19412,7 +19292,7 @@ snapshots:
|
|||||||
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.60.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nolyfill/is-core-module': 1.0.39
|
'@nolyfill/is-core-module': 1.0.39
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
eslint: 8.57.1
|
eslint: 8.57.1
|
||||||
get-tsconfig: 4.14.0
|
get-tsconfig: 4.14.0
|
||||||
is-bun-module: 2.0.0
|
is-bun-module: 2.0.0
|
||||||
@@ -19540,7 +19420,7 @@ snapshots:
|
|||||||
ajv: 6.15.0
|
ajv: 6.15.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
cross-spawn: 7.0.6
|
cross-spawn: 7.0.6
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
doctrine: 3.0.0
|
doctrine: 3.0.0
|
||||||
escape-string-regexp: 4.0.0
|
escape-string-regexp: 4.0.0
|
||||||
eslint-scope: 7.2.2
|
eslint-scope: 7.2.2
|
||||||
@@ -19770,7 +19650,7 @@ snapshots:
|
|||||||
content-type: 1.0.5
|
content-type: 1.0.5
|
||||||
cookie: 0.7.2
|
cookie: 0.7.2
|
||||||
cookie-signature: 1.2.2
|
cookie-signature: 1.2.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
@@ -19823,7 +19703,7 @@ snapshots:
|
|||||||
|
|
||||||
extract-zip@2.0.1:
|
extract-zip@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
get-stream: 5.2.0
|
get-stream: 5.2.0
|
||||||
yauzl: 2.10.0
|
yauzl: 2.10.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
@@ -19974,7 +19854,7 @@ snapshots:
|
|||||||
|
|
||||||
finalhandler@2.1.1:
|
finalhandler@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
on-finished: 2.4.1
|
on-finished: 2.4.1
|
||||||
@@ -20220,7 +20100,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
basic-ftp: 5.3.1
|
basic-ftp: 5.3.1
|
||||||
data-uri-to-buffer: 6.0.2
|
data-uri-to-buffer: 6.0.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -20501,7 +20381,7 @@ snapshots:
|
|||||||
http-proxy-agent@7.0.2:
|
http-proxy-agent@7.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -20514,14 +20394,14 @@ snapshots:
|
|||||||
https-proxy-agent@5.0.1:
|
https-proxy-agent@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 6.0.2
|
agent-base: 6.0.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
https-proxy-agent@7.0.6:
|
https-proxy-agent@7.0.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -20946,7 +20826,7 @@ snapshots:
|
|||||||
|
|
||||||
istanbul-lib-source-maps@4.0.1:
|
istanbul-lib-source-maps@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
istanbul-lib-coverage: 3.2.2
|
istanbul-lib-coverage: 3.2.2
|
||||||
source-map: 0.6.1
|
source-map: 0.6.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -21592,7 +21472,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.6.2
|
chalk: 5.6.2
|
||||||
commander: 13.1.0
|
commander: 13.1.0
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
execa: 8.0.1
|
execa: 8.0.1
|
||||||
lilconfig: 3.1.3
|
lilconfig: 3.1.3
|
||||||
listr2: 8.3.3
|
listr2: 8.3.3
|
||||||
@@ -22389,7 +22269,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tootallnate/quickjs-emscripten': 0.23.0
|
'@tootallnate/quickjs-emscripten': 0.23.0
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
get-uri: 6.0.5
|
get-uri: 6.0.5
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
@@ -22727,7 +22607,7 @@ snapshots:
|
|||||||
proxy-agent@6.5.0:
|
proxy-agent@6.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2
|
||||||
https-proxy-agent: 7.0.6
|
https-proxy-agent: 7.0.6
|
||||||
lru-cache: 7.18.3
|
lru-cache: 7.18.3
|
||||||
@@ -22756,7 +22636,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@puppeteer/browsers': 2.13.2
|
'@puppeteer/browsers': 2.13.2
|
||||||
chromium-bidi: 14.0.0(devtools-protocol@0.0.1608973)
|
chromium-bidi: 14.0.0(devtools-protocol@0.0.1608973)
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
devtools-protocol: 0.0.1608973
|
devtools-protocol: 0.0.1608973
|
||||||
typed-query-selector: 2.12.2
|
typed-query-selector: 2.12.2
|
||||||
webdriver-bidi-protocol: 0.4.1
|
webdriver-bidi-protocol: 0.4.1
|
||||||
@@ -23009,15 +22889,6 @@ snapshots:
|
|||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
- react-is
|
- react-is
|
||||||
|
|
||||||
react-css-nocode-editor@1.0.13(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6):
|
|
||||||
dependencies:
|
|
||||||
react: 19.2.6
|
|
||||||
react-dom: 19.2.6(react@19.2.6)
|
|
||||||
styled-components: 5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
- react-is
|
|
||||||
|
|
||||||
react-day-picker@8.10.2(date-fns@3.6.0)(react@19.2.6):
|
react-day-picker@8.10.2(date-fns@3.6.0)(react@19.2.6):
|
||||||
dependencies:
|
dependencies:
|
||||||
date-fns: 3.6.0
|
date-fns: 3.6.0
|
||||||
@@ -23591,7 +23462,7 @@ snapshots:
|
|||||||
|
|
||||||
router@2.2.0:
|
router@2.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
is-promise: 4.0.0
|
is-promise: 4.0.0
|
||||||
parseurl: 1.3.3
|
parseurl: 1.3.3
|
||||||
@@ -23709,7 +23580,7 @@ snapshots:
|
|||||||
|
|
||||||
send@1.2.1:
|
send@1.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
encodeurl: 2.0.0
|
encodeurl: 2.0.0
|
||||||
escape-html: 1.0.3
|
escape-html: 1.0.3
|
||||||
etag: 1.8.1
|
etag: 1.8.1
|
||||||
@@ -23925,7 +23796,7 @@ snapshots:
|
|||||||
|
|
||||||
socket.io-adapter@2.5.8:
|
socket.io-adapter@2.5.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
ws: 8.21.0
|
ws: 8.21.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
@@ -23935,7 +23806,7 @@ snapshots:
|
|||||||
socket.io-client@4.8.3:
|
socket.io-client@4.8.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@socket.io/component-emitter': 3.1.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
engine.io-client: 6.6.5
|
engine.io-client: 6.6.5
|
||||||
socket.io-parser: 4.2.6
|
socket.io-parser: 4.2.6
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
@@ -23946,7 +23817,7 @@ snapshots:
|
|||||||
socket.io-parser@4.2.6:
|
socket.io-parser@4.2.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@socket.io/component-emitter': 3.1.2
|
'@socket.io/component-emitter': 3.1.2
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -23955,7 +23826,7 @@ snapshots:
|
|||||||
accepts: 1.3.8
|
accepts: 1.3.8
|
||||||
base64id: 2.0.0
|
base64id: 2.0.0
|
||||||
cors: 2.8.6
|
cors: 2.8.6
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
engine.io: 6.6.9
|
engine.io: 6.6.9
|
||||||
socket.io-adapter: 2.5.8
|
socket.io-adapter: 2.5.8
|
||||||
socket.io-parser: 4.2.6
|
socket.io-parser: 4.2.6
|
||||||
@@ -23967,7 +23838,7 @@ snapshots:
|
|||||||
socks-proxy-agent@8.0.5:
|
socks-proxy-agent@8.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
socks: 2.8.9
|
socks: 2.8.9
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
@@ -24245,24 +24116,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
|
|
||||||
styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6):
|
|
||||||
dependencies:
|
|
||||||
'@babel/helper-module-imports': 7.29.7(supports-color@5.5.0)
|
|
||||||
'@babel/traverse': 7.29.7(supports-color@5.5.0)
|
|
||||||
'@emotion/is-prop-valid': 1.4.0
|
|
||||||
'@emotion/stylis': 0.8.5
|
|
||||||
'@emotion/unitless': 0.7.5
|
|
||||||
babel-plugin-styled-components: 2.3.0(styled-components@5.3.11(react-dom@19.2.6(react@19.2.6))(react-is@19.2.7)(react@19.2.6))(supports-color@5.5.0)
|
|
||||||
css-to-react-native: 3.2.0
|
|
||||||
hoist-non-react-statics: 3.3.2
|
|
||||||
react: 19.2.6
|
|
||||||
react-dom: 19.2.6(react@19.2.6)
|
|
||||||
react-is: 19.2.7
|
|
||||||
shallowequal: 1.1.0
|
|
||||||
supports-color: 5.5.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- '@babel/core'
|
|
||||||
|
|
||||||
styled-jsx@5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1):
|
styled-jsx@5.1.1(babel-plugin-macros@3.1.0)(react@18.3.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
client-only: 0.0.1
|
client-only: 0.0.1
|
||||||
@@ -24288,7 +24141,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
component-emitter: 1.3.1
|
component-emitter: 1.3.1
|
||||||
cookiejar: 2.1.4
|
cookiejar: 2.1.4
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
fast-safe-stringify: 2.1.1
|
fast-safe-stringify: 2.1.1
|
||||||
form-data: 4.0.5
|
form-data: 4.0.5
|
||||||
formidable: 3.5.4
|
formidable: 3.5.4
|
||||||
@@ -24797,7 +24650,7 @@ snapshots:
|
|||||||
app-root-path: 3.1.0
|
app-root-path: 3.1.0
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
dayjs: 1.11.21
|
dayjs: 1.11.21
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
glob: 10.5.0
|
glob: 10.5.0
|
||||||
@@ -24821,7 +24674,7 @@ snapshots:
|
|||||||
app-root-path: 3.1.0
|
app-root-path: 3.1.0
|
||||||
buffer: 6.0.3
|
buffer: 6.0.3
|
||||||
dayjs: 1.11.21
|
dayjs: 1.11.21
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
dedent: 1.7.2(babel-plugin-macros@3.1.0)
|
||||||
dotenv: 16.6.1
|
dotenv: 16.6.1
|
||||||
glob: 10.5.0
|
glob: 10.5.0
|
||||||
@@ -25124,7 +24977,7 @@ snapshots:
|
|||||||
vite-node@2.1.9(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0):
|
vite-node@2.1.9(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
vite: 5.4.21(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@22.20.1)(lightningcss@1.32.0)(terser@5.48.0)
|
||||||
@@ -25142,7 +24995,7 @@ snapshots:
|
|||||||
vite-node@2.1.9(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0):
|
vite-node@2.1.9(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
es-module-lexer: 1.7.0
|
es-module-lexer: 1.7.0
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
vite: 5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)
|
vite: 5.4.21(@types/node@24.13.1)(lightningcss@1.32.0)(terser@5.48.0)
|
||||||
@@ -25189,7 +25042,7 @@ snapshots:
|
|||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
'@vitest/utils': 2.1.9
|
'@vitest/utils': 2.1.9
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
expect-type: 1.3.0
|
expect-type: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
@@ -25225,7 +25078,7 @@ snapshots:
|
|||||||
'@vitest/spy': 2.1.9
|
'@vitest/spy': 2.1.9
|
||||||
'@vitest/utils': 2.1.9
|
'@vitest/utils': 2.1.9
|
||||||
chai: 5.3.3
|
chai: 5.3.3
|
||||||
debug: 4.4.3(supports-color@8.1.1)
|
debug: 4.4.3(supports-color@5.5.0)
|
||||||
expect-type: 1.3.0
|
expect-type: 1.3.0
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
|
|||||||
Reference in New Issue
Block a user