mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 15:25:45 +00:00
feat(companies): add Transit Agent service linked to transit-agent roster; forwarder/agent onboarding step, assigned-bookings tab, booking assignment + notify, drop agent validity window
This commit is contained in:
@@ -166,14 +166,14 @@ export function TransitAssigneePanel({
|
||||
) : null}
|
||||
<Select
|
||||
label="Transit officer"
|
||||
description="Active, currently-valid transit agents only — configure the roster in Transit Agents settings"
|
||||
description="Active transit agents only — configure the roster in Transit Agents settings"
|
||||
placeholder={loadingAgents ? "Loading…" : "Select transit officer"}
|
||||
data={agentOptions}
|
||||
value={transitAgentId}
|
||||
onChange={setTransitAgentId}
|
||||
searchable
|
||||
disabled={readOnly || loadingAgents}
|
||||
nothingFoundMessage="No active, valid transit agents — add one in Transit Agents settings"
|
||||
nothingFoundMessage="No active transit agents — add one in Transit Agents settings"
|
||||
/>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
{changing ? (
|
||||
|
||||
@@ -56,6 +56,7 @@ const PROFILE_TYPE_COLOR: Record<ProfileType, string> = {
|
||||
freight_forwarder: "blue",
|
||||
dj_freight_forwarder: "indigo",
|
||||
transporter: "grape",
|
||||
transit_agent: "lime",
|
||||
};
|
||||
|
||||
export function CompanyStatusBadge({ status }: { status: CompanyStatus }) {
|
||||
|
||||
@@ -80,24 +80,12 @@ export const formatCell = (
|
||||
);
|
||||
}
|
||||
|
||||
if (format === "validityBadge") {
|
||||
const status = String(value);
|
||||
const label =
|
||||
status === "VALID"
|
||||
? "Valid"
|
||||
: status === "EXPIRED"
|
||||
? "Expired"
|
||||
: "Not started";
|
||||
const color =
|
||||
status === "VALID"
|
||||
? "edr-green"
|
||||
: status === "EXPIRED"
|
||||
? "red"
|
||||
: "yellow";
|
||||
if (format === "country") {
|
||||
const code = String(value ?? "");
|
||||
return (
|
||||
<Badge color={color} variant="filled" size="sm" radius="md">
|
||||
{label}
|
||||
</Badge>
|
||||
<span>
|
||||
{code === "ET" ? "Ethiopia" : code === "DJ" ? "Djibouti" : code}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -954,6 +954,22 @@ export default function CustomerDetailPage() {
|
||||
<InfoField label="Email" value={company.email} />
|
||||
<InfoField label="Phone" value={company.phone} />
|
||||
<InfoField label="Website" value={company.website} />
|
||||
{/* Which roster entry the company is — shown only for
|
||||
a transit agent or forwarder, since nobody else
|
||||
has one. Unlinked = onboarded before the link. */}
|
||||
{company.companyProfiles?.some(
|
||||
(p) =>
|
||||
p.type === "freight_forwarder" ||
|
||||
p.type === "transit_agent",
|
||||
) && (
|
||||
<InfoField
|
||||
label="Transit agent"
|
||||
value={
|
||||
company.transitAgent?.name ??
|
||||
"Not linked to a transit agent"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<InfoField
|
||||
label="Submitted on"
|
||||
value={formatDate(company.createdAt)}
|
||||
|
||||
@@ -123,6 +123,7 @@ const CUSTOMER_FILTER_DEFS: FilterDef[] = [
|
||||
"freight_forwarder",
|
||||
"dj_freight_forwarder",
|
||||
"transporter",
|
||||
"transit_agent",
|
||||
] as const
|
||||
).map((value) => ({ value, label: humanize(value) })),
|
||||
},
|
||||
|
||||
@@ -613,7 +613,10 @@ const RuleEngineResourcePage = () => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{config.slug === "transit-agents" ? (
|
||||
{/* No invite for an Ethiopian agent: it is a freight forwarder
|
||||
and registers itself on the portal — the API refuses it too. */}
|
||||
{config.slug === "transit-agents" &&
|
||||
(row.original as unknown as TransitAgent).country !== "ET" ? (
|
||||
<TransitAgentAccountAction
|
||||
agent={row.original as unknown as TransitAgent}
|
||||
disabled={!canUpdateControls}
|
||||
|
||||
@@ -10,8 +10,8 @@ export type ColumnFormat =
|
||||
| "boolean"
|
||||
| "activeBadge"
|
||||
| "rateStatus"
|
||||
| "validityBadge"
|
||||
| "accountBadge"
|
||||
| "country"
|
||||
| "date"
|
||||
| "number"
|
||||
| "currency"
|
||||
@@ -667,21 +667,14 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
label: "Transit Agents",
|
||||
category: "configuration",
|
||||
subtitle:
|
||||
"Djibouti transit officers GL Djibouti may assign to a shipment — each carries a validity window",
|
||||
"Transit officers GL Djibouti may assign to a shipment, and the Ethiopian roster a freight forwarder registers itself against",
|
||||
searchPlaceholder: "Search transit agents by name...",
|
||||
cardTitleKey: "name",
|
||||
columns: [
|
||||
{ id: "name", header: "Name", accessorKey: "name" },
|
||||
{ id: "country", header: "Country", accessorKey: "country", format: "country" },
|
||||
{ id: "email", header: "Email", accessorKey: "email" },
|
||||
{ id: "phoneNumber", header: "Phone", accessorKey: "phoneNumber" },
|
||||
{ id: "validFrom", header: "Valid from", accessorKey: "validFrom", format: "date" },
|
||||
{ id: "validTo", header: "Valid to", accessorKey: "validTo", format: "date" },
|
||||
{
|
||||
id: "validityStatus",
|
||||
header: "Validity",
|
||||
accessorKey: "validityStatus",
|
||||
format: "validityBadge",
|
||||
},
|
||||
{
|
||||
id: "hasAccount",
|
||||
header: "Portal account",
|
||||
@@ -692,19 +685,27 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
],
|
||||
formFields: [
|
||||
{ name: "name", label: "Name", type: "text", required: true },
|
||||
{ name: "validFrom", label: "Valid from", type: "date", required: true },
|
||||
{
|
||||
name: "validTo",
|
||||
label: "Valid to",
|
||||
type: "date",
|
||||
name: "country",
|
||||
label: "Country",
|
||||
type: "select",
|
||||
required: true,
|
||||
description: "Expired or not-yet-started agents can't be assigned — extend the dates or add a new one",
|
||||
options: [
|
||||
{ label: "Djibouti", value: "DJ" },
|
||||
{ label: "Ethiopia", value: "ET" },
|
||||
],
|
||||
description:
|
||||
"Ethiopian agents are also the list a customer picks itself from when registering as a freight forwarder — the two are the same business. They carry no email, phone or portal account here: the forwarder registers with its own on the portal.",
|
||||
},
|
||||
// Hidden for an Ethiopian agent: it is a freight forwarder and signs up on
|
||||
// the portal with its own email and phone. An account minted here would
|
||||
// claim that email first and its own registration would then be refused.
|
||||
{
|
||||
name: "email",
|
||||
label: "Email",
|
||||
type: "email",
|
||||
optional: true,
|
||||
hideWhen: { field: "country", equals: ["ET"] },
|
||||
description:
|
||||
"Filling this on create makes the portal account and emails the activation link. On an existing agent, use the Invite button instead — editing here only corrects the address.",
|
||||
},
|
||||
@@ -713,9 +714,10 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
label: "Phone number",
|
||||
type: "phone",
|
||||
optional: true,
|
||||
description: "Ethiopian and Djiboutian mobiles also receive the link by SMS",
|
||||
hideWhen: { field: "country", equals: ["ET"] },
|
||||
description: "Djiboutian mobiles also receive the link by SMS",
|
||||
},
|
||||
{ name: "isActive", label: "Active", type: "boolean", description: "Off suspends the officer regardless of the validity window" },
|
||||
{ name: "isActive", label: "Active", type: "boolean", description: "Off suspends the officer — it disappears from the assignment and forwarder lists" },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -5,8 +5,7 @@ import type { ResetChannel } from "../types/shippingLineCompany";
|
||||
export interface TransitAgent {
|
||||
id: string;
|
||||
name: string;
|
||||
validFrom: string;
|
||||
validTo: string;
|
||||
country: "ET" | "DJ";
|
||||
isActive: boolean;
|
||||
/** Null on every agent that exists only as a GL-assignable roster entry. */
|
||||
email?: string | null;
|
||||
@@ -29,7 +28,7 @@ export interface ActivationSendResult {
|
||||
}
|
||||
|
||||
export const transitAgentsService = {
|
||||
/** Active + currently inside its validity window — the assignment dropdown. */
|
||||
/** Every active agent — the assignment dropdown. */
|
||||
async listAssignable() {
|
||||
const response = await apiClient.get<TransitAgent[]>(
|
||||
URL_CONSTANTS.RULE_ENGINE.TRANSIT_AGENTS_ASSIGNABLE,
|
||||
|
||||
@@ -32,7 +32,8 @@ export type ProfileType =
|
||||
| "exporter"
|
||||
| "freight_forwarder"
|
||||
| "dj_freight_forwarder"
|
||||
| "transporter";
|
||||
| "transporter"
|
||||
| "transit_agent";
|
||||
|
||||
/** Mirrors backend `ProfileStatus`. */
|
||||
export type ProfileStatus =
|
||||
@@ -280,6 +281,13 @@ export interface Company {
|
||||
* them against the investment licence on the Documents tab.
|
||||
*/
|
||||
investorLicence?: boolean;
|
||||
/**
|
||||
* The transit-agent roster entry a freight forwarder registered itself as —
|
||||
* the two are one business. Null for importers/exporters and for forwarders
|
||||
* linked before the field existed.
|
||||
*/
|
||||
transitAgentId?: string | null;
|
||||
transitAgent?: { id: string; name: string } | null;
|
||||
address?: string | null;
|
||||
phone?: string | null;
|
||||
email?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user