diff --git a/apps/edr-freight-api/src/modules/companies/companies.controller.ts b/apps/edr-freight-api/src/modules/companies/companies.controller.ts index 1dd28ca9c..78f8db410 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -456,6 +456,36 @@ export class CompaniesController { return this.companiesService.clearGmIdentity(user.id); } + @Post("identity/poa/same-as-owner") + @PortalCustomer() + @ApiOperation({ + summary: + "Declare the Power of Attorney is the company's owner, copying the owner's identity across. " + + "Waives the DARS delegation paper — nobody delegates to themselves. " + + "Refused for an Ethiopian company whose owner is not Fayda-verified yet: its representative must be verified, and there would be nothing proven to copy.", + }) + async setPoaSameAsOwner( + @CurrentUser() user: CurrentIamUser, + ): Promise { + return this.companiesService.setPoaSameAsOwner(user.id, { + email: user.email, + phoneNumber: user.phoneNumber, + }); + } + + @Delete("identity/poa/same-as-owner") + @PortalCustomer() + @ApiOperation({ + summary: + "Undo the Power of Attorney \"same as owner\" declaration and the identity it copied, leaving the representative open to be verified in their own right. " + + "Unlike DELETE identity/fayda/poa this is allowed for a freight forwarder — it is how they change who represents them — and leaves the delegation paper on file.", + }) + async clearPoaSameAsOwner( + @CurrentUser() user: CurrentIamUser, + ): Promise { + return this.companiesService.clearPoaSameAsOwner(user.id); + } + @Delete("identity/fayda/poa") @PortalCustomer() @ApiOperation({ diff --git a/apps/edr-freight-api/src/modules/companies/companies.fayda-identity.spec.ts b/apps/edr-freight-api/src/modules/companies/companies.fayda-identity.spec.ts index 4a53915ec..fe40cf08f 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.fayda-identity.spec.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.fayda-identity.spec.ts @@ -229,8 +229,10 @@ describe("Fayda identity verification binds a person to the company", () => { expect(state.owner.verified).toBe(true); }); - it("refuses to make one identity both owner and PoA", async () => { - const { service } = makeService({ + it("lets one identity be both owner and PoA", async () => { + // An owner who represents their own company is the ordinary small-business + // case, not a conflict — the same answer the GM has always been allowed. + const { service, ctx } = makeService({ attributes: { ownerFaydaSub: "same-person" }, verification: { purpose: "VERIFY", @@ -240,13 +242,14 @@ describe("Fayda identity verification binds a person to the company", () => { }, }); - await expect( - service.completeIdentityVerification("user-1", { - subject: "poa", - code: "c", - state: "s", - }), - ).rejects.toBeInstanceOf(BadRequestException); + const state = await service.completeIdentityVerification("user-1", { + subject: "poa", + code: "c", + state: "s", + }); + + expect(state.poa.verified).toBe(true); + expect(ctx.attributes.poaFaydaSub).toBe("same-person"); }); it("stages an owner re-verification for review on an approved company", async () => { @@ -701,9 +704,8 @@ describe("Ethiopian companies verify with Fayda; foreign companies verify identi }); it("lets the GM verify as the same human as the owner", async () => { - // The owner/PoA collision check exists because self-delegation is not - // delegation. It must not fire here: the GM being the owner is a supported - // answer, so verifying with the owner's own Fayda sub has to succeed. + // One human in every role is the ordinary small-business shape, so + // verifying with the owner's own Fayda sub has to succeed. const { service, ctx } = makeService({ attributes: { ...OWNER_VERIFIED }, verification: { @@ -727,25 +729,42 @@ describe("Ethiopian companies verify with Fayda; foreign companies verify identi expect(ctx.attributes.generalManagerName).toBe("Abebe Bikila"); }); - it("still refuses a PoA who is the owner", async () => { - // The GM exemption above must not have widened into the PoA. - const { service } = makeService({ - attributes: { ...OWNER_VERIFIED }, - verification: { - purpose: "VERIFY", - verified: true, - sub: "owner-sub", - fullName: "Abebe Bikila", - }, + it("declares the PoA is the owner, copying the verified identity across", async () => { + const { service, ctx } = makeService({ attributes: { ...OWNER_VERIFIED } }); + + const state = await service.setPoaSameAsOwner("user-1"); + + expect(state.poaSameAsOwner).toBe(true); + expect(state.poa.verified).toBe(true); + expect(ctx.attributes.poaFaydaSub).toBe(OWNER_VERIFIED.ownerFaydaSub); + expect(ctx.attributes.poaName).toBe(OWNER_VERIFIED.ownerName); + }); + + it("refuses to declare the PoA is the owner while an Ethiopian owner is unverified", async () => { + // Its representative must be Fayda-verified, so a declaration here would + // record one that could never satisfy the gate. + const { service } = makeService({ attributes: {} }); + + await expect(service.setPoaSameAsOwner("user-1")).rejects.toBeInstanceOf( + BadRequestException, + ); + }); + + it("undoes the PoA \"same as owner\" declaration without touching a real verification", async () => { + const { service, ctx } = makeService({ + attributes: { ...OWNER_VERIFIED, ...POA_VERIFIED }, }); - await expect( - service.completeIdentityVerification("user-1", { - subject: "poa", - code: "c", - state: "s", - }), - ).rejects.toBeInstanceOf(BadRequestException); + // No declaration in place: the verified representative must survive. + await service.clearPoaSameAsOwner("user-1"); + expect(ctx.attributes.poaFaydaSub).toBe(POA_VERIFIED.poaFaydaSub); + + await service.setPoaSameAsOwner("user-1"); + const state = await service.clearPoaSameAsOwner("user-1"); + + expect(state.poaSameAsOwner).toBe(false); + expect(state.poa.verified).toBe(false); + expect(ctx.attributes.poaFaydaSub).toBeNull(); }); it("reports a pre-existing typed GM as unverified rather than blank", async () => { diff --git a/apps/edr-freight-api/src/modules/companies/companies.poa-delegation.spec.ts b/apps/edr-freight-api/src/modules/companies/companies.poa-delegation.spec.ts index 365bd4639..b85344e8e 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.poa-delegation.spec.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.poa-delegation.spec.ts @@ -174,6 +174,31 @@ describe("PoA delegation paper is enforced wherever PoA state changes", () => { ).resolves.toBeDefined(); }); + it("waives the paper when the owner represents the company themselves", async () => { + // Nobody delegates to themselves, so a self-declared PoA owes no DARS + // paper — the representative's own details are still required. + const { service } = makeService({ + attributes: { ...VERIFIED_IDENTITIES, poaSameAsOwner: true }, + }); + + await expect( + service.updateProfile("user-1", POA as never), + ).resolves.toBeDefined(); + }); + + it("grants the forwarder role to a self-represented company with no paper", async () => { + const { service } = makeService({ + attributes: { ...VERIFIED_IDENTITIES, ...POA, poaSameAsOwner: true }, + }); + + await expect( + service.createCompanyProfileForUser( + "user-1", + ProfileType.freightForwarder, + ), + ).resolves.toBeDefined(); + }); + it("rejects a paper the reviewer sent back for correction", async () => { const { service } = makeService({ files: [paper("change_requested")] }); 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 72dcfdded..0ed91c9e3 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -133,12 +133,6 @@ const IDENTITY_PREFIX: Record = { gm: "gm", }; -const IDENTITY_LABEL: Record = { - owner: "owner", - poa: "Power of Attorney", - gm: "General Manager", -}; - /** * Typed GM columns a GM verification also writes. Three notifier services mail * `company.generalManagerEmail` directly, so leaving these behind would mean a @@ -2046,7 +2040,13 @@ export class CompaniesService { (company.attributes?.[k] as string | undefined)?.trim(), ); const delegation = await this.getPoaDelegationState(company.id); - const delegationDue = poaRequired || poaProvided; + // "There is a representative" and "a paper is owed for them" used to be the + // same condition. They part company once the owner represents the company + // themselves: the representative's details are still required, but nobody + // delegates to themselves, so no DARS paper is due (`assertPoaDelegationSatisfied` + // returns on the same flag — the two must agree). + const poaDue = poaRequired || poaProvided; + const delegationDue = poaDue && !identity.poaSameAsOwner; // The representative's details normally arrive from their Fayda // verification — but Fayda's email and phone claims are optional and // routinely come back empty, and the PoA step renders an input for whatever @@ -2055,7 +2055,7 @@ export class CompaniesService { // nothing here let a freight forwarder finish onboarding with a // representative the API's own `REQUIRED_POA_FIELDS` calls incomplete, then // 400'd their next PoA edit for it. - const missingPoaFields = delegationDue + const missingPoaFields = poaDue ? REQUIRED_POA_FIELDS.filter( (f) => !(company.attributes?.[f.key] as string | undefined)?.trim(), ) @@ -2115,7 +2115,8 @@ export class CompaniesService { // The delegation paper plus the representative's own required details — // `completed` below subtracts every one of those it is still missing, so // leaving them out of the total would make the bar understate progress. - const poaItemCount = delegationDue ? 1 + REQUIRED_POA_FIELDS.length : 0; + const poaItemCount = + (poaDue ? REQUIRED_POA_FIELDS.length : 0) + (delegationDue ? 1 : 0); // One item per identity credential the company has to prove: the owner // always (Fayda for Ethiopian, passport for foreign), plus the PoA once // there is one — Fayda for an Ethiopian company, a named representative @@ -2127,11 +2128,10 @@ export class CompaniesService { const ownerCredentialProven = identity.faydaRequired ? identity.owner.verified : Boolean(identity.owner.passportNumber); - const identityItemCount = - (ownerCredentialDue ? 1 : 0) + (delegationDue ? 1 : 0); + const identityItemCount = (ownerCredentialDue ? 1 : 0) + (poaDue ? 1 : 0); const missingIdentityCount = (ownerCredentialDue && !ownerCredentialProven ? 1 : 0) + - (delegationDue && !poaProven ? 1 : 0); + (poaDue && !poaProven ? 1 : 0); const total = requiredInfo.length + requiredDocCount + @@ -2159,6 +2159,7 @@ export class CompaniesService { poa: { required: poaRequired, provided: poaProvided, + delegationLetterRequired: delegationDue, delegationLetterUploaded: delegation.onFile, delegationLetterFlagged: delegation.flagged, missingFields: missingPoaFields, @@ -2707,6 +2708,12 @@ export class CompaniesService { } } + // Nobody delegates to themselves: an owner representing their own company + // has no delegation to evidence, so the DARS paper is not owed. The + // representative's own details are still required above — a forwarder's + // counterparties need someone to contact either way. + if (attributes?.poaSameAsOwner) return; + const { onFile, flagged } = await this.getPoaDelegationState( companyId, opts.ignoreFileIds, @@ -2783,21 +2790,11 @@ export class CompaniesService { ); } - // The owner delegating power of attorney to themselves is not a - // delegation — it would let one identity satisfy both halves of the check. - // Only owner/PoA collide this way: the GM is very often the owner, and - // saying so is a supported answer rather than a conflict, so it is left out - // of this check entirely. - if (dto.subject === "owner" || dto.subject === "poa") { - const other: IdentitySubject = dto.subject === "poa" ? "owner" : "poa"; - const otherSub = - company.attributes?.[`${IDENTITY_PREFIX[other]}FaydaSub`]; - if (otherSub && otherSub === result.sub) { - throw new BadRequestException( - `This identity is already registered as the company's ${IDENTITY_LABEL[other]}. The Power of Attorney must be a different person from the owner.`, - ); - } - } + // An owner who is also the company's representative is a supported answer, + // not a conflict — the same way the GM is very often the owner. Small + // companies routinely have one human in all three roles, and the portal's + // "same as owner" cards exist precisely so they can say so. No identity + // here is refused for colliding with another. const now = new Date().toISOString(); @@ -2837,6 +2834,12 @@ export class CompaniesService { // mail `generalManagerEmail`), and clears any earlier "same as owner" // declaration — verifying in their own right is the GM answering for // themselves. + // Verifying the representative in their own right answers the question the + // "same as owner" declaration answered, so the declaration goes. + if (dto.subject === "poa") { + identity.poaSameAsOwner = false; + } + if (dto.subject === "gm") { identity.gmSameAsOwner = false; if (result.fullName) identity.generalManagerName = result.fullName; @@ -2953,6 +2956,118 @@ export class CompaniesService { return this.getCompanyIdentityState(updated); } + /** + * Declare that the company's Power of Attorney is its owner. + * + * An owner representing their own company is the ordinary case for a small + * business, so this is a supported answer rather than the conflict it used to + * be refused as. Two shapes, matching {@link setGmSameAsOwner}: + * + * - A Fayda-verified owner is a proven identity, so it is copied outright — + * the representative inherits the verification instead of the same human + * being sent through Fayda a second time. + * - A foreign company's owner is backed by a typed passport, so there is + * nothing proven to copy. The declaration is still recorded (it is what + * waives the DARS paper) and whatever owner details exist come across; the + * portal types the rest, which `poaProven` accepts for a foreign company. + * + * Refused for an Ethiopian company whose owner is not verified yet: Fayda is + * mandatory for its representative, so a declaration there would record a + * representative that could never satisfy the gate. + */ + async setPoaSameAsOwner( + userId: string, + /** Same fallback as {@link completeIdentityVerification} — an owner whose + * Fayda claims carried no email/phone has none stored, and copying blanks + * onto a freight forwarder's PoA would block the submit on + * `REQUIRED_POA_FIELDS`. */ + account?: { email?: string; phoneNumber?: string }, + ): Promise { + const { company } = await this.getCompanyInfoByUserId(userId); + const attrs = company.attributes ?? {}; + const state = buildCompanyIdentityState(company); + const ownerSub = attrs.ownerFaydaSub as string | undefined; + + if (state.faydaRequired && !ownerSub) { + throw new BadRequestException( + "Verify the company owner with Fayda first — there is no proven identity to reuse yet.", + ); + } + + const ownerEmail = (attrs.ownerEmail as string | undefined) || account?.email; + const ownerPhone = + (attrs.ownerPhone as string | undefined) || account?.phoneNumber; + + // Only non-blank values are copied: a blank here would overwrite something + // the portal typed for a foreign company, whose owner has no verified + // claims to draw on. + const copied: Record = { poaSameAsOwner: true }; + const copy = (key: string, value: unknown) => { + if (value !== null && value !== undefined && value !== "") + copied[key] = value; + }; + copy("poaName", attrs.ownerName); + copy("poaEmail", ownerEmail); + copy("poaPhone", ownerPhone ? normalizeE164(ownerPhone) : undefined); + copy("poaAddress", attrs.ownerAddress); + + if (ownerSub) { + copied.poaFaydaSub = ownerSub; + copied.poaFaydaVerifiedAt = + attrs.ownerFaydaVerifiedAt ?? new Date().toISOString(); + copy("poaBirthdate", attrs.ownerBirthdate); + copy("poaGender", attrs.ownerGender); + } + + const updated = await this.companiesRepo.update(company.id, { + attributes: { ...attrs, ...copied }, + }); + if (!updated) + throw new NotFoundException(`Company ${company.id} not found`); + updated.companyProfiles = company.companyProfiles; + return this.getCompanyIdentityState(updated); + } + + /** + * Undo the PoA "same as owner" declaration, clearing the identity it copied + * so a different representative can be verified (or typed, for a foreign + * company). + * + * Separate from {@link removePoaIdentity}, which drops the representative and + * their paper and is refused to a freight forwarder. Undoing a declaration is + * how a forwarder changes its mind about who represents it, so it must stay + * open to them — the submit gate still refuses a forwarder that never names a + * replacement. The delegation paper is left alone for the same reason: the + * company still owes one, now for whoever comes next. + * + * A no-op when no declaration is in place: a stray call must not wipe a + * representative who verified in their own right. + */ + async clearPoaSameAsOwner(userId: string): Promise { + const { company } = await this.getCompanyInfoByUserId(userId); + const attrs = { ...(company.attributes ?? {}) }; + if (!attrs.poaSameAsOwner) return this.getCompanyIdentityState(company); + + attrs.poaSameAsOwner = false; + for (const key of [ + ...POA_ATTRIBUTES, + "poaFaydaSub", + "poaFaydaVerifiedAt", + "poaBirthdate", + "poaGender", + ]) { + attrs[key] = null; + } + + const updated = await this.companiesRepo.update(company.id, { + attributes: attrs, + }); + if (!updated) + throw new NotFoundException(`Company ${company.id} not found`); + updated.companyProfiles = company.companyProfiles; + return this.getCompanyIdentityState(updated); + } + /** * Drop the Power of Attorney entirely — the verified identity, the details it * wrote and the delegation paper together. @@ -2974,7 +3089,7 @@ export class CompaniesService { ); } - const cleared: Record = {}; + const cleared: Record = { poaSameAsOwner: false }; for (const key of [ ...POA_ATTRIBUTES, "poaFaydaSub", diff --git a/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts index ae4a73a77..df39949d4 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts @@ -75,6 +75,12 @@ export class CompanyIdentityStateDto { @ApiProperty({ type: IdentityVerificationStateDto }) poa!: IdentityVerificationStateDto; + @ApiProperty({ + description: + "True when the Power of Attorney is the company's owner, declared through the portal's \"same as owner\" copy. Waives the DARS delegation paper — nobody delegates to themselves.", + }) + poaSameAsOwner!: boolean; + @ApiProperty({ type: IdentityVerificationStateDto, description: @@ -196,6 +202,7 @@ export function buildCompanyIdentityState( const gm = stateFor(attrs, "gm"); const gmSameAsOwner = Boolean(attrs.gmSameAsOwner); + const poaSameAsOwner = Boolean(attrs.poaSameAsOwner); const ownerProven = faydaRequired ? owner.verified @@ -220,6 +227,7 @@ export function buildCompanyIdentityState( passportRequired, owner, poa, + poaSameAsOwner, gm, gmSameAsOwner, complete, diff --git a/apps/edr-freight-api/src/modules/companies/dto/onboarding-requirements-response.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/onboarding-requirements-response.dto.ts index a8d2f24a2..9b69bb15d 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/onboarding-requirements-response.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/onboarding-requirements-response.dto.ts @@ -42,6 +42,12 @@ export interface OnboardingPoaState { required: boolean; /** True once any PoA detail has been entered. */ provided: boolean; + /** + * True when the DARS delegation paper is owed — a PoA exists (or is + * mandatory) and is not the owner themselves. An owner representing their own + * company delegates to nobody, so there is no delegation to evidence. + */ + delegationLetterRequired: boolean; /** True when the DARS delegation paper is stored for the company. */ delegationLetterUploaded: boolean; /** True when a reviewer sent the paper back for correction. */ diff --git a/apps/edr-freight-web/backoffice/index.css b/apps/edr-freight-web/backoffice/index.css index fa9eb3e3d..8e410f026 100644 --- a/apps/edr-freight-web/backoffice/index.css +++ b/apps/edr-freight-web/backoffice/index.css @@ -1,11 +1,13 @@ @import "tailwindcss"; @import "@edr/ui-common/theme.css" layer(theme); -/* The app toggles dark mode by setting the `dark` class on (see - main.tsx / FreightDashboardLayout). Without this, Tailwind v4 compiles - `dark:` utilities to `@media (prefers-color-scheme: dark)` and they follow - the OS setting instead of the in-app toggle. */ -@custom-variant dark (&:where(.dark, .dark *)); +/* The backoffice is light-only — there is no theme toggle and nothing sets the + `dark` class. This override is load-bearing: without it Tailwind v4 compiles + `dark:` utilities to `@media (prefers-color-scheme: dark)`, so every + leftover `dark:` class (the vendored IAM UI is full of them) would activate + on an OS-level dark setting. Binding the variant to a class that is never + rendered keeps those utilities inert no matter what the OS says. */ +@custom-variant dark (&:where(.edr-dark-disabled)); /* Bridge the central Mantine theme into Tailwind. freightMantineTheme (createTheme) is the single source of truth; these just alias its generated diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx index 8e609a26e..00d5e0ddb 100644 --- a/apps/edr-freight-web/backoffice/src/App.tsx +++ b/apps/edr-freight-web/backoffice/src/App.tsx @@ -143,7 +143,6 @@ const DashboardShell = () => { sidebarSections={sidebarSections} activeHref={location.pathname} onNavigate={navigate} - enableThemeToggle userName={displayName} userEmail={user?.email} onLogout={logout} diff --git a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx index 2ca73f10a..0144db306 100644 --- a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx +++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardHeader.tsx @@ -15,9 +15,7 @@ import { FileSignature, Languages, LogOut, - Moon, Search, - Sun, User, } from "lucide-react"; import { type ReactNode } from "react"; @@ -31,13 +29,10 @@ import type { PageMeta } from "./types"; export interface FreightDashboardHeaderProps { pageMeta: PageMeta; headerRight?: ReactNode; - enableThemeToggle?: boolean; userName?: string; userEmail?: string; userInitials?: string; onLogout?: () => void; - theme: "light" | "dark"; - onToggleTheme: () => void; mobileOpened: boolean; onToggleMobile: () => void; /** Hide the mobile burger when the shell has no sidebar to open. */ @@ -51,13 +46,10 @@ const ISLAND = const FreightDashboardHeader = ({ headerRight, - enableThemeToggle = false, userName = "User", userEmail, userInitials, onLogout, - theme, - onToggleTheme, mobileOpened, onToggleMobile, hideSidebarBurger = false, @@ -129,26 +121,6 @@ const FreightDashboardHeader = ({ - {enableThemeToggle && ( - - - {theme === "dark" ? ( - - ) : ( - - )} - - - )} - {/* Avatar pill */} diff --git a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx index f3772cd76..7e6f1c523 100644 --- a/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx +++ b/apps/edr-freight-web/backoffice/src/components/layout/FreightDashboardLayout.tsx @@ -1,26 +1,15 @@ import { AppShell, Box } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; -import { type ReactNode, useEffect, useState } from "react"; +import { type ReactNode } from "react"; import FreightDashboardHeader from "./FreightDashboardHeader"; import FreightSidebar from "./FreightSidebar"; import { getPageMeta } from "./route-meta"; import type { SidebarSection } from "./types"; -type Theme = "light" | "dark"; -const THEME_STORAGE_KEY = "edr-theme"; const HEADER_HEIGHT = 64; const NAVBAR_WIDTH = 280; -function getInitialTheme(): Theme { - if (typeof window === "undefined") return "light"; - const stored = window.localStorage.getItem(THEME_STORAGE_KEY); - if (stored === "dark" || stored === "light") return stored; - return window.matchMedia?.("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; -} - export interface FreightDashboardLayoutProps { sidebarSections: SidebarSection[]; /** Render the shell with no navbar at all (used by GL clearance-only users). */ @@ -28,7 +17,6 @@ export interface FreightDashboardLayoutProps { activeHref?: string; onNavigate?: (href: string) => void; headerRight?: ReactNode; - enableThemeToggle?: boolean; userName?: string; userEmail?: string; userInitials?: string; @@ -42,7 +30,6 @@ const FreightDashboardLayout = ({ activeHref = "", onNavigate, headerRight, - enableThemeToggle = false, userName, userEmail, userInitials, @@ -53,20 +40,6 @@ const FreightDashboardLayout = ({ const [mobileOpened, { toggle: toggleMobile, close: closeMobile }] = useDisclosure(false); - const [theme, setTheme] = useState(() => - enableThemeToggle ? getInitialTheme() : "light", - ); - - useEffect(() => { - if (!enableThemeToggle) return; - const root = document.documentElement; - root.classList.toggle("dark", theme === "dark"); - window.localStorage.setItem(THEME_STORAGE_KEY, theme); - }, [theme, enableThemeToggle]); - - const toggleTheme = () => - setTheme((current) => (current === "dark" ? "light" : "dark")); - const navigate = (href: string) => { closeMobile(); onNavigate?.(href); @@ -93,13 +66,10 @@ const FreightDashboardLayout = ({ { const { pathname } = useLocation(); const { logout } = useAuthUser(); const userDetails = useUser(); - const { isDarkMode, toggleDarkMode } = useDarkMode(); const { config: tenantConfig } = useTenantConfig(); const moduleConfig = resolveModuleConfig(tenantConfig); const isAdminModuleVisible = userDetails?.roles?.some( @@ -66,7 +62,6 @@ export const TopBar = () => { moduleConfig.siteManagement && isAdminModuleVisible, ].filter(Boolean).length; const shouldShowHomePageLink = visibleModuleCount > 1; - const isObjectiveManagementRoute = pathname.startsWith("/objective-management"); const fullName = userDetails?.name?.en || t("user"); const splittedName = fullName.trim().split(" "); const initials = @@ -269,19 +264,6 @@ export const TopBar = () => { )} - -