mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
merge conflict
This commit is contained in:
@@ -51,8 +51,7 @@ import { NO_ACCESS_PATH, resolveLandingPath } from "./lib/landing";
|
||||
import NoAccessPage from "./pages/NoAccessPage";
|
||||
import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage";
|
||||
import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage";
|
||||
import StampSettings from "./record-management/components/Settings/uploadTeeterandSingature";
|
||||
import InvoiceStampSettingsPage from "./pages/settings/InvoiceStampSettingsPage";
|
||||
import CompanyStampSettingsPage from "./pages/settings/CompanyStampSettingsPage";
|
||||
import ContractTemplatesPage from "./pages/contract_templates/ContractTemplatesPage";
|
||||
import PortalContentPage from "./pages/portal_content/PortalContentPage";
|
||||
import ContractTemplateEditorPage from "./pages/contract_templates/ContractTemplateEditorPage";
|
||||
@@ -786,23 +785,27 @@ const App = () => {
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
{/*
|
||||
The ONE company stamp, for every generated document. The per-officer
|
||||
teeter (ማህተም) that used to sit beside it at /dashboard/stamp-settings
|
||||
now lives at /user-management/teeter-and-signature — it is a different
|
||||
thing (an individual's approval stamp), and pairing the two here was
|
||||
the duplication.
|
||||
*/}
|
||||
<Route
|
||||
path="stamp-settings"
|
||||
element={
|
||||
<RequirePermission permission={FREIGHT_PERMS.settings.stamp.view}>
|
||||
<StampSettings />
|
||||
<RequirePermission
|
||||
permission={FREIGHT_PERMS.settings.stamp.view}
|
||||
>
|
||||
<CompanyStampSettingsPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
{/* Old URL kept alive so existing links/bookmarks do not 404. */}
|
||||
<Route
|
||||
path="invoice-stamp-settings"
|
||||
element={
|
||||
<RequirePermission
|
||||
permission={FREIGHT_PERMS.settings.invoiceStamp.view}
|
||||
>
|
||||
<InvoiceStampSettingsPage />
|
||||
</RequirePermission>
|
||||
}
|
||||
element={<Navigate to="/dashboard/stamp-settings" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="contract-templates"
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Badge, Card, Divider, Group, Stack, Text } from "@mantine/core";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface PersonField {
|
||||
label: string;
|
||||
value?: string | null;
|
||||
}
|
||||
|
||||
export interface PersonCardProps {
|
||||
/** OWNER / POA / CONTACT PERSON — the person's role, not their name. */
|
||||
title: string;
|
||||
icon?: ReactNode;
|
||||
/**
|
||||
* Identity state. `undefined` = this person has no identity check at all
|
||||
* (contact person), so no badge is rendered rather than a misleading "not
|
||||
* verified" one.
|
||||
*/
|
||||
verified?: boolean;
|
||||
/** Extra pills after the verification badge (e.g. "Verifies for this company"). */
|
||||
badges?: ReactNode;
|
||||
/** Rendered between the header and the fields — alerts, match warnings. */
|
||||
notice?: ReactNode;
|
||||
fields: PersonField[];
|
||||
/** Shown when the API returned nothing for every field. */
|
||||
emptyMessage: string;
|
||||
/** Attachments or anything else that belongs to this person. */
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* One person in the customer's people column: owner, power of attorney, contact
|
||||
* person. Empty fields are dropped rather than rendered as "—", so a field the
|
||||
* API stops sending simply disappears instead of leaving a dead row behind.
|
||||
*/
|
||||
export function PersonCard({
|
||||
title,
|
||||
icon,
|
||||
verified,
|
||||
badges,
|
||||
notice,
|
||||
fields,
|
||||
emptyMessage,
|
||||
children,
|
||||
}: PersonCardProps) {
|
||||
const filled = fields.filter(
|
||||
(f) => f.value != null && String(f.value).trim(),
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Stack gap="sm">
|
||||
<Group gap={8} wrap="wrap">
|
||||
{icon}
|
||||
<Text
|
||||
size="xs"
|
||||
fw={700}
|
||||
c="edr-muted"
|
||||
tt="uppercase"
|
||||
style={{ letterSpacing: "0.06em" }}
|
||||
>
|
||||
{title}
|
||||
</Text>
|
||||
{verified !== undefined &&
|
||||
(verified ? (
|
||||
<Badge size="xs" color="edr-green" variant="light">
|
||||
Fayda verified
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size="xs" color="gray" variant="light">
|
||||
Not verified
|
||||
</Badge>
|
||||
))}
|
||||
{badges}
|
||||
</Group>
|
||||
|
||||
{notice}
|
||||
|
||||
{filled.length > 0 ? (
|
||||
<Stack gap="xs">
|
||||
{filled.map((f) => (
|
||||
<Stack key={f.label} gap={0}>
|
||||
<Text size="xs" c="edr-muted">
|
||||
{f.label}
|
||||
</Text>
|
||||
<Text
|
||||
size="sm"
|
||||
c="edr-text"
|
||||
style={{ wordBreak: "break-word" }}
|
||||
>
|
||||
{f.value}
|
||||
</Text>
|
||||
</Stack>
|
||||
))}
|
||||
</Stack>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
{emptyMessage}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{children && (
|
||||
<>
|
||||
<Divider />
|
||||
{children}
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default PersonCard;
|
||||
@@ -24,4 +24,9 @@ export {
|
||||
type ResetPasswordActionProps,
|
||||
} from "./ResetPasswordAction";
|
||||
export { formatBytes, formatDate, formatMoney, humanize } from "./format";
|
||||
export {
|
||||
PersonCard,
|
||||
type PersonCardProps,
|
||||
type PersonField,
|
||||
} from "./PersonCard";
|
||||
export { TableCard, type TableCardProps } from "./TableCard";
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
PackageOpen,
|
||||
Paperclip,
|
||||
Receipt,
|
||||
Stamp,
|
||||
ScrollText,
|
||||
Send,
|
||||
Settings,
|
||||
@@ -486,17 +487,14 @@ export const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[]
|
||||
permission: FREIGHT_PERMS.settings.dropdown.view,
|
||||
},
|
||||
{
|
||||
label: "Stamp settings",
|
||||
// One entry, one stamp. The former "Stamp settings" entry here pointed
|
||||
// at the per-officer teeter (ማህተም), not a company seal — it moved to
|
||||
// /user-management/teeter-and-signature.
|
||||
label: "Company stamp",
|
||||
href: "/dashboard/stamp-settings",
|
||||
icon: <FileSignature />,
|
||||
icon: <Stamp />,
|
||||
permission: FREIGHT_PERMS.settings.stamp.view,
|
||||
},
|
||||
{
|
||||
label: "Invoice stamp",
|
||||
href: "/dashboard/invoice-stamp-settings",
|
||||
icon: <Receipt />,
|
||||
permission: FREIGHT_PERMS.settings.invoiceStamp.view,
|
||||
},
|
||||
{
|
||||
label: "Contract templates",
|
||||
href: "/dashboard/contract-templates",
|
||||
|
||||
@@ -328,15 +328,18 @@ export const FREIGHT_PERMS = {
|
||||
view: "edr_freight_app:settings:dropdown:view",
|
||||
manage: "edr_freight_app:settings:dropdown:manage",
|
||||
},
|
||||
// The ONE company stamp/seal, applied to every generated document
|
||||
// (invoices, receipts, warehouse papers, the EDR side of contracts).
|
||||
stamp: {
|
||||
view: "edr_freight_app:settings:stamp:view",
|
||||
manage: "edr_freight_app:settings:stamp:manage",
|
||||
},
|
||||
// Company stamp/seal image stamped onto invoice/receipt PDFs — separate
|
||||
// from `stamp` above, which is the per-employee approval-record teeter.
|
||||
invoiceStamp: {
|
||||
view: "edr_freight_app:settings:invoice_stamp:view",
|
||||
manage: "edr_freight_app:settings:invoice_stamp:manage",
|
||||
// The per-officer approval teeter (ማህተም) + signature — genuinely per-person,
|
||||
// and NOT the company seal above. Retired: `invoiceStamp`, which used to
|
||||
// gate the company stamp before the two were untangled.
|
||||
teeter: {
|
||||
view: "edr_freight_app:settings:teeter:view",
|
||||
manage: "edr_freight_app:settings:teeter:manage",
|
||||
},
|
||||
exchangeRate: {
|
||||
view: "edr_freight_app:settings:exchange_rate:view",
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
Card,
|
||||
Center,
|
||||
Container,
|
||||
Divider,
|
||||
Grid,
|
||||
Group,
|
||||
Loader,
|
||||
SimpleGrid,
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Banknote,
|
||||
Contact,
|
||||
Download,
|
||||
Eye,
|
||||
FileText,
|
||||
@@ -32,6 +33,8 @@ import {
|
||||
FilePen,
|
||||
Paperclip,
|
||||
Receipt,
|
||||
UserCheck,
|
||||
UserRound,
|
||||
} from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo, useState } from "react";
|
||||
@@ -46,6 +49,7 @@ import {
|
||||
CompanyTypeBadge,
|
||||
InvoiceStatusBadge,
|
||||
PaymentStatusBadge,
|
||||
PersonCard,
|
||||
ProfileApprovalActions,
|
||||
ProfileChips,
|
||||
ProfileStatusBadge,
|
||||
@@ -259,7 +263,9 @@ export default function CustomerDetailPage() {
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={`View ${f.name}`}
|
||||
onClick={() => void fetchViewableFile(f.id, f.name).then(view)}
|
||||
onClick={() =>
|
||||
void fetchViewableFile(f.id, f.name).then(view)
|
||||
}
|
||||
>
|
||||
<Eye size={14} />
|
||||
</ActionIcon>
|
||||
@@ -268,7 +274,9 @@ export default function CustomerDetailPage() {
|
||||
type="button"
|
||||
size="xs"
|
||||
lineClamp={1}
|
||||
onClick={() => void fetchViewableFile(f.id, f.name).then(view)}
|
||||
onClick={() =>
|
||||
void fetchViewableFile(f.id, f.name).then(view)
|
||||
}
|
||||
style={{
|
||||
maxWidth: 170,
|
||||
textAlign: "left",
|
||||
@@ -615,7 +623,10 @@ export default function CustomerDetailPage() {
|
||||
[],
|
||||
);
|
||||
|
||||
const licenseProfiles = (company?.companyProfiles ?? []).filter(
|
||||
/** Never read `company.companyProfiles` directly — an endpoint that stops
|
||||
* loading the relation would otherwise crash the whole page. */
|
||||
const profiles = company?.companyProfiles ?? [];
|
||||
const licenseProfiles = profiles.filter(
|
||||
(p) => p.licenseFiles && p.licenseFiles.length > 0,
|
||||
);
|
||||
|
||||
@@ -629,14 +640,13 @@ export default function CustomerDetailPage() {
|
||||
[documents],
|
||||
);
|
||||
const poaLive = poaDocuments.filter((d) => d.code === POA_DELEGATION_CODE);
|
||||
const poaFields = [
|
||||
{ label: "PoA name", value: company?.poaName },
|
||||
{ label: "PoA email", value: company?.poaEmail },
|
||||
{ label: "PoA phone", value: company?.poaPhone },
|
||||
{ label: "PoA location", value: company?.poaLocation },
|
||||
{ label: "PoA address", value: company?.poaAddress },
|
||||
];
|
||||
const hasPoaDetails = poaFields.some((f) => f.value?.trim());
|
||||
const hasPoaDetails = [
|
||||
company?.poaName,
|
||||
company?.poaEmail,
|
||||
company?.poaPhone,
|
||||
company?.poaLocation,
|
||||
company?.poaAddress,
|
||||
].some((v) => v?.trim());
|
||||
// Shared with the portal (buildCompanyIdentityState) — same derivation, so
|
||||
// this page can never disagree with the rule the API actually enforces.
|
||||
const identityState = company?.identity;
|
||||
@@ -645,9 +655,7 @@ export default function CustomerDetailPage() {
|
||||
const hasEtradeRecord = Boolean(company?.licenceNumber?.trim());
|
||||
// A freight forwarder acts on other companies' behalf, so its PoA — details
|
||||
// and DARS delegation paper both — is mandatory rather than optional.
|
||||
const poaMandatory = (company?.companyProfiles ?? []).some(
|
||||
(p) => p.type === "freight_forwarder",
|
||||
);
|
||||
const poaMandatory = profiles.some((p) => p.type === "freight_forwarder");
|
||||
const delegationMissing =
|
||||
company?.identity?.poaDeclared === "yes" && poaLive.length === 0;
|
||||
|
||||
@@ -685,8 +693,9 @@ export default function CustomerDetailPage() {
|
||||
]}
|
||||
backTo="/dashboard/customers"
|
||||
title={company.name}
|
||||
subtitle={`TIN ${company.tin}${company.country ? ` · ${company.country}` : ""
|
||||
}`}
|
||||
subtitle={`TIN ${company.tin}${
|
||||
company.country ? ` · ${company.country}` : ""
|
||||
}`}
|
||||
meta={
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<CompanyTypeBadge type={company.type} />
|
||||
@@ -749,7 +758,7 @@ export default function CustomerDetailPage() {
|
||||
items={[
|
||||
{
|
||||
label: "Profiles",
|
||||
value: company.companyProfiles.length,
|
||||
value: profiles.length,
|
||||
icon: IdCard,
|
||||
color: "edr-green",
|
||||
},
|
||||
@@ -761,9 +770,7 @@ export default function CustomerDetailPage() {
|
||||
: "Pending approval",
|
||||
value: stillOnboarding
|
||||
? "—"
|
||||
: company.companyProfiles.filter(
|
||||
(p) => p.status === "pending",
|
||||
).length,
|
||||
: profiles.filter((p) => p.status === "pending").length,
|
||||
icon: IdCard,
|
||||
color: "yellow",
|
||||
},
|
||||
@@ -782,407 +789,392 @@ export default function CustomerDetailPage() {
|
||||
]}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<Stack gap="lg">
|
||||
<Text fw={600} c="edr-text">
|
||||
Company information
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
||||
<InfoField label="TIN" value={company.tin} />
|
||||
<InfoField label="VAT number" value={company.vatNumber} />
|
||||
<InfoField label="FAN number" value={company.fanNumber} />
|
||||
<InfoField
|
||||
label="Submitted on"
|
||||
value={formatDate(company.createdAt)}
|
||||
/>
|
||||
<InfoField
|
||||
label="Approved on"
|
||||
value={
|
||||
company.approvedAt
|
||||
? formatDate(company.approvedAt)
|
||||
: "Not yet approved"
|
||||
}
|
||||
/>
|
||||
<InfoField
|
||||
label="Owner identity"
|
||||
value={
|
||||
ownerIdentity?.verified
|
||||
? "Fayda verified"
|
||||
: ownerIdentity?.passportNumber
|
||||
? `Passport ${ownerIdentity.passportNumber}`
|
||||
: "Not verified"
|
||||
}
|
||||
/>
|
||||
<InfoField label="Country" value={company.country} />
|
||||
<InfoField
|
||||
label="Nationality"
|
||||
value={
|
||||
company.nationality
|
||||
? humanize(company.nationality)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{/* Why this company's registration was typed rather than
|
||||
fetched, and why it carries no business licence. */}
|
||||
<InfoField
|
||||
label="Registration"
|
||||
value={
|
||||
company.cooperative
|
||||
? "Co-operative union / farm (no trade licence)"
|
||||
: "eTrade trade licence"
|
||||
}
|
||||
/>
|
||||
<InfoField label="Address" value={company.address} />
|
||||
<InfoField label="Website" value={company.website} />
|
||||
<InfoField label="Email" value={company.email} />
|
||||
<InfoField label="Phone" value={company.phone} />
|
||||
<Box />
|
||||
<InfoField
|
||||
label="Contact person"
|
||||
value={company.contactPersonName}
|
||||
/>
|
||||
<InfoField
|
||||
label="Contact phone"
|
||||
value={company.contactPersonPhone}
|
||||
/>
|
||||
<Box />
|
||||
<InfoField label="Owner" value={company.ownerName} />
|
||||
<InfoField label="Owner email" value={company.ownerEmail} />
|
||||
<InfoField label="Owner phone" value={company.ownerPhone} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Stack gap="lg">
|
||||
<Group gap="xs" wrap="nowrap" justify="space-between">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text fw={600} c="edr-text">
|
||||
eTrade registration
|
||||
</Text>
|
||||
{hasEtradeRecord ? (
|
||||
<Badge size="sm" color="edr-green" variant="light">
|
||||
Verified with eTrade
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size="sm" color="gray" variant="light">
|
||||
No eTrade record
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
{hasEtradeRecord && (
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
aria-label="Download TIN record"
|
||||
onClick={() => downloadTinRecord(company)}
|
||||
>
|
||||
<Download size={16} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
</Group>
|
||||
{hasEtradeRecord ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
||||
<InfoField
|
||||
label="License number"
|
||||
value={company.licenceNumber}
|
||||
/>
|
||||
<InfoField label="Status" value={company.statusDescription} />
|
||||
<InfoField
|
||||
label="Date registered"
|
||||
value={company.dateRegistered}
|
||||
/>
|
||||
<InfoField label="Renewed from" value={company.renewedFrom} />
|
||||
<InfoField label="Renewal date" value={company.renewalDate} />
|
||||
<InfoField label="Renewed to" value={company.renewedTo} />
|
||||
<InfoField label="Region" value={company.region} />
|
||||
<InfoField label="Zone" value={company.zone} />
|
||||
<InfoField label="Woreda" value={company.woreda} />
|
||||
<InfoField label="Kebele" value={company.kebele} />
|
||||
<InfoField label="House No" value={company.houseNo} />
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
No eTrade registration record on file for this customer's
|
||||
TIN.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Stack gap="lg">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text fw={600} c="edr-text">
|
||||
Owner identity
|
||||
</Text>
|
||||
{identityState?.subject === "owner" && (
|
||||
<Badge size="sm" color="blue" variant="light">
|
||||
Verifies for this company
|
||||
</Badge>
|
||||
)}
|
||||
{ownerIdentity?.verified ? (
|
||||
<Badge size="sm" color="edr-green" variant="light">
|
||||
Fayda verified
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size="sm" color="gray" variant="light">
|
||||
Not verified
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{/* THE check: is the owner the company put forward the person
|
||||
the eTrade licence actually names? Advisory — eTrade and
|
||||
Fayda transliterate Amharic names differently, so this is a
|
||||
prompt to look, not a verdict. */}
|
||||
{identityState?.ownerMatchesEtrade === false ? (
|
||||
<Alert
|
||||
color="amber"
|
||||
variant="light"
|
||||
icon={<AlertTriangle size={16} />}
|
||||
title="Does not match the eTrade licence"
|
||||
>
|
||||
The licence names{" "}
|
||||
<strong>{identityState.etradeManagerName}</strong>, but this
|
||||
company recorded <strong>{company.ownerName}</strong>.
|
||||
</Alert>
|
||||
) : identityState?.ownerMatchesEtrade === true ? (
|
||||
<Badge
|
||||
size="sm"
|
||||
color="edr-green"
|
||||
variant="light"
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Matches the eTrade licence
|
||||
</Badge>
|
||||
) : company.cooperative ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
A co-operative union or farm holds no trade licence, so
|
||||
there is no eTrade record to check the owner against.
|
||||
</Text>
|
||||
) : (
|
||||
<Text size="xs" c="dimmed">
|
||||
No eTrade manager name on file to compare against.
|
||||
</Text>
|
||||
)}
|
||||
{ownerIdentity?.verified ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
||||
<InfoField label="Name" value={ownerIdentity.name} />
|
||||
<InfoField label="Phone" value={ownerIdentity.phone} />
|
||||
<InfoField label="Email" value={ownerIdentity.email} />
|
||||
<InfoField label="Address" value={ownerIdentity.address} />
|
||||
<InfoField
|
||||
label="Verified at"
|
||||
value={formatDate(ownerIdentity.verifiedAt)}
|
||||
/>
|
||||
<InfoField
|
||||
label="Birthdate"
|
||||
value={ownerIdentity.birthdate}
|
||||
/>
|
||||
<InfoField label="Gender" value={ownerIdentity.gender} />
|
||||
<InfoField
|
||||
label="Passport number"
|
||||
value={ownerIdentity.passportNumber}
|
||||
/>
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
{ownerIdentity?.passportNumber
|
||||
? `Not Fayda verified — identified by passport ${ownerIdentity.passportNumber}.`
|
||||
: "The company owner has not verified their identity with Fayda."}
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Stack gap="lg">
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text fw={600} c="edr-text">
|
||||
Power of Attorney
|
||||
</Text>
|
||||
{poaMandatory && (
|
||||
<Badge size="xs" color="blue" variant="light">
|
||||
Required for freight forwarder
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
{delegationMissing ? (
|
||||
<Badge size="sm" color="red" variant="light">
|
||||
DARS delegation paper missing
|
||||
</Badge>
|
||||
) : poaLive.length > 0 ? (
|
||||
<Badge size="sm" color="edr-green" variant="light">
|
||||
DARS delegation paper on file
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size="sm" color="gray" variant="light">
|
||||
Not provided
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{hasPoaDetails ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
||||
{poaFields.map((f) => (
|
||||
<InfoField
|
||||
key={f.label}
|
||||
label={f.label}
|
||||
value={f.value}
|
||||
/>
|
||||
))}
|
||||
<InfoField
|
||||
label="PoA Fayda"
|
||||
value={
|
||||
poaIdentity?.verified ? "Verified" : "Not verified"
|
||||
}
|
||||
/>
|
||||
{poaIdentity?.verified && (
|
||||
<>
|
||||
<Grid gap="lg" align="flex-start">
|
||||
{/* Company facts — the wide column. People live in the narrow one
|
||||
beside it, so nothing about a person is stated twice. */}
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
<Stack gap="lg">
|
||||
<Card>
|
||||
<Stack gap="lg">
|
||||
<Text fw={600} c="edr-text">
|
||||
Company information
|
||||
</Text>
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="lg">
|
||||
<InfoField label="TIN" value={company.tin} />
|
||||
<InfoField
|
||||
label="PoA verified at"
|
||||
value={formatDate(poaIdentity.verifiedAt)}
|
||||
label="VAT number"
|
||||
value={company.vatNumber}
|
||||
/>
|
||||
<InfoField label="Country" value={company.country} />
|
||||
<InfoField
|
||||
label="Nationality"
|
||||
value={
|
||||
company.nationality
|
||||
? humanize(company.nationality)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{/* Why this company's registration was typed rather
|
||||
than fetched, and why it carries no licence. */}
|
||||
<InfoField
|
||||
label="Registration"
|
||||
value={
|
||||
company.cooperative
|
||||
? "Co-operative union / farm (no trade licence)"
|
||||
: "eTrade trade licence"
|
||||
}
|
||||
/>
|
||||
<InfoField label="Address" value={company.address} />
|
||||
<InfoField label="Email" value={company.email} />
|
||||
<InfoField label="Phone" value={company.phone} />
|
||||
<InfoField label="Website" value={company.website} />
|
||||
<InfoField
|
||||
label="Submitted on"
|
||||
value={formatDate(company.createdAt)}
|
||||
/>
|
||||
<InfoField
|
||||
label="PoA birthdate"
|
||||
value={poaIdentity.birthdate}
|
||||
label="Approved on"
|
||||
value={
|
||||
company.approvedAt
|
||||
? formatDate(company.approvedAt)
|
||||
: "Not yet approved"
|
||||
}
|
||||
/>
|
||||
<InfoField label="PoA gender" value={poaIdentity.gender} />
|
||||
</>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
No Power of Attorney representative recorded for this
|
||||
customer.
|
||||
</Text>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Stack gap="sm">
|
||||
<Text
|
||||
size="xs"
|
||||
fw={600}
|
||||
c="edr-muted"
|
||||
tt="uppercase"
|
||||
style={{ letterSpacing: "0.04em" }}
|
||||
>
|
||||
DARS delegation paper
|
||||
</Text>
|
||||
|
||||
{documentsQuery.isLoading ? (
|
||||
<Group gap="xs">
|
||||
<Loader size="xs" />
|
||||
<Text size="sm" c="dimmed">
|
||||
Loading documents…
|
||||
</Text>
|
||||
</Group>
|
||||
) : documentsQuery.isError ? (
|
||||
<Group gap="sm">
|
||||
<Text size="sm" c="red">
|
||||
Failed to load documents.
|
||||
</Text>
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
size="xs"
|
||||
onClick={() => void documentsQuery.refetch()}
|
||||
>
|
||||
Retry
|
||||
</Anchor>
|
||||
</Group>
|
||||
) : poaDocuments.length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
No DARS delegation paper uploaded.
|
||||
</Text>
|
||||
) : (
|
||||
poaDocuments.map((doc) => (
|
||||
<Group key={doc.id} justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm" wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<Paperclip
|
||||
size={14}
|
||||
className="shrink-0 text-edr-muted"
|
||||
/>
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
size="sm"
|
||||
lineClamp={1}
|
||||
onClick={() =>
|
||||
void fetchViewableFile(doc.id, doc.name).then(view)
|
||||
}
|
||||
>
|
||||
{doc.name}
|
||||
</Anchor>
|
||||
<Text size="xs" c="dimmed" className="shrink-0">
|
||||
{formatBytes(doc.size)} ·{" "}
|
||||
{formatDate(doc.uploadedAt)}
|
||||
<Card>
|
||||
<Stack gap="lg">
|
||||
<Group gap="xs" wrap="nowrap" justify="space-between">
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<Text fw={600} c="edr-text">
|
||||
eTrade registration
|
||||
</Text>
|
||||
{doc.code === POA_DELEGATION_PENDING_CODE && (
|
||||
<Badge
|
||||
size="xs"
|
||||
color="yellow"
|
||||
variant="light"
|
||||
className="shrink-0"
|
||||
>
|
||||
Pending approval
|
||||
{hasEtradeRecord ? (
|
||||
<Badge size="sm" color="edr-green" variant="light">
|
||||
Verified with eTrade
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge size="sm" color="gray" variant="light">
|
||||
No eTrade record
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
<Group gap={4} wrap="nowrap">
|
||||
{hasEtradeRecord && (
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={`Preview ${doc.name}`}
|
||||
onClick={() =>
|
||||
void fetchViewableFile(doc.id, doc.name).then(view)
|
||||
}
|
||||
>
|
||||
<Eye size={16} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
component="button"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
void downloadBookingFile(doc.id, doc.name)
|
||||
}
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={`Download ${doc.name}`}
|
||||
variant="default"
|
||||
aria-label="Download TIN record"
|
||||
onClick={() => downloadTinRecord(company)}
|
||||
>
|
||||
<Download size={16} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
)}
|
||||
</Group>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
{hasEtradeRecord ? (
|
||||
<SimpleGrid
|
||||
cols={{ base: 1, sm: 2, lg: 3 }}
|
||||
spacing="lg"
|
||||
>
|
||||
<InfoField
|
||||
label="License number"
|
||||
value={company.licenceNumber}
|
||||
/>
|
||||
<InfoField
|
||||
label="Status"
|
||||
value={company.statusDescription}
|
||||
/>
|
||||
<InfoField
|
||||
label="Date registered"
|
||||
value={company.dateRegistered}
|
||||
/>
|
||||
<InfoField
|
||||
label="Renewed from"
|
||||
value={company.renewedFrom}
|
||||
/>
|
||||
<InfoField
|
||||
label="Renewal date"
|
||||
value={company.renewalDate}
|
||||
/>
|
||||
<InfoField
|
||||
label="Renewed to"
|
||||
value={company.renewedTo}
|
||||
/>
|
||||
<InfoField label="Region" value={company.region} />
|
||||
<InfoField label="Zone" value={company.zone} />
|
||||
<InfoField label="Woreda" value={company.woreda} />
|
||||
<InfoField label="Kebele" value={company.kebele} />
|
||||
<InfoField label="House No" value={company.houseNo} />
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<Text size="sm" c="dimmed">
|
||||
No eTrade registration record on file for this
|
||||
customer's TIN.
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Text fw={600} c="edr-text">
|
||||
Role profiles
|
||||
</Text>
|
||||
<ProfileChips profiles={company.companyProfiles} />
|
||||
</Group>
|
||||
<Box style={{ overflowX: "auto" }} w="100%">
|
||||
<Box miw={1040}>
|
||||
<DataTable
|
||||
columns={profileColumns}
|
||||
data={company.companyProfiles}
|
||||
status="success"
|
||||
emptyMessage="No profiles registered."
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
<Card>
|
||||
<Stack gap="md">
|
||||
<Group justify="space-between">
|
||||
<Text fw={600} c="edr-text">
|
||||
Role profiles
|
||||
</Text>
|
||||
<ProfileChips profiles={profiles} />
|
||||
</Group>
|
||||
{/* Narrower than the old full-width layout — the table
|
||||
shares the row with the people column now. */}
|
||||
<Box style={{ overflowX: "auto" }} w="100%">
|
||||
<Box miw={760}>
|
||||
<DataTable
|
||||
columns={profileColumns}
|
||||
data={profiles}
|
||||
status="success"
|
||||
emptyMessage="No profiles registered."
|
||||
containerClassName="border-0 shadow-none bg-transparent"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
|
||||
{/* People: owner, then power of attorney, then contact person —
|
||||
the order a reviewer checks them in. */}
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<Stack gap="md">
|
||||
<PersonCard
|
||||
title="Owner"
|
||||
icon={<UserRound size={15} className="text-edr-muted" />}
|
||||
verified={Boolean(ownerIdentity?.verified)}
|
||||
badges={
|
||||
<>
|
||||
{identityState?.subject === "owner" && (
|
||||
<Badge size="xs" color="blue" variant="light">
|
||||
Verifies for this company
|
||||
</Badge>
|
||||
)}
|
||||
{identityState?.ownerMatchesEtrade === true && (
|
||||
<Badge size="xs" color="edr-green" variant="light">
|
||||
Matches eTrade licence
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
notice={
|
||||
/* THE check: is the owner the company put forward the
|
||||
person the eTrade licence actually names? Advisory —
|
||||
eTrade and Fayda transliterate Amharic names
|
||||
differently, so this is a prompt to look, not a
|
||||
verdict. */
|
||||
identityState?.ownerMatchesEtrade === false ? (
|
||||
<Alert
|
||||
color="amber"
|
||||
variant="light"
|
||||
p="xs"
|
||||
icon={<AlertTriangle size={16} />}
|
||||
title="Does not match the eTrade licence"
|
||||
>
|
||||
<Text size="xs">
|
||||
The licence names{" "}
|
||||
<strong>{identityState.etradeManagerName}</strong>,
|
||||
but this company recorded{" "}
|
||||
<strong>{company.ownerName ?? "nobody"}</strong>.
|
||||
</Text>
|
||||
</Alert>
|
||||
) : !ownerIdentity?.verified &&
|
||||
ownerIdentity?.passportNumber ? (
|
||||
<Text size="xs" c="dimmed">
|
||||
Identified by passport rather than Fayda.
|
||||
</Text>
|
||||
) : null
|
||||
}
|
||||
fields={[
|
||||
{
|
||||
label: "Name",
|
||||
value: company.ownerName ?? ownerIdentity?.name,
|
||||
},
|
||||
{
|
||||
label: "Email",
|
||||
value: company.ownerEmail ?? ownerIdentity?.email,
|
||||
},
|
||||
{
|
||||
label: "Phone",
|
||||
value: company.ownerPhone ?? ownerIdentity?.phone,
|
||||
},
|
||||
{
|
||||
label: "Passport number",
|
||||
value: ownerIdentity?.passportNumber,
|
||||
},
|
||||
{ label: "Address", value: ownerIdentity?.address },
|
||||
{ label: "Birthdate", value: ownerIdentity?.birthdate },
|
||||
{ label: "Gender", value: ownerIdentity?.gender },
|
||||
{
|
||||
label: "Verified on",
|
||||
value: ownerIdentity?.verifiedAt
|
||||
? formatDate(ownerIdentity.verifiedAt)
|
||||
: null,
|
||||
},
|
||||
]}
|
||||
emptyMessage="No owner recorded for this company."
|
||||
/>
|
||||
|
||||
<PersonCard
|
||||
title="Power of attorney"
|
||||
icon={<UserCheck size={15} className="text-edr-muted" />}
|
||||
verified={
|
||||
// No PoA at all → no badge, rather than a "not verified"
|
||||
// that reads as a problem where none exists.
|
||||
hasPoaDetails || identityState?.poaDeclared === "yes"
|
||||
? Boolean(poaIdentity?.verified)
|
||||
: undefined
|
||||
}
|
||||
badges={
|
||||
<>
|
||||
{identityState?.subject === "poa" && (
|
||||
<Badge size="xs" color="blue" variant="light">
|
||||
Verifies for this company
|
||||
</Badge>
|
||||
)}
|
||||
{poaMandatory && (
|
||||
<Badge size="xs" color="blue" variant="light">
|
||||
Required for freight forwarder
|
||||
</Badge>
|
||||
)}
|
||||
{delegationMissing && (
|
||||
<Badge size="xs" color="red" variant="light">
|
||||
Delegation paper missing
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
fields={[
|
||||
{ label: "Name", value: company.poaName },
|
||||
{ label: "Email", value: company.poaEmail },
|
||||
{ label: "Phone", value: company.poaPhone },
|
||||
{ label: "Location", value: company.poaLocation },
|
||||
{ label: "Address", value: company.poaAddress },
|
||||
{ label: "Birthdate", value: poaIdentity?.birthdate },
|
||||
{ label: "Gender", value: poaIdentity?.gender },
|
||||
{
|
||||
label: "Verified on",
|
||||
value: poaIdentity?.verifiedAt
|
||||
? formatDate(poaIdentity.verifiedAt)
|
||||
: null,
|
||||
},
|
||||
]}
|
||||
emptyMessage="No representative recorded for this customer."
|
||||
>
|
||||
<Stack gap="xs">
|
||||
<Text size="xs" c="edr-muted">
|
||||
DARS delegation paper
|
||||
</Text>
|
||||
|
||||
{documentsQuery.isLoading ? (
|
||||
<Group gap="xs">
|
||||
<Loader size="xs" />
|
||||
<Text size="sm" c="dimmed">
|
||||
Loading…
|
||||
</Text>
|
||||
</Group>
|
||||
) : documentsQuery.isError ? (
|
||||
<Group gap="sm">
|
||||
<Text size="sm" c="red">
|
||||
Failed to load documents.
|
||||
</Text>
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
size="xs"
|
||||
onClick={() => void documentsQuery.refetch()}
|
||||
>
|
||||
Retry
|
||||
</Anchor>
|
||||
</Group>
|
||||
) : poaDocuments.length === 0 ? (
|
||||
<Text size="sm" c="dimmed">
|
||||
Not uploaded.
|
||||
</Text>
|
||||
) : (
|
||||
poaDocuments.map((doc) => (
|
||||
<Stack key={doc.id} gap={2}>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Paperclip
|
||||
size={13}
|
||||
className="shrink-0 text-edr-muted"
|
||||
/>
|
||||
<Anchor
|
||||
component="button"
|
||||
type="button"
|
||||
size="sm"
|
||||
lineClamp={1}
|
||||
style={{ flex: 1, textAlign: "left" }}
|
||||
onClick={() =>
|
||||
void fetchViewableFile(doc.id, doc.name).then(
|
||||
view,
|
||||
)
|
||||
}
|
||||
>
|
||||
{doc.name}
|
||||
</Anchor>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={`Preview ${doc.name}`}
|
||||
onClick={() =>
|
||||
void fetchViewableFile(doc.id, doc.name).then(
|
||||
view,
|
||||
)
|
||||
}
|
||||
>
|
||||
<Eye size={15} />
|
||||
</ActionIcon>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
component="button"
|
||||
type="button"
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
aria-label={`Download ${doc.name}`}
|
||||
onClick={() =>
|
||||
void downloadBookingFile(doc.id, doc.name)
|
||||
}
|
||||
>
|
||||
<Download size={15} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
<Group gap={6} pl={19} wrap="wrap">
|
||||
<Text size="xs" c="dimmed">
|
||||
{formatBytes(doc.size)} ·{" "}
|
||||
{formatDate(doc.uploadedAt)}
|
||||
</Text>
|
||||
{doc.code === POA_DELEGATION_PENDING_CODE && (
|
||||
<Badge size="xs" color="yellow" variant="light">
|
||||
Pending approval
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
</Stack>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</PersonCard>
|
||||
|
||||
<PersonCard
|
||||
title="Contact person"
|
||||
icon={<Contact size={15} className="text-edr-muted" />}
|
||||
fields={[
|
||||
{ label: "Name", value: company.contactPersonName },
|
||||
{ label: "Phone", value: company.contactPersonPhone },
|
||||
]}
|
||||
emptyMessage="No contact person recorded."
|
||||
/>
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</Stack>
|
||||
</Tabs.Panel>
|
||||
|
||||
@@ -1198,9 +1190,9 @@ export default function CustomerDetailPage() {
|
||||
error={
|
||||
bookingsQuery.isError
|
||||
? {
|
||||
message: "Failed to load bookings.",
|
||||
onRetry: () => void bookingsQuery.refetch(),
|
||||
}
|
||||
message: "Failed to load bookings.",
|
||||
onRetry: () => void bookingsQuery.refetch(),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
@@ -1220,9 +1212,9 @@ export default function CustomerDetailPage() {
|
||||
error={
|
||||
documentsQuery.isError
|
||||
? {
|
||||
message: "Failed to load documents.",
|
||||
onRetry: () => void documentsQuery.refetch(),
|
||||
}
|
||||
message: "Failed to load documents.",
|
||||
onRetry: () => void documentsQuery.refetch(),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
@@ -1297,9 +1289,9 @@ export default function CustomerDetailPage() {
|
||||
error={
|
||||
paymentsQuery.isError
|
||||
? {
|
||||
message: "Failed to load payments.",
|
||||
onRetry: () => void paymentsQuery.refetch(),
|
||||
}
|
||||
message: "Failed to load payments.",
|
||||
onRetry: () => void paymentsQuery.refetch(),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
@@ -1320,9 +1312,9 @@ export default function CustomerDetailPage() {
|
||||
error={
|
||||
invoicesQuery.isError
|
||||
? {
|
||||
message: "Failed to load invoices.",
|
||||
onRetry: () => void invoicesQuery.refetch(),
|
||||
}
|
||||
message: "Failed to load invoices.",
|
||||
onRetry: () => void invoicesQuery.refetch(),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
pagination={{
|
||||
|
||||
@@ -17,10 +17,16 @@ import {
|
||||
} from "@/hooks/useStampSettings";
|
||||
|
||||
/**
|
||||
* The one company stamp/seal stamped onto every generated invoice/receipt
|
||||
* PDF (InvoiceDocumentService). Single global image — no per-employee choice.
|
||||
* The ONE company stamp/seal, read by every document path server-side via
|
||||
* StampSettingsService: invoices and receipts (InvoiceDocumentService),
|
||||
* warehouse release + handover papers, and the EDR side of contract signature
|
||||
* blocks. Single global image — no per-employee choice, and staff never upload
|
||||
* one when signing.
|
||||
*
|
||||
* Not to be confused with the per-officer teeter (ማህተም) at
|
||||
* /user-management/teeter-and-signature, which is genuinely per-person.
|
||||
*/
|
||||
export default function InvoiceStampSettingsPage() {
|
||||
export default function CompanyStampSettingsPage() {
|
||||
const { data, isLoading } = useStampSettingsQuery();
|
||||
const setStamp = useSetStamp();
|
||||
const clearStamp = useClearStamp();
|
||||
@@ -47,11 +53,13 @@ export default function InvoiceStampSettingsPage() {
|
||||
<div className="p-4 w-full max-w-screen-sm mx-auto">
|
||||
<Card className="shadow-lg border-gray-200 dark:border-gray-700">
|
||||
<CardHeader>
|
||||
<CardTitle>Invoice stamp</CardTitle>
|
||||
<CardTitle>Company stamp</CardTitle>
|
||||
<CardDescription>
|
||||
Stamped onto every generated invoice and receipt PDF. Replacing it
|
||||
here changes it everywhere at once — there is no per-invoice or
|
||||
per-user choice.
|
||||
The single EDR seal, applied to every generated document — invoices
|
||||
and receipts, warehouse release and handover papers, and the EDR
|
||||
side of signed contracts. Replacing it here changes it everywhere at
|
||||
once; there is no per-document, per-invoice or per-employee choice.
|
||||
Staff do not upload their own.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
@@ -3,7 +3,7 @@ import { Navigate, Outlet, Route } from "react-router-dom";
|
||||
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
import { NO_ACCESS_PATH, resolveLandingPath } from "@/lib/landing";
|
||||
import { isSuperAdmin } from "@/lib/permissions";
|
||||
import { FREIGHT_PERMS, isSuperAdmin } from "@/lib/permissions";
|
||||
import { WithPermission } from "@/shared/hooks/useHas";
|
||||
import PendingExternalUsers from "@/super-admin/components/externalUsers/PendingExternalUsers";
|
||||
import TemplatePage from "@/super-admin/components/templates/components/templates";
|
||||
@@ -44,6 +44,8 @@ import { SidebarProvider } from "@/shared/common/ui/sidebar";
|
||||
import { AuthProvider as UmAuthProvider } from "@/shared/context/AuthContext";
|
||||
import { PermissionProvider } from "@/shared/context/PermissionContext";
|
||||
import UserManagementPage from "@/pages/UserManagementPage";
|
||||
import { RequirePermission } from "@/components/auth/RequirePermission";
|
||||
import UploadTeeterAndSignature from "@/record-management/components/Settings/uploadTeeterandSingature";
|
||||
|
||||
/**
|
||||
* Provider shell for the vendored IAM UI. Feeds its Auth + Permission contexts
|
||||
@@ -138,6 +140,29 @@ export function UserManagementRoutes(): ReactElement {
|
||||
path="user-management/position-management"
|
||||
element={<PositionManagementPage />}
|
||||
/>
|
||||
{/*
|
||||
The per-officer teeter (ማህተም) + signature upload. This
|
||||
is NOT the company stamp: it is the individual approval
|
||||
stamp a record officer applies to records, locale-aware
|
||||
(am/en) and genuinely per-person. It used to sit at
|
||||
/dashboard/stamp-settings under Settings, next to the
|
||||
single global company stamp, which read as duplication.
|
||||
|
||||
Gated by settings:teeter:*, split out of settings:stamp:*
|
||||
when the two were untangled — settings:stamp:* now means
|
||||
the company stamp, so anyone who held it for the teeter
|
||||
needs the new key granted.
|
||||
*/}
|
||||
<Route
|
||||
path="user-management/teeter-and-signature"
|
||||
element={
|
||||
<RequirePermission
|
||||
permission={FREIGHT_PERMS.settings.teeter.view}
|
||||
>
|
||||
<UploadTeeterAndSignature />
|
||||
</RequirePermission>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="user-management/migrated-records-management"
|
||||
element={<MigratedDataManagementPage />}
|
||||
|
||||
@@ -169,7 +169,8 @@ export default function OnboardingWizardDialog({
|
||||
setCooperative(checked);
|
||||
if (checked) {
|
||||
setRoles((prev) => prev.filter((r) => r !== "freight_forwarder"));
|
||||
setNationality((prev) => (prev === "foreign" ? "ethiopian" : prev));
|
||||
// Ethiopian is then the only answer left, so it is made rather than asked.
|
||||
setNationality("ethiopian");
|
||||
}
|
||||
}, []);
|
||||
const [documentFiles, setDocumentFiles] = useState<
|
||||
|
||||
@@ -50,7 +50,7 @@ function buildLicenseSetting(
|
||||
isMultiple: true,
|
||||
maxFiles: 10,
|
||||
allowedExtensions: ["pdf", "png", "jpg", "jpeg"],
|
||||
maxSizeMb: 10,
|
||||
maxSizeMb: 25,
|
||||
order: 1,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -29,7 +29,11 @@ export default function NationalitySelect({
|
||||
<SimpleGrid cols={{ base: 1, sm: excludeForeign ? 1 : 2 }} spacing="md">
|
||||
<RoleCard
|
||||
label="Ethiopian Company"
|
||||
description="Registered in Ethiopia. You'll provide a TIN certificate, commercial registration and national ID."
|
||||
description={
|
||||
excludeForeign
|
||||
? "Registered in Ethiopia. You'll provide a TIN certificate, your co-operative registration certificate and national ID."
|
||||
: "Registered in Ethiopia. You'll provide a TIN certificate, commercial registration and national ID."
|
||||
}
|
||||
icon={<MapPin size={22} />}
|
||||
selected={value === "ethiopian"}
|
||||
onClick={() => onChange("ethiopian")}
|
||||
|
||||
Reference in New Issue
Block a user