feat: add shipping line companies management

- Implement ShippingLineCompaniesService for registering and managing shipping line companies.
- Create ResendActivationAction component for resending activation links to shipping lines.
- Develop ShippingLineCompaniesPage for listing and registering shipping lines with validation.
- Introduce shippingLineCompanies.service for API interactions related to shipping lines.
- Define types for shipping line companies, including registration and pagination.
- Add placeholder pages for shipping line portal, including home, bookings, help, invoices, and settings.
This commit is contained in:
marshalyordanos
2026-08-13 08:54:20 +03:00
parent e37f1e0807
commit 9aae132dd4
42 changed files with 2463 additions and 117 deletions

View File

@@ -107,23 +107,20 @@ function getActivePage(
activePath: string,
): { label: string } | null {
const path = activePath.toLowerCase();
for (const item of items) {
if (
path === item.href.toLowerCase() ||
path.startsWith(item.href.toLowerCase() + "/")
) {
return { label: item.label };
}
if (item.children) {
const childMatch = item.children.find(
(c) =>
path === c.href.toLowerCase() ||
path.startsWith(c.href.toLowerCase() + "/"),
);
if (childMatch) return { label: childMatch.label };
}
}
return null;
// Longest match wins, for the same reason as the sidebar's isItemActive:
// a nested href like "/shipping-line/bookings" must beat its "/shipping-line"
// parent, which a first-match-wins scan would report as "Home".
const best = items
.flatMap((item) => [item, ...(item.children ?? [])])
.filter(
(item) =>
path === item.href.toLowerCase() ||
path.startsWith(item.href.toLowerCase() + "/"),
)
.sort((a, b) => b.href.length - a.href.length)[0];
return best ? { label: best.label } : null;
}
const navClassNames = (active: boolean) => {
@@ -262,9 +259,19 @@ export function AppLayout({
const serviceLabel = (m: ServiceType) => PROFILE_TYPE_LABELS[m] ?? m;
const isSuspendedAppeal = reapplyStatus === "suspended";
// Longest matching href wins. A plain prefix test would light up every
// ancestor: with a "/shipping-line" home item alongside "/shipping-line/
// bookings", Home would stay highlighted on every page beneath it. Exact
// matches still win outright, so customer routes are unaffected — their
// sidebar hrefs are siblings, never nested inside one another.
const bestMatchHref = sidebarItems
.flatMap((item) => [item, ...(item.children ?? [])])
.map((item) => item.href.toLowerCase())
.filter((href) => activePath === href || activePath.startsWith(href + "/"))
.sort((a, b) => b.length - a.length)[0];
const isItemActive = (item: SidebarItem) =>
activePath === item.href.toLowerCase() ||
activePath.startsWith(item.href.toLowerCase() + "/");
bestMatchHref === item.href.toLowerCase();
return (
<AppShell
@@ -371,7 +378,9 @@ export function AppLayout({
onClick={() =>
openServiceModal(p.type as ServiceType, p)
}
leftSection={<RefreshCw size={15} strokeWidth={1.8} />}
leftSection={
<RefreshCw size={15} strokeWidth={1.8} />
}
>
{serviceLabel(p.type as ServiceType)}
</Menu.Item>
@@ -893,7 +902,9 @@ export function AppLayout({
</Alert>
)}
<FileInput
label={reapplyId ? "Business license (optional)" : "Business license"}
label={
reapplyId ? "Business license (optional)" : "Business license"
}
multiple
clearable
accept="application/pdf,image/png,image/jpeg"

View File

@@ -162,7 +162,15 @@ export default function OnboardingResumeBanner({
* Self-hides when there's nothing outstanding.
*/
export function AccountReviewBanner() {
const { company, companyStatus, reviewStatus, reviewNote } = useAuth();
const { company, companyStatus, reviewStatus, reviewNote, isShippingLine } =
useAuth();
// Shipping lines have no company approval, no operational profiles and no
// profile-edit review — every branch below is about customer state they do
// not have. Bail explicitly rather than relying on each check happening to
// fall through.
if (isShippingLine) return null;
const profiles = company?.company?.companyProfiles ?? [];
const pending = profiles.filter((p) => p.status === "pending");
const approved = profiles.filter((p) => p.status === "active");