Merge pull request #285 from Tria-plc/freight/fix/type-errors

Freight/fix/type errors
This commit is contained in:
Nathnael Wondisha
2026-06-25 10:15:33 +03:00
committed by GitHub
11 changed files with 267 additions and 345 deletions

View File

@@ -0,0 +1,33 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Contact email/phone for an external profile is sourced from IAM (the user's
* identity) and from the company record, so the duplicated `email`/`phone`
* columns on external_profiles are redundant and are dropped. Dropping `email`
* also removes its UNIQUE constraint.
*/
export class DropEmailPhoneFromExternalProfiles1820000000011
implements MigrationInterface
{
name = 'DropEmailPhoneFromExternalProfiles1820000000011';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.external_profiles DROP COLUMN IF EXISTS email;`,
);
await queryRunner.query(
`ALTER TABLE freight.external_profiles DROP COLUMN IF EXISTS phone;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Re-added as nullable (the original email was UNIQUE NOT NULL) since the
// dropped values cannot be recovered to satisfy those constraints.
await queryRunner.query(
`ALTER TABLE freight.external_profiles ADD COLUMN IF NOT EXISTS email varchar(150);`,
);
await queryRunner.query(
`ALTER TABLE freight.external_profiles ADD COLUMN IF NOT EXISTS phone varchar(20);`,
);
}
}

View File

@@ -139,10 +139,12 @@ export class CompaniesService {
}
}
const existingProfile = await this.profilesRepo.findByEmail(identity.email);
const existingProfile = await this.profilesRepo.findByUserId(
identity.userId,
);
if (existingProfile) {
throw new ConflictException(
`Profile with email ${identity.email} already exists`,
`Profile for user ${identity.userId} already exists`,
);
}
@@ -176,8 +178,6 @@ export class CompaniesService {
companyId: company.id,
firstName: identity.firstName,
lastName: identity.lastName,
email: identity.email,
phone: normalizeE164(identity.phone) ?? identity.phone,
jobTitle: dto.jobTitle ?? null,
isPrimaryContact: dto.isPrimaryContact ?? true,
activeProfileType,
@@ -251,15 +251,6 @@ export class CompaniesService {
return this.getCompanyInfoByUserId(identity.userId);
}
// A profile may exist for the same email under a different IAM id — block
// duplicates as the final create does.
const byEmail = await this.profilesRepo.findByEmail(identity.email);
if (byEmail) {
throw new ConflictException(
`Profile with email ${identity.email} already exists`,
);
}
const allowedTypes = this.getProfileTypeForCompanyType(companyType);
const chosenTypes = roles.filter((t) => allowedTypes.includes(t));
const activeProfileType =
@@ -284,8 +275,6 @@ export class CompaniesService {
companyId: company.id,
firstName: identity.firstName,
lastName: identity.lastName,
email: identity.email,
phone: normalizeE164(identity.phone) ?? identity.phone,
isPrimaryContact: true,
activeProfileType,
onboardingStep: "company",
@@ -637,10 +626,10 @@ export class CompaniesService {
async createProfile(dto: CreateExternalProfileDto): Promise<ExternalProfile> {
await this.findCompanyById(dto.companyId);
const existing = await this.profilesRepo.findByEmail(dto.email);
const existing = await this.profilesRepo.findByUserId(dto.userId);
if (existing) {
throw new ConflictException(
`Profile with email ${dto.email} already exists`,
`Profile for user ${dto.userId} already exists`,
);
}

View File

@@ -1,5 +1,4 @@
import { IsString, IsNotEmpty, IsOptional, IsEmail, MaxLength, IsBoolean, IsUUID } from 'class-validator';
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
import { IsString, IsNotEmpty, IsOptional, MaxLength, IsBoolean, IsUUID } from 'class-validator';
export class CreateExternalProfileDto {
@IsUUID()
@@ -20,16 +19,6 @@ export class CreateExternalProfileDto {
@MaxLength(100)
lastName!: string;
@IsEmail()
@IsNotEmpty()
email!: string;
@IsOptional()
@IsString()
@MaxLength(20)
@IsValidPhone()
phone?: string;
@IsOptional()
@IsString()
@MaxLength(50)

View File

@@ -10,8 +10,6 @@ export class ResponseExternalProfileDto {
companyId: string;
firstName: string;
lastName: string;
email: string;
phone?: string | null;
nationalId?: string | null;
jobTitle?: string | null;
isPrimaryContact: boolean;
@@ -34,8 +32,6 @@ export class ResponseExternalProfileDto {
this.companyId = profile.companyId;
this.firstName = profile.firstName;
this.lastName = profile.lastName;
this.email = profile.email;
this.phone = profile.phone;
this.nationalId = profile.nationalId;
this.jobTitle = profile.jobTitle;
this.isPrimaryContact = profile.isPrimaryContact;

View File

@@ -23,12 +23,6 @@ export class ExternalProfile extends BaseEntity {
@Column({ name: 'last_name', type: 'varchar', length: 100 })
lastName!: string;
@Column({ name: 'email', type: 'varchar', length: 150, unique: true })
email!: string;
@Column({ name: 'phone', type: 'varchar', length: 20, nullable: true })
phone?: string | null;
@Column({ name: 'national_id', type: 'varchar', length: 50, nullable: true })
nationalId?: string | null;

View File

@@ -23,8 +23,4 @@ export class ExternalProfileRepository extends BaseRepository<ExternalProfile> {
async findByCompanyId(companyId: string): Promise<ExternalProfile[]> {
return this.repository.find({ where: { companyId } as any });
}
async findByEmail(email: string): Promise<ExternalProfile | null> {
return this.repository.findOne({ where: { email } as any });
}
}

View File

@@ -2,7 +2,6 @@ import { AppLayout, type SidebarItem } from "@/components/AppLayout";
import { useDisclosure } from "@mantine/hooks";
import {
CalendarCheck,
Clock,
Home,
Layers,
Loader2,
@@ -112,13 +111,11 @@ function isOnboardingAllowedPath(pathname: string): boolean {
* as users who haven't completed onboarding.
*/
function OnboardingGate() {
const { company, onboardingCompleted, companyStatus } = useAuth();
const { company, onboardingCompleted } = useAuth();
const location = useLocation();
const needsOnboarding = !company || !onboardingCompleted;
const allowedHere = isOnboardingAllowedPath(location.pathname);
// Onboarding done but not yet approved by an admin → awaiting-approval state.
const awaitingApproval = !needsOnboarding && companyStatus === "pending";
// Open by default while onboarding is pending (covers the login case).
const [wizardOpen, { open: openWizard, close: closeWizard }] =
@@ -148,9 +145,7 @@ function OnboardingGate() {
return (
<>
{needsOnboarding && (
<OnboardingResumeBanner onResume={openWizard} />
)}
{needsOnboarding && <OnboardingResumeBanner onResume={openWizard} />}
{!needsOnboarding && <AccountReviewBanner />}
<Outlet />
<OnboardingWizardDialog
@@ -275,7 +270,10 @@ const App = () => {
<Route path="/tracking" element={<TrackingPage />} />
<Route path="/billing" element={<BillingPage />} />
{/* Profile was merged into Settings — keep old links working. */}
<Route path="/profile" element={<Navigate to="/settings" replace />} />
<Route
path="/profile"
element={<Navigate to="/settings" replace />}
/>
<Route path="/signature" element={<MySignaturePage />} />
<Route path="/settings" element={<SettingsPage />} />
</Route>

View File

@@ -20,7 +20,6 @@ export default function MyPortalPage() {
null,
);
const {
customer,
companyProfiles,
bookingsQuery,
dashboardQuery,
@@ -105,9 +104,7 @@ export default function MyPortalPage() {
<FreightVolumeSection
totalTonnes={dashboard?.freightVolume.totalTonnes ?? 0}
totalValue={dashboard?.freightVolume.totalValue ?? 0}
currency={
(dashboard?.freightVolume.currency ?? "ETB") as Currency
}
currency={(dashboard?.freightVolume.currency ?? "ETB") as Currency}
ytdChangePct={dashboard?.freightVolume.ytdChangePct ?? 0}
volumePoints={volumePoints}
maxVolume={maxVolume}

View File

@@ -63,8 +63,7 @@ const maskPhone = (p: string) =>
p.length > 4 ? `${p.slice(0, 7)}${"*".repeat(Math.max(0, p.length - 7))}` : p;
const onboardingSchema = z.object({
companyFirstName: z.string().min(1, "First name is required"),
companyLastName: z.string().min(1, "Last name is required"),
companyName: z.string().min(1, "Company name is required"),
companyEmail: z.string().email("Invalid email address"),
companyPhone: z
.string()
@@ -74,10 +73,7 @@ const onboardingSchema = z.object({
// Derived from the eTrade address parts (kebele/woreda/zone/region); no
// standalone input — the granular fields live in the registration section.
companyAddress: z.string().optional(),
tinNumber: z
.string()
.length(10, "TIN must be exactly 10 digits")
.regex(/^00\d{8}$/, "TIN must be 10 digits starting with 00"),
tinNumber: z.string().length(10, "TIN must be exactly 10 digits"),
vatNumber: z
.string()
.min(1, "VAT number is required")
@@ -97,12 +93,7 @@ const onboardingSchema = z.object({
kebele: z.string().min(1, "Kebele is required"),
houseNo: z.string().min(1, "House number is required"),
etradePhone: z.string().optional(),
contactPersonFirstName: z
.string()
.min(1, "Contact person first name is required"),
contactPersonLastName: z
.string()
.min(1, "Contact person last name is required"),
contactPersonName: z.string().min(1, "Contact person name is required"),
contactPersonPosition: z.string().optional(),
contactPersonEmail: z
.string()
@@ -119,8 +110,7 @@ const onboardingSchema = z.object({
.string()
.min(1, "Manager phone is required")
.refine(isValidPhone, "Enter a valid phone number"),
poaFirstName: z.string().optional(),
poaLastName: z.string().optional(),
poaName: z.string().optional(),
poaPhone: z
.string()
.optional()
@@ -134,8 +124,7 @@ type FormData = z.infer<typeof onboardingSchema>;
const stepFields: Record<CompanyStep, (keyof FormData)[]> = {
company: [
"companyFirstName",
"companyLastName",
"companyName",
"companyEmail",
"companyPhone",
"companyLocation",
@@ -157,14 +146,12 @@ const stepFields: Record<CompanyStep, (keyof FormData)[]> = {
"etradePhone",
],
personnel: [
"generalManagerFirstName",
"generalManagerLastName",
"generalManagerName",
"generalManagerEmail",
"generalManagerPhone",
],
contact: [
"contactPersonFirstName",
"contactPersonLastName",
"contactPersonName",
"contactPersonPosition",
"contactPersonEmail",
"contactPersonPhone",
@@ -175,23 +162,9 @@ const stepFields: Record<CompanyStep, (keyof FormData)[]> = {
additional: [],
};
/** Join first + last into the single name the API stores. */
function joinName(first?: string, last?: string): string {
return [first?.trim(), last?.trim()].filter(Boolean).join(" ");
}
/** Split a stored single name into first (first token) + last (the rest). */
function splitName(full?: string | null): { first: string; last: string } {
const trimmed = (full ?? "").trim();
if (!trimmed) return { first: "", last: "" };
const idx = trimmed.indexOf(" ");
if (idx === -1) return { first: trimmed, last: "" };
return { first: trimmed.slice(0, idx), last: trimmed.slice(idx + 1).trim() };
}
function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
return {
companyName: joinName(data.companyFirstName, data.companyLastName),
companyName: data.companyName,
companyEmail: data.companyEmail,
companyPhone: data.companyPhone,
companyLocation: data.companyLocation,
@@ -200,20 +173,14 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
vatNumber: data.vatNumber,
fanNumber: data.fanNumber,
attributes: {
contactPersonName: joinName(
data.contactPersonFirstName,
data.contactPersonLastName,
),
contactPersonName: data.contactPersonName,
contactPersonPosition: data.contactPersonPosition || undefined,
contactPersonEmail: data.contactPersonEmail || undefined,
contactPersonPhone: data.contactPersonPhone,
generalManagerName: joinName(
data.generalManagerFirstName,
data.generalManagerLastName,
),
generalManagerName: data.generalManagerName,
generalManagerEmail: data.generalManagerEmail,
generalManagerPhone: data.generalManagerPhone,
poaName: joinName(data.poaFirstName, data.poaLastName) || undefined,
poaName: data.poaName || undefined,
poaPhone: data.poaPhone || undefined,
poaAddress: data.poaAddress || undefined,
poaEmail: data.poaEmail || undefined,
@@ -230,7 +197,7 @@ function stepPayload(
switch (step) {
case "company":
return {
companyName: joinName(d.companyFirstName, d.companyLastName),
companyName: d.companyName,
companyEmail: d.companyEmail,
companyPhone: d.companyPhone,
companyLocation: d.companyLocation,
@@ -253,26 +220,20 @@ function stepPayload(
};
case "personnel":
return {
generalManagerName: joinName(
d.generalManagerFirstName,
d.generalManagerLastName,
),
generalManagerName: d.generalManagerName,
generalManagerEmail: d.generalManagerEmail,
generalManagerPhone: d.generalManagerPhone,
};
case "contact":
return {
contactPersonName: joinName(
d.contactPersonFirstName,
d.contactPersonLastName,
),
contactPersonName: d.contactPersonName,
contactPersonPosition: d.contactPersonPosition || undefined,
contactPersonEmail: d.contactPersonEmail || undefined,
contactPersonPhone: d.contactPersonPhone,
};
case "poa":
return {
poaName: joinName(d.poaFirstName, d.poaLastName) || undefined,
poaName: d.poaName || undefined,
poaPhone: d.poaPhone || undefined,
poaEmail: d.poaEmail || undefined,
poaLocation: d.poaLocation || undefined,
@@ -287,13 +248,8 @@ function stepPayload(
function toFormValues(p: ProfileResponse): FormData {
// The draft placeholder TIN ("D…") shouldn't show as a real value.
const tin = p.tinNumber && !p.tinNumber.startsWith("D") ? p.tinNumber : "";
const companyN = splitName(p.companyName);
const contactN = splitName(p.contactPersonName);
const gmN = splitName(p.generalManagerName);
const poaN = splitName(p.poaName);
return {
companyFirstName: companyN.first,
companyLastName: companyN.last,
companyName: p.companyName ?? "",
companyEmail: p.companyEmail ?? "",
companyPhone: p.companyPhone ?? "",
companyLocation: p.companyLocation ?? "",
@@ -313,17 +269,14 @@ function toFormValues(p: ProfileResponse): FormData {
kebele: p.kebele ?? "",
houseNo: p.houseNo ?? "",
etradePhone: p.etradePhone ?? "",
contactPersonFirstName: contactN.first,
contactPersonLastName: contactN.last,
contactPersonName: p.contactPersonName ?? "",
contactPersonPosition: p.contactPersonPosition ?? "",
contactPersonEmail: p.contactPersonEmail ?? "",
contactPersonPhone: p.contactPersonPhone ?? "",
generalManagerFirstName: gmN.first,
generalManagerLastName: gmN.last,
generalManagerName: p.generalManagerName ?? "",
generalManagerEmail: p.generalManagerEmail ?? "",
generalManagerPhone: p.generalManagerPhone ?? "",
poaFirstName: poaN.first,
poaLastName: poaN.last,
poaName: p.poaName ?? "",
poaPhone: p.poaPhone ?? "",
poaAddress: p.poaAddress ?? "",
poaEmail: p.poaEmail ?? "",
@@ -438,8 +391,7 @@ export default function CompanyProfileForm({
} = useForm<FormData>({
resolver: zodResolver(onboardingSchema),
defaultValues: {
companyFirstName: "",
companyLastName: "",
companyName: "",
companyEmail: "",
companyPhone: "",
companyLocation: "",
@@ -459,17 +411,14 @@ export default function CompanyProfileForm({
kebele: "",
houseNo: "",
etradePhone: "",
contactPersonFirstName: "",
contactPersonLastName: "",
contactPersonName: "",
contactPersonPosition: "",
contactPersonEmail: "",
contactPersonPhone: "",
generalManagerFirstName: "",
generalManagerLastName: "",
generalManagerName: "",
generalManagerEmail: "",
generalManagerPhone: "",
poaFirstName: "",
poaLastName: "",
poaName: "",
poaPhone: "",
poaAddress: "",
poaEmail: "",
@@ -510,19 +459,16 @@ export default function CompanyProfileForm({
}, [region, zone, woreda, kebele, houseNo]);
// The business owner/manager pulled from eTrade — powers "Use owner as
// manager" on the General Manager step.
// manager" on the General Manager step. Null until a TIN lookup succeeds.
const [etradeOwner, setEtradeOwner] = useState<{
name: string;
phone: string;
email?: string;
} | null>(null);
const handleETradeDataLoaded = (data: CompanyRegistrationData) => {
// Company name comes from the eTrade manager/owner name on the license.
if (data.managerName) {
const { first, last } = splitName(data.managerName);
setValue("companyFirstName", first, { shouldValidate: true });
setValue("companyLastName", last, { shouldValidate: true });
setValue("companyName", data.managerName, { shouldValidate: true });
}
setValue("licenceNumber", data.licenceNumber);
setValue("statusDescription", data.statusDescription);
@@ -554,20 +500,13 @@ export default function CompanyProfileForm({
phone: toEthiopianE164(
data.managerPhone || data.regularPhone || data.mobilePhone,
),
email: data.managerEmail || undefined,
});
};
/** Fill the General Manager from the eTrade business owner. */
const toggleOwnerAsGm = (checked: boolean) => {
setOwnerIsGm(checked);
if (!checked || !etradeOwner) return;
const { first, last } = splitName(etradeOwner.name);
setValue("generalManagerFirstName", first, { shouldValidate: true });
setValue("generalManagerLastName", last, { shouldValidate: true });
setValue("generalManagerEmail", etradeOwner.email ?? "", {
shouldValidate: true,
});
const useOwnerAsManager = () => {
if (!etradeOwner) return;
setValue("generalManagerName", etradeOwner.name);
setValue("generalManagerPhone", etradeOwner.phone ?? "", {
shouldValidate: true,
});
@@ -797,23 +736,15 @@ export default function CompanyProfileForm({
<Divider my="sm" />
<TextInput
label="Company Name"
placeholder="Global Logistics Ltd"
error={errors.companyName?.message}
{...register("companyName")}
/>
<SimpleGrid cols={2} spacing="md">
<TextInput
label={<>First Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
placeholder="Global"
error={errors.companyFirstName?.message}
{...register("companyFirstName")}
/>
<TextInput
label={<>Last Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
placeholder="Logistics Ltd"
error={errors.companyLastName?.message}
{...register("companyLastName")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label={<>Company Email <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
label="Company Email"
type="email"
placeholder="ops@company.com"
error={errors.companyEmail?.message}
@@ -827,21 +758,21 @@ export default function CompanyProfileForm({
/>
</SimpleGrid>
<TextInput
label={<>Location <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
label="Location"
placeholder="Addis Ababa, Ethiopia"
error={errors.companyLocation?.message}
{...register("companyLocation")}
/>
<SimpleGrid cols={2} spacing="md">
<TextInput
label={<>VAT Number <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
label="VAT Number"
placeholder="VAT-12345"
maxLength={10}
error={errors.vatNumber?.message}
{...register("vatNumber")}
/>
<TextInput
label={<>FAN Number (16 digits) <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
label="FAN Number (16 digits)"
placeholder="1234567890123456"
maxLength={16}
error={errors.fanNumber?.message}
@@ -944,33 +875,31 @@ export default function CompanyProfileForm({
{step === "personnel" && (
<>
<Text fw={600} size="sm" c="edr-text">
General Manager
</Text>
<Checkbox
color="edr-green"
label="Use eTrade business owner as General Manager"
checked={ownerIsGm}
disabled={!etradeOwner}
onChange={(e) => toggleOwnerAsGm(e.currentTarget.checked)}
<Group justify="space-between" align="center">
<Text fw={600} size="sm" c="edr-text">
General Manager
</Text>
{etradeOwner && (
<Button
variant="light"
color="edr-green"
size="xs"
leftSection={<UserCheck size={14} />}
onClick={useOwnerAsManager}
>
Use owner as manager
</Button>
)}
</Group>
<TextInput
label="Name"
placeholder="Abebe Bikila"
error={errors.generalManagerName?.message}
{...register("generalManagerName")}
/>
<SimpleGrid cols={2} spacing="md">
<TextInput
label={<>First Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
placeholder="Abebe"
error={errors.generalManagerFirstName?.message}
{...register("generalManagerFirstName")}
/>
<TextInput
label={<>Last Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
placeholder="Bikila"
error={errors.generalManagerLastName?.message}
{...register("generalManagerLastName")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label={<>Email <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
label="Email"
type="email"
placeholder="gm@company.com"
error={errors.generalManagerEmail?.message}
@@ -1017,25 +946,19 @@ export default function CompanyProfileForm({
</Group>
<SimpleGrid cols={2} spacing="md">
<TextInput
label={<>First Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
placeholder="Jane"
error={errors.contactPersonFirstName?.message}
{...register("contactPersonFirstName")}
label="Name"
placeholder="Jane Smith"
error={errors.contactPersonName?.message}
{...register("contactPersonName")}
/>
<TextInput
label={<>Last Name <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
placeholder="Smith"
error={errors.contactPersonLastName?.message}
{...register("contactPersonLastName")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Position (Optional)"
placeholder="Operations Lead"
error={errors.contactPersonPosition?.message}
{...register("contactPersonPosition")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Email (Optional)"
type="email"
@@ -1043,8 +966,6 @@ export default function CompanyProfileForm({
error={errors.contactPersonEmail?.message}
{...register("contactPersonEmail")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<ControlledPhoneField
control={control}
name="contactPersonPhone"

View File

@@ -33,8 +33,7 @@ import {
import { ShipmentTrackingModal } from "./tracking/ShipmentTrackingModal";
import { PayNowButton } from "./payments/PayNowButton";
import useAuth from "@/hooks/useAuth";
import { PROFILE_TYPE_LABELS } from "@/constants/profileMode";
import { NewBookingButton } from "@/components/NewBookingButton";
import {
BookingTypeBadge,
CargoModeCell,
@@ -64,7 +63,11 @@ import {
// ── Status filter options (grouped by lifecycle) ──────────────────────────────
const STATUS_FILTERS = [
{ key: "all", label: "All bookings", statuses: undefined as string | undefined },
{
key: "all",
label: "All bookings",
statuses: undefined as string | undefined,
},
{
key: "active",
label: "In progress",
@@ -80,12 +83,19 @@ const STATUS_FILTERS = [
},
{ key: "transit", label: "In transit", statuses: "PAID,IN_TRANSIT" },
{ key: "done", label: "Completed", statuses: "COMPLETED,DELIVERED" },
{ key: "closed", label: "Cancelled / rejected", statuses: "CANCELLED,REJECTED" },
{
key: "closed",
label: "Cancelled / rejected",
statuses: "CANCELLED,REJECTED",
},
] as const;
type StatusFilterKey = (typeof STATUS_FILTERS)[number]["key"];
const SELECT_DATA = STATUS_FILTERS.map((f) => ({ value: f.key, label: f.label }));
const SELECT_DATA = STATUS_FILTERS.map((f) => ({
value: f.key,
label: f.label,
}));
// ── Summary stat cards (clickable lifecycle filters) ──────────────────────────
@@ -96,42 +106,42 @@ const STAT_CARDS: Array<{
iconBg: string;
iconColor: string;
}> = [
{
key: "all",
label: "All bookings",
icon: LayoutList,
iconBg: "#ECF6F1",
iconColor: "#0A8A5F",
},
{
key: "active",
label: "In progress",
icon: Package,
iconBg: "#FDF3E0",
iconColor: "#C77F09",
},
{
key: "payment",
label: "Awaiting payment",
icon: Wallet,
iconBg: "#FEF6E6",
iconColor: "#F2A516",
},
{
key: "draft",
label: "Drafts",
icon: FileEdit,
iconBg: "#F1F4F7",
iconColor: "#475569",
},
{
key: "done",
label: "Completed",
icon: CheckCircle2,
iconBg: "#ECF6F1",
iconColor: "#0A8A5F",
},
];
{
key: "all",
label: "All bookings",
icon: LayoutList,
iconBg: "#ECF6F1",
iconColor: "#0A8A5F",
},
{
key: "active",
label: "In progress",
icon: Package,
iconBg: "#FDF3E0",
iconColor: "#C77F09",
},
{
key: "payment",
label: "Awaiting payment",
icon: Wallet,
iconBg: "#FEF6E6",
iconColor: "#F2A516",
},
{
key: "draft",
label: "Drafts",
icon: FileEdit,
iconBg: "#F1F4F7",
iconColor: "#475569",
},
{
key: "done",
label: "Completed",
icon: CheckCircle2,
iconBg: "#ECF6F1",
iconColor: "#0A8A5F",
},
];
// ── Status badge (reuses the shared portal status config) ─────────────────────
@@ -139,8 +149,12 @@ function StatusBadge({ status }: { status: string }) {
const cfg = STATUS_CONFIG[status];
const label = cfg?.badgeLabel ?? status.replace(/_/g, " ");
const bg = cfg ? `var(--mantine-color-${cfg.badgeBg}-0, #F1F4F7)` : "#F1F4F7";
const text = cfg ? `var(--mantine-color-${cfg.badgeText}-7, #475569)` : "#475569";
const dot = cfg ? `var(--mantine-color-${cfg.badgeDot}-6, #94A3B8)` : "#94A3B8";
const text = cfg
? `var(--mantine-color-${cfg.badgeText}-7, #475569)`
: "#475569";
const dot = cfg
? `var(--mantine-color-${cfg.badgeDot}-6, #94A3B8)`
: "#94A3B8";
return (
<Group
gap={6}
@@ -191,7 +205,10 @@ function PrimaryAction({
fw={700}
fz={13}
rightSection={<ArrowRight size={14} />}
style={{ backgroundColor: "var(--mantine-color-edr-ink-0)", color: "#fff" }}
style={{
backgroundColor: "var(--mantine-color-edr-ink-0)",
color: "#fff",
}}
onClick={go}
>
Continue
@@ -220,7 +237,14 @@ function PrimaryAction({
return <PayNowButton booking={booking} />;
}
return (
<Button size="xs" radius="md" variant="default" fw={600} fz={13} onClick={go}>
<Button
size="xs"
radius="md"
variant="default"
fw={600}
fz={13}
onClick={go}
>
View
</Button>
);
@@ -232,7 +256,11 @@ function ColHeader({ label }: { label: string }) {
fz={11}
fw={700}
c="edr-muted"
style={{ letterSpacing: "0.6px", textTransform: "uppercase", whiteSpace: "nowrap" }}
style={{
letterSpacing: "0.6px",
textTransform: "uppercase",
whiteSpace: "nowrap",
}}
>
{label}
</Text>
@@ -247,22 +275,19 @@ function fmtDate(iso?: string | null): string {
return Number.isNaN(d.getTime())
? ""
: d.toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric",
});
year: "numeric",
month: "short",
day: "numeric",
});
}
// ── Main component ────────────────────────────────────────────────────────────
// Lightweight count query for a single lifecycle filter (reads only `total`).
function useStatusCount(
statuses: string | undefined,
companyProfileId?: string,
): number | undefined {
function useStatusCount(statuses: string | undefined): number | undefined {
const { data } = useQuery(
api.bookings.list.queryOptions({
input: { statuses, companyProfileId, page: 1, pageSize: 1 },
input: { statuses, page: 1, pageSize: 1 },
staleTime: 30_000,
}),
);
@@ -338,21 +363,10 @@ export default function MyBookings() {
const [query, setQuery] = useState("");
const [typeFilter, setTypeFilter] = useState<string | null>(null);
const [freightFilter, setFreightFilter] = useState<string | null>(null);
const [serviceFilter, setServiceFilter] = useState<string | null>(null);
const [createdFrom, setCreatedFrom] = useState<string>("");
const [createdTo, setCreatedTo] = useState<string>("");
// Operational-service options (importer / exporter / freight forwarder) for
// the per-page filter. Empty for non-customer companies.
const { company } = useAuth();
const companyProfiles = company?.company?.companyProfiles ?? [];
const serviceOptions = companyProfiles.map((p) => ({
value: p.id,
label: `${PROFILE_TYPE_LABELS[p.type] ?? p.type} · ${p.reference}`,
}));
const [trackingBooking, setTrackingBooking] = useState<Freight.IBooking | null>(
null,
);
const [trackingBooking, setTrackingBooking] =
useState<Freight.IBooking | null>(null);
const statuses = STATUS_FILTERS.find((t) => t.key === statusFilter)?.statuses;
@@ -365,15 +379,10 @@ export default function MyBookings() {
};
const hasExtraFilters =
!!typeFilter ||
!!freightFilter ||
!!serviceFilter ||
!!createdFrom ||
!!createdTo;
!!typeFilter || !!freightFilter || !!createdFrom || !!createdTo;
const clearExtraFilters = () => {
setTypeFilter(null);
setFreightFilter(null);
setServiceFilter(null);
setCreatedFrom("");
setCreatedTo("");
resetPage();
@@ -384,7 +393,6 @@ export default function MyBookings() {
statuses,
bookingType: typeFilter ?? undefined,
freightType: freightFilter ?? undefined,
companyProfileId: serviceFilter ?? undefined,
createdFrom: createdFrom || undefined,
// include the whole selected end day
createdTo: createdTo ? `${createdTo}T23:59:59.999Z` : undefined,
@@ -395,7 +403,6 @@ export default function MyBookings() {
statuses,
typeFilter,
freightFilter,
serviceFilter,
createdFrom,
createdTo,
pagination.pageIndex,
@@ -407,33 +414,25 @@ export default function MyBookings() {
api.bookings.list.queryOptions({ input: filter }),
);
// Per-card lifecycle counts (one cheap query each, total-only). Scoped to the
// selected service so the cards match the filtered table.
const svc = serviceFilter ?? undefined;
const allCount = useStatusCount(undefined, svc);
// Per-card lifecycle counts (one cheap query each, total-only).
const allCount = useStatusCount(undefined);
const activeCount = useStatusCount(
STATUS_FILTERS.find((f) => f.key === "active")!.statuses,
svc,
);
const paymentCount = useStatusCount(
STATUS_FILTERS.find((f) => f.key === "payment")!.statuses,
svc,
);
const draftCount = useStatusCount(
STATUS_FILTERS.find((f) => f.key === "draft")!.statuses,
svc,
);
const doneCount = useStatusCount(
STATUS_FILTERS.find((f) => f.key === "done")!.statuses,
svc,
);
const transitCount = useStatusCount(
STATUS_FILTERS.find((f) => f.key === "transit")!.statuses,
svc,
);
const closedCount = useStatusCount(
STATUS_FILTERS.find((f) => f.key === "closed")!.statuses,
svc,
);
const cardCounts: Record<StatusFilterKey, number | undefined> = {
all: allCount,
@@ -461,8 +460,7 @@ export default function MyBookings() {
const pageCount = Math.max(1, Math.ceil(total / pagination.pageSize));
const dataTableStatus = isLoading ? "loading" : isError ? "error" : "success";
const showEmpty =
!isLoading && !isError && rows.length === 0;
const showEmpty = !isLoading && !isError && rows.length === 0;
const columns: ColumnDef<Freight.IBooking>[] = [
{
@@ -472,7 +470,8 @@ export default function MyBookings() {
header: () => <ColHeader label="Booking" />,
cell: ({ row }) => {
const b = row.original;
const cargoLabel = b.freightType === "BULK" ? "Bulk cargo" : "Container";
const cargoLabel =
b.freightType === "BULK" ? "Bulk cargo" : "Container";
return (
<Group gap={12} wrap="nowrap" align="center">
<Box
@@ -487,7 +486,11 @@ export default function MyBookings() {
justifyContent: "center",
}}
>
<Package size={18} color="var(--mantine-color-edr-green-7)" strokeWidth={2} />
<Package
size={18}
color="var(--mantine-color-edr-green-7)"
strokeWidth={2}
/>
</Box>
<Box style={{ minWidth: 0 }}>
<Text fz={14} fw={700} c="edr-text" truncate>
@@ -593,7 +596,12 @@ export default function MyBookings() {
const booking = row.original;
const trackable = TRACKABLE_STATUSES.has(booking.status);
return (
<Group justify="flex-end" gap={8} wrap="nowrap" onClick={(e) => e.stopPropagation()}>
<Group
justify="flex-end"
gap={8}
wrap="nowrap"
onClick={(e) => e.stopPropagation()}
>
{trackable && (
<Button
size="xs"
@@ -611,7 +619,12 @@ export default function MyBookings() {
<PrimaryAction booking={booking} onNavigate={navigate} />
<Menu position="bottom-end" withinPortal shadow="md" radius="md">
<Menu.Target>
<ActionIcon variant="transparent" size={30} radius="md" aria-label="More options">
<ActionIcon
variant="transparent"
size={30}
radius="md"
aria-label="More options"
>
<MoreVertical size={16} color="#9AA8B5" />
</ActionIcon>
</Menu.Target>
@@ -642,7 +655,12 @@ export default function MyBookings() {
<Group justify="space-between" align="flex-end" wrap="wrap" gap="md">
<Box>
<Group gap={10} align="center">
<Title order={1} fw={800} fz={26} style={{ letterSpacing: "-0.01em" }}>
<Title
order={1}
fw={800}
fz={26}
style={{ letterSpacing: "-0.01em" }}
>
Bookings
</Title>
</Group>
@@ -674,7 +692,9 @@ export default function MyBookings() {
px={20}
py={14}
wrap="wrap"
style={{ borderBottom: "1px solid var(--mantine-color-edr-border-0)" }}
style={{
borderBottom: "1px solid var(--mantine-color-edr-border-0)",
}}
>
<Group gap={10} wrap="wrap" style={{ flex: 1, minWidth: 260 }}>
<TextInput
@@ -700,7 +720,9 @@ export default function MyBookings() {
<Select
data={SELECT_DATA}
value={statusFilter}
onChange={(value) => selectFilter((value as StatusFilterKey) ?? "all")}
onChange={(value) =>
selectFilter((value as StatusFilterKey) ?? "all")
}
allowDeselect={false}
radius="md"
checkIconPosition="right"
@@ -742,22 +764,6 @@ export default function MyBookings() {
style={{ width: 150 }}
aria-label="Filter by cargo type"
/>
{serviceOptions.length > 1 && (
<Select
placeholder="All services"
data={serviceOptions}
value={serviceFilter}
onChange={(v) => {
setServiceFilter(v);
resetPage();
}}
clearable
radius="md"
comboboxProps={{ withinPortal: true }}
style={{ width: 200 }}
aria-label="Filter by service"
/>
)}
<TextInput
type="date"
value={createdFrom}
@@ -802,11 +808,19 @@ export default function MyBookings() {
{showEmpty ? (
<Stack align="center" gap={4} px="lg" py={64} ta="center">
<ThemeIcon size={56} radius="lg" color="edr-green" variant="light" mb="xs">
<ThemeIcon
size={56}
radius="lg"
color="edr-green"
variant="light"
mb="xs"
>
<Package size={28} />
</ThemeIcon>
<Text size="sm" fw={600} c="edr-text">
{query ? "No bookings match your search" : "No bookings here yet"}
{query
? "No bookings match your search"
: "No bookings here yet"}
</Text>
<Text size="xs" c="edr-muted" maw={320}>
{query
@@ -814,7 +828,11 @@ export default function MyBookings() {
: "Create your first booking to get started."}
</Text>
{!query && (
<NewBookingButton label="Create first booking" size="sm" mt="md" />
<NewBookingButton
label="Create first booking"
size="sm"
mt="md"
/>
)}
</Stack>
) : (
@@ -822,7 +840,9 @@ export default function MyBookings() {
columns={columns}
data={rows}
status={dataTableStatus}
onRowClick={(row) => navigate(`/bookings/${(row as Freight.IBooking).id}`)}
onRowClick={(row) =>
navigate(`/bookings/${(row as Freight.IBooking).id}`)
}
pagination={{
pageIndex: pagination.pageIndex,
pageSize: pagination.pageSize,
@@ -848,7 +868,8 @@ export default function MyBookings() {
bookingId={trackingBooking?.id ?? ""}
bookingReference={trackingBooking?.reference ?? ""}
originLabel={
trackingBooking?.originYard?.label ?? trackingBooking?.originYard?.code
trackingBooking?.originYard?.label ??
trackingBooking?.originYard?.code
}
destinationLabel={
trackingBooking?.destinationYard?.label ??

46
pnpm-lock.yaml generated
View File

@@ -86,10 +86,10 @@ importers:
version: 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)))
'@tria-plc/api-common':
specifier: file:../../local-packages/tria-plc-api-common-1.4.3.tgz
version: file:local-packages/tria-plc-api-common-1.4.3.tgz(28ab85b15f2c569b3d04dfa1367528a6)
version: file:local-packages/tria-plc-api-common-1.4.3.tgz(7eb88ce5307a74a35f2916434d2a0c8f)
'@tria-plc/iamapi-common':
specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.3.tgz
version: file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(578386f46cf99fd4720e3e99f196f69e)
specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.4.tgz
version: file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(578386f46cf99fd4720e3e99f196f69e)
amqp-connection-manager:
specifier: ^5.0.0
version: 5.0.0(amqplib@2.0.1)
@@ -479,10 +479,10 @@ importers:
version: 8.1.6
'@tria-plc/api-common':
specifier: file:../../local-packages/tria-plc-api-common-1.4.3.tgz
version: file:local-packages/tria-plc-api-common-1.4.3.tgz(d81a2b6a79840fd7ce8c5d0cfc968145)
version: file:local-packages/tria-plc-api-common-1.4.3.tgz(e6b80acddd4bb7fc40e438635b24d1bc)
'@tria-plc/iamapi-common':
specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.3.tgz
version: file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(c97ba831ddde82920910406ab5262991)
specifier: file:../../local-packages/tria-plc-iamapi-common-0.7.4.tgz
version: file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(c97ba831ddde82920910406ab5262991)
'@types/bcrypt':
specifier: ^6.0.0
version: 6.0.0
@@ -4065,9 +4065,9 @@ packages:
rxjs: ^7.8.0
typeorm: ^0.3.0
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.3.tgz':
resolution: {integrity: sha512-poMG3sm+HmnfNbWNqMDZJMf3pXdc3HHA+o/ay6CA4E6ehLKkO7Np77C6xrCYuGxyDgqqKdmWiFLqKKddkv5rig==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.3.tgz}
version: 0.7.3
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.4.tgz':
resolution: {integrity: sha512-6Ot921laEp3rZZBXDFX+gL7nPKEyHIRJaHSIP1i+seG20+PCCGA/QHDglcJXSgF5ccnpxBdlxmI/BZbYu7LV/A==, tarball: file:local-packages/tria-plc-iamapi-common-0.7.4.tgz}
version: 0.7.4
engines: {node: '>=20'}
peerDependencies:
'@nestjs/axios': ^4.0.0
@@ -5211,12 +5211,6 @@ packages:
resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==}
engines: {node: '>=10.0.0'}
batch@0.6.1:
resolution: {integrity: sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==}
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
bcrypt@6.0.0:
resolution: {integrity: sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==}
engines: {node: '>= 18'}
@@ -15197,7 +15191,7 @@ snapshots:
'@tootallnate/quickjs-emscripten@0.23.0': {}
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(28ab85b15f2c569b3d04dfa1367528a6)':
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(7eb88ce5307a74a35f2916434d2a0c8f)':
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/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -15208,7 +15202,7 @@ snapshots:
'@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/throttler': 6.5.0(@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)
'@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)))
'@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(578386f46cf99fd4720e3e99f196f69e)
'@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(578386f46cf99fd4720e3e99f196f69e)
argon2: 0.43.1
axios: 1.17.0
change-case: 5.4.4
@@ -15241,7 +15235,7 @@ snapshots:
- debug
- supports-color
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(d81a2b6a79840fd7ce8c5d0cfc968145)':
'@tria-plc/api-common@file:local-packages/tria-plc-api-common-1.4.3.tgz(e6b80acddd4bb7fc40e438635b24d1bc)':
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/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -15252,7 +15246,7 @@ snapshots:
'@nestjs/swagger': 7.4.2(@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/throttler': 6.5.0(@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)
'@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)))
'@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(c97ba831ddde82920910406ab5262991)
'@tria-plc/iamapi-common': file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(c97ba831ddde82920910406ab5262991)
argon2: 0.43.1
axios: 1.17.0
change-case: 5.4.4
@@ -15285,7 +15279,7 @@ snapshots:
- debug
- supports-color
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(578386f46cf99fd4720e3e99f196f69e)':
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(578386f46cf99fd4720e3e99f196f69e)':
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/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -15296,7 +15290,7 @@ snapshots:
'@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/throttler': 6.5.0(@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)
'@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)))
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(28ab85b15f2c569b3d04dfa1367528a6)
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(7eb88ce5307a74a35f2916434d2a0c8f)
api-common: 1.2.2
argon2: 0.43.1
axios: 1.17.0
@@ -15320,7 +15314,7 @@ snapshots:
- '@faker-js/faker'
- supports-color
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.3.tgz(c97ba831ddde82920910406ab5262991)':
'@tria-plc/iamapi-common@file:local-packages/tria-plc-iamapi-common-0.7.4.tgz(c97ba831ddde82920910406ab5262991)':
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/common': 11.1.24(class-transformer@0.5.1)(class-validator@0.14.4)(reflect-metadata@0.2.2)(rxjs@7.8.2)
@@ -15331,7 +15325,7 @@ snapshots:
'@nestjs/swagger': 7.4.2(@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/throttler': 6.5.0(@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)
'@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)))
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(d81a2b6a79840fd7ce8c5d0cfc968145)
'@tria-plc/api-common': file:local-packages/tria-plc-api-common-1.4.3.tgz(e6b80acddd4bb7fc40e438635b24d1bc)
api-common: 1.2.2
argon2: 0.43.1
axios: 1.17.0
@@ -16718,12 +16712,6 @@ snapshots:
basic-ftp@5.3.1: {}
batch@0.6.1: {}
bcrypt-pbkdf@1.0.2:
dependencies:
tweetnacl: 0.14.5
bcrypt@6.0.0:
dependencies:
node-addon-api: 8.8.0