Merge branch 'staging' into freight/fix/pay

This commit is contained in:
ghost2023
2026-08-01 14:21:28 +03:00
79 changed files with 2209 additions and 260 deletions

View File

@@ -28,6 +28,31 @@ interface ETradeInfoProps {
const isValidTin = (tin: string) => tin.length === 10;
/** Plain-text summary of the fetched eTrade record, downloaded client-side (eTrade returns data, not a document). */
function downloadTinRecord(tin: string, data: CompanyRegistrationData) {
const lines = [
`TIN: ${tin}`,
`Company name: ${data.companyName}`,
`Licence number: ${data.licenceNumber}`,
`Status: ${data.statusDescription}`,
`Date registered: ${data.dateRegistered}`,
`Renewed from: ${data.renewedFrom}`,
`Renewal date: ${data.renewalDate}`,
`Renewed to: ${data.renewedTo}`,
`Address: ${[data.region, data.zone, data.woreda, data.kebele, data.houseNo].filter(Boolean).join(", ")}`,
`Manager: ${data.managerName}`,
];
const blob = new Blob([lines.join("\n")], { type: "text/plain;charset=utf-8" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `tin-${tin}.txt`;
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 60_000);
}
export default function ETradeInfo({
tin,
register,
@@ -123,6 +148,17 @@ export default function ETradeInfo({
{isLoading ? "Getting..." : "Get Data"}
</Button>
)}
{status === "verified" && mutation.data && !mutation.data.tinTaken && (
<Button
variant="light"
color="edr-green"
onClick={() => downloadTinRecord(tin, mutation.data!)}
leftSection={<Download size={16} />}
mt="24px"
>
Download
</Button>
)}
</Group>
{notFound && (

View File

@@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import { Link } from "react-router-dom";
import { AlertTriangle, ArrowRight, Clock } from "lucide-react";
import { AlertTriangle, ArrowRight, CheckCircle2, Clock } from "lucide-react";
import { api } from "@/services/api";
import useAuth from "@/hooks/useAuth";
import type { OnboardingRequirements } from "@/services/companies.service";
@@ -150,18 +150,46 @@ export default function OnboardingResumeBanner({
/**
* Post-onboarding review banner. Surfaces (in priority order):
* 0. The account is suspended/blacklisted — hard lock.
* 1. A pending profile-edit review — the whole account is locked until an admin
* approves the submitted changes.
* 2. A rejected profile-edit review — links to Settings to amend & resubmit.
* 3. Per-operational-profile approval — bookings unlock as each role clears.
* 2. Backoffice requested changes — soft: same edit-and-resubmit call to
* action as a rejection, but the edit appends to the same request.
* 3. A rejected profile-edit review — links to Settings to amend & resubmit
* (starts a fresh request).
* 4/5. Company- and per-operational-profile approval — bookings unlock as
* each role clears.
* Self-hides when there's nothing outstanding.
*/
export function AccountReviewBanner() {
const { company, reviewStatus, reviewNote } = useAuth();
const { company, companyStatus, reviewStatus, reviewNote } = useAuth();
const profiles = company?.company?.companyProfiles ?? [];
const pending = profiles.filter((p) => p.status === "pending");
const approved = profiles.filter((p) => p.status === "active");
// 0. Account suspended/blacklisted — the hardest lock, takes priority over
// everything else since nothing below matters if the account is shut down.
if (companyStatus === "suspended" || companyStatus === "blacklisted") {
return (
<div className="border-b border-red-200 bg-red-50 px-6 py-3">
<div className="mx-auto flex max-w-6xl items-center gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-red-100 text-red-700">
<AlertTriangle size={18} />
</span>
<span className="flex flex-col gap-0.5">
<span className="text-sm font-semibold text-red-900">
Your account has been suspended
</span>
<span className="text-xs text-red-800">
Contact EDR support to resolve this before you can continue
working.
</span>
</span>
</div>
</div>
);
}
// 1. Profile-edit review pending — the account-wide lock.
if (reviewStatus === "pending") {
return (
@@ -184,7 +212,41 @@ export function AccountReviewBanner() {
);
}
// 2. Profile-edit review rejected — prompt to fix & resubmit.
// 2. Backoffice asked for specific changes — soft: same edit-and-resubmit
// call to action as a rejection, but the copy stays collaborative since
// the edit appends to this same request instead of starting over.
if (reviewStatus === "changes_requested") {
return (
<div className="border-b border-amber-200 bg-amber-50 px-6 py-3">
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-amber-100 text-amber-700">
<AlertTriangle size={18} />
</span>
<span className="flex flex-col gap-0.5">
<span className="text-sm font-semibold text-amber-900">
Changes requested on your submission
</span>
<span className="text-xs text-amber-800">
{reviewNote
? `Reviewer note: ${reviewNote}`
: "Please update the requested details and resubmit for review."}
</span>
</span>
</div>
<Link
to="/settings"
className="inline-flex items-center gap-2 rounded-lg bg-amber-600 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-transform hover:scale-[1.02]"
>
Review &amp; resubmit
<ArrowRight size={16} />
</Link>
</div>
</div>
);
}
// 3. Profile-edit review rejected — prompt to fix & resubmit.
if (reviewStatus === "rejected") {
return (
<div className="border-b border-red-200 bg-red-50 px-6 py-3">
@@ -216,8 +278,45 @@ export function AccountReviewBanner() {
);
}
// 3. Per-operational-profile approval (existing behaviour).
if (profiles.length === 0 || pending.length === 0) return null;
// 4. Company approved and awaiting its first operational profile — nothing
// profile-specific to report yet, but the account itself is pending.
if (profiles.length === 0) {
if (companyStatus === "pending") {
return (
<div className="border-b border-amber-200 bg-amber-50 px-6 py-3">
<div className="mx-auto flex max-w-6xl items-center gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-amber-100 text-amber-700">
<Clock size={18} />
</span>
<span className="text-sm font-semibold text-amber-900">
Your account is pending approval
</span>
</div>
</div>
);
}
return null;
}
// 5. Per-operational-profile approval (existing behaviour).
if (pending.length === 0) {
// Nothing outstanding — a quiet confirmation that the account is live.
if (companyStatus === "active") {
return (
<div className="border-b border-emerald-200 bg-emerald-50 px-6 py-3">
<div className="mx-auto flex max-w-6xl items-center gap-3">
<span className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-emerald-100 text-emerald-700">
<CheckCircle2 size={18} />
</span>
<span className="text-sm font-semibold text-emerald-900">
Your account is approved
</span>
</div>
</div>
);
}
return null;
}
const pendingLabel = pending
.map((p) => p.type.replace(/_/g, " "))

View File

@@ -130,6 +130,8 @@ export const URL_CONSTANTS = {
CANCEL: (id: string | number) => `/api/bookings/${id}/cancel`,
CONFIRM: (id: string | number) => `/api/bookings/${id}/confirm`,
CUSTOMER_TRUCKS: (id: string) => `/api/bookings/${id}/customer-trucks`,
CUSTOMER_TRUCKS_BULK: (id: string) =>
`/api/bookings/${id}/customer-trucks/bulk`,
CUSTOMER_TRUCK: (id: string, assignmentId: string) =>
`/api/bookings/${id}/customer-trucks/${assignmentId}`,
},

View File

@@ -1,3 +1,6 @@
// Must stay the first import: pins all date/time display to EAT before any
// module can create a formatter in the PC's local timezone.
import "@edr/ui-common/display-timezone";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router-dom";

View File

@@ -1,5 +1,4 @@
import { Box, Group, Text } from "@mantine/core";
import { format } from "date-fns";
import { memo } from "react";
import { STATUS_CONFIG, cv } from "../constants";
@@ -56,7 +55,10 @@ export const ActivityRow = memo(function ActivityRow({
</Text>
</Box>
<Text fz={11} c="edr-muted" className="shrink-0">
{format(new Date(booking.createdAt), "MMM d")}
{new Date(booking.createdAt).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
})}
</Text>
</Group>
);

View File

@@ -294,6 +294,26 @@ export default function SettingsPage() {
Attorney stay editable.
</Alert>
)}
{reviewStatus === "changes_requested" && (
<Alert
color="yellow"
variant="light"
icon={<AlertTriangle size={18} />}
title="Changes requested on your submission"
>
<Stack gap={4}>
{profile.reviewNote && (
<Text size="sm">
<strong>Reviewer note:</strong> {profile.reviewNote}
</Text>
)}
<Text size="sm">
Please update the requested details below and save again to
resubmit for review.
</Text>
</Stack>
</Alert>
)}
{reviewStatus === "rejected" && (
<Alert
color="red"

View File

@@ -106,7 +106,8 @@ function Countdown({
day: "numeric",
hour: "2-digit",
minute: "2-digit",
})}
})}{" "}
EAT
</Text>
{onPay && (
<Button

View File

@@ -4,6 +4,7 @@ import { Upload, Download, AlertCircle, CheckCircle, AlertTriangle } from "lucid
import { useMutation } from "@tanstack/react-query";
import { client } from "@/utils/api";
import { URL_CONSTANTS } from "@/constants/URLS";
import { generateTruckAssignmentTemplate, parseTruckAssignmentFile } from "@/utils/truck-assignment-template";
interface BulkTruckUploadModalProps {
@@ -32,7 +33,7 @@ export function BulkTruckUploadModal({
const uploadMutation = useMutation({
mutationFn: async () => {
const { data } = await client.post(`/bookings/${bookingId}/customer-trucks/bulk`, {
const { data } = await client.post(URL_CONSTANTS.BOOKINGS.CUSTOMER_TRUCKS_BULK(bookingId), {
trucks: parsed,
});
return data;

View File

@@ -68,6 +68,7 @@ function formatPriceUnit(unit: string): string {
const map: Record<string, string> = {
PER_CONTAINER: "per container",
PER_TON: "per ton",
PER_ITEM: "per item",
PER_WAGON: "per wagon",
PER_KM: "per km",
FLAT: "flat",

View File

@@ -10,7 +10,6 @@ import {
Text,
Textarea,
} from "@mantine/core";
import { format } from "date-fns";
import {
Calendar,
CheckCircle2,
@@ -239,7 +238,12 @@ export function Step8Review({
values.destinationYard;
const scheduleLabel = values.scheduledDate
? format(new Date(values.scheduledDate), "EEEE, MMM d, yyyy")
? new Date(values.scheduledDate).toLocaleDateString("en-US", {
weekday: "long",
month: "short",
day: "numeric",
year: "numeric",
})
: "—";
const directionLabel = direction

View File

@@ -94,10 +94,12 @@ export interface CompanyInfoResponse {
company: CompanyResponse;
/**
* Open profile-edit review, if any. `pending` locks the settings page + new
* contract/booking creation; `rejected` surfaces the note for reapply.
* contract/booking creation; `rejected`/`changes_requested` both surface the
* note for reapply — `changes_requested` just means the edit appends to the
* same request instead of starting a fresh one.
*/
review?: {
status: "pending" | "rejected";
status: "pending" | "rejected" | "changes_requested";
note: string | null;
} | null;
}
@@ -106,7 +108,7 @@ export interface CompanyInfoResponse {
export interface ChangeRequestResponse {
id: string;
companyId: string;
status: "pending" | "approved" | "rejected";
status: "pending" | "approved" | "rejected" | "changes_requested";
snapshot: Record<string, any>;
documentFileIds: string[];
note: string | null;

View File

@@ -51,10 +51,12 @@ export interface ProfileResponse {
profileId: string;
/**
* Open profile-edit review. `pending` → the settings page is read-only until an
* admin decides; `rejected` → the note explains why and the forms prefill the
* declined values so the customer can amend & resubmit.
* admin decides; `rejected`/`changes_requested` → the note explains why and
* the forms prefill the declined values so the customer can amend & resubmit
* (`changes_requested` appends that edit to this same request instead of
* starting a fresh one).
*/
reviewStatus?: "pending" | "rejected" | null;
reviewStatus?: "pending" | "rejected" | "changes_requested" | null;
reviewNote?: string | null;
pendingChanges?: Record<string, any> | null;
}