diff --git a/apps/edr-freight-api/src/migrations/1820000000011-DropEmailPhoneFromExternalProfiles.ts b/apps/edr-freight-api/src/migrations/1820000000011-DropEmailPhoneFromExternalProfiles.ts new file mode 100644 index 000000000..757c20720 --- /dev/null +++ b/apps/edr-freight-api/src/migrations/1820000000011-DropEmailPhoneFromExternalProfiles.ts @@ -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 { + 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 { + // 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);`, + ); + } +} diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index 7858945e2..a838495d5 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -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 { 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`, ); } diff --git a/apps/edr-freight-api/src/modules/companies/dto/create-external-profile.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/create-external-profile.dto.ts index 7a9b94c44..ff0f94495 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/create-external-profile.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/create-external-profile.dto.ts @@ -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) diff --git a/apps/edr-freight-api/src/modules/companies/dto/response-external-profile.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/response-external-profile.dto.ts index 7e17bcc60..256641074 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/response-external-profile.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/response-external-profile.dto.ts @@ -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; diff --git a/apps/edr-freight-api/src/modules/companies/entities/external-profile.entity.ts b/apps/edr-freight-api/src/modules/companies/entities/external-profile.entity.ts index 3b1554cc9..93e499b5e 100644 --- a/apps/edr-freight-api/src/modules/companies/entities/external-profile.entity.ts +++ b/apps/edr-freight-api/src/modules/companies/entities/external-profile.entity.ts @@ -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; diff --git a/apps/edr-freight-api/src/modules/companies/external-profile.repository.ts b/apps/edr-freight-api/src/modules/companies/external-profile.repository.ts index 581dfd72b..70c05abd7 100644 --- a/apps/edr-freight-api/src/modules/companies/external-profile.repository.ts +++ b/apps/edr-freight-api/src/modules/companies/external-profile.repository.ts @@ -23,8 +23,4 @@ export class ExternalProfileRepository extends BaseRepository { async findByCompanyId(companyId: string): Promise { return this.repository.find({ where: { companyId } as any }); } - - async findByEmail(email: string): Promise { - return this.repository.findOne({ where: { email } as any }); - } } diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx index ae795a892..1403ac234 100644 --- a/apps/edr-freight-web/portal/src/App.tsx +++ b/apps/edr-freight-web/portal/src/App.tsx @@ -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 && ( - - )} + {needsOnboarding && } {!needsOnboarding && } { } /> } /> {/* Profile was merged into Settings — keep old links working. */} - } /> + } + /> } /> } /> diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx index a8e37d44f..23cc908a6 100644 --- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx @@ -20,7 +20,6 @@ export default function MyPortalPage() { null, ); const { - customer, companyProfiles, bookingsQuery, dashboardQuery, @@ -105,9 +104,7 @@ export default function MyPortalPage() { 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; const stepFields: Record = { company: [ - "companyFirstName", - "companyLastName", + "companyName", "companyEmail", "companyPhone", "companyLocation", @@ -157,14 +146,12 @@ const stepFields: Record = { "etradePhone", ], personnel: [ - "generalManagerFirstName", - "generalManagerLastName", + "generalManagerName", "generalManagerEmail", "generalManagerPhone", ], contact: [ - "contactPersonFirstName", - "contactPersonLastName", + "contactPersonName", "contactPersonPosition", "contactPersonEmail", "contactPersonPhone", @@ -175,23 +162,9 @@ const stepFields: Record = { 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({ 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({ + First Name *} - placeholder="Global" - error={errors.companyFirstName?.message} - {...register("companyFirstName")} - /> - Last Name *} - placeholder="Logistics Ltd" - error={errors.companyLastName?.message} - {...register("companyLastName")} - /> - - - Company Email *} + label="Company Email" type="email" placeholder="ops@company.com" error={errors.companyEmail?.message} @@ -827,21 +758,21 @@ export default function CompanyProfileForm({ /> Location *} + label="Location" placeholder="Addis Ababa, Ethiopia" error={errors.companyLocation?.message} {...register("companyLocation")} /> VAT Number *} + label="VAT Number" placeholder="VAT-12345" maxLength={10} error={errors.vatNumber?.message} {...register("vatNumber")} /> FAN Number (16 digits) *} + label="FAN Number (16 digits)" placeholder="1234567890123456" maxLength={16} error={errors.fanNumber?.message} @@ -944,33 +875,31 @@ export default function CompanyProfileForm({ {step === "personnel" && ( <> - - General Manager - - toggleOwnerAsGm(e.currentTarget.checked)} + + + General Manager + + {etradeOwner && ( + + )} + + First Name *} - placeholder="Abebe" - error={errors.generalManagerFirstName?.message} - {...register("generalManagerFirstName")} - /> - Last Name *} - placeholder="Bikila" - error={errors.generalManagerLastName?.message} - {...register("generalManagerLastName")} - /> - - - Email *} + label="Email" type="email" placeholder="gm@company.com" error={errors.generalManagerEmail?.message} @@ -1017,25 +946,19 @@ export default function CompanyProfileForm({ First Name *} - placeholder="Jane" - error={errors.contactPersonFirstName?.message} - {...register("contactPersonFirstName")} + label="Name" + placeholder="Jane Smith" + error={errors.contactPersonName?.message} + {...register("contactPersonName")} /> - Last Name *} - placeholder="Smith" - error={errors.contactPersonLastName?.message} - {...register("contactPersonLastName")} - /> - - + + - - ({ 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 ( } - 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 ; } return ( - ); @@ -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} @@ -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(null); const [freightFilter, setFreightFilter] = useState(null); - const [serviceFilter, setServiceFilter] = useState(null); const [createdFrom, setCreatedFrom] = useState(""); const [createdTo, setCreatedTo] = useState(""); - - // 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( - null, - ); + const [trackingBooking, setTrackingBooking] = + useState(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 = { 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[] = [ { @@ -472,7 +470,8 @@ export default function MyBookings() { header: () => , cell: ({ row }) => { const b = row.original; - const cargoLabel = b.freightType === "BULK" ? "Bulk cargo" : "Container"; + const cargoLabel = + b.freightType === "BULK" ? "Bulk cargo" : "Container"; return ( - + @@ -593,7 +596,12 @@ export default function MyBookings() { const booking = row.original; const trackable = TRACKABLE_STATUSES.has(booking.status); return ( - e.stopPropagation()}> + e.stopPropagation()} + > {trackable && (