diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx
index 15137cb26..3a32f2b78 100644
--- a/apps/edr-freight-web/portal/src/App.tsx
+++ b/apps/edr-freight-web/portal/src/App.tsx
@@ -7,7 +7,6 @@ import {
MapPin,
Receipt,
Settings,
- Sparkles,
} from "lucide-react";
import { useDisclosure } from "@mantine/hooks";
import { useEffect, useRef } from "react";
@@ -21,6 +20,7 @@ import {
} from "react-router-dom";
import useAuth from "./hooks/useAuth";
+import OnboardingResumeBanner from "./components/onboarding/OnboardingResumeBanner";
import OnboardingWizardDialog from "./components/onboarding/OnboardingWizardDialog";
import EDRFreightLandingPage from "./pages/EDRFreightLandingPage";
import MyPortalPage from "./pages/MyPortalPage";
@@ -155,28 +155,6 @@ function OnboardingGate() {
);
}
-/** Slim sticky prompt shown on allowed pages after the wizard is dismissed. */
-function OnboardingResumeBanner({ onResume }: { onResume: () => void }) {
- return (
-
-
-
-
- Finish setting up your company to unlock bookings, tracking and
- billing.
-
-
-
-
- );
-}
-
/** Keeps authenticated users off the login/signup pages. */
function RedirectIfAuthed() {
const { isPending, isAuthenticated } = useAuth();
diff --git a/apps/edr-freight-web/portal/src/components/onboarding/OnboardingResumeBanner.tsx b/apps/edr-freight-web/portal/src/components/onboarding/OnboardingResumeBanner.tsx
new file mode 100644
index 000000000..e0df7346f
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/components/onboarding/OnboardingResumeBanner.tsx
@@ -0,0 +1,145 @@
+import { useQuery } from "@tanstack/react-query";
+import { ArrowRight } from "lucide-react";
+import { api } from "@/services/api";
+import {
+ getProfileCompletion,
+ type ProfileCompletion,
+} from "@/utils/profileCompletion";
+
+interface OnboardingResumeBannerProps {
+ /** Re-opens the onboarding wizard. */
+ onResume: () => void;
+}
+
+interface BannerCopy {
+ title: string;
+ subtitle: string;
+ cta: string;
+}
+
+/** Picks wording based on how far through setup the user actually is. */
+function getCopy(
+ completion: ProfileCompletion,
+ pct: number,
+ isPending: boolean,
+): BannerCopy {
+ // Until the profile loads, or before anything is filled in, treat it as a
+ // fresh start rather than guessing progress.
+ if (isPending || completion.completed === 0) {
+ return {
+ title: "Set up your company profile",
+ subtitle: "Unlock bookings, tracking and billing — it only takes a minute.",
+ cta: "Start onboarding",
+ };
+ }
+
+ const remaining = completion.total - completion.completed;
+ if (remaining <= 2) {
+ return {
+ title: `Almost done — you're ${pct}% set up`,
+ subtitle: `Just ${remaining} more ${
+ remaining === 1 ? "detail" : "details"
+ } to unlock bookings, tracking and billing.`,
+ cta: "Finish onboarding",
+ };
+ }
+
+ return {
+ title: `You're ${pct}% set up`,
+ subtitle: `${completion.completed} of ${completion.total} details added — finish to unlock bookings, tracking and billing.`,
+ cta: "Continue onboarding",
+ };
+}
+
+/** Circular percentage meter that reads at a glance against the dark banner. */
+function ProgressRing({ pct }: { pct: number }) {
+ const size = 56;
+ const stroke = 5;
+ const r = (size - stroke) / 2;
+ const circumference = 2 * Math.PI * r;
+ const offset = circumference * (1 - pct / 100);
+
+ return (
+
+
+ {pct}%
+
+ );
+}
+
+/**
+ * Prominent banner shown on onboarding-allowed pages after the wizard is
+ * dismissed. It reads the company profile directly so it stays aware of real
+ * progress: a percentage ring and the copy adapt as fields get filled, and the
+ * whole banner disappears once every required detail is complete.
+ */
+export default function OnboardingResumeBanner({
+ onResume,
+}: OnboardingResumeBannerProps) {
+ const profileQuery = useQuery(
+ api.companies.getProfile.queryOptions({ retry: false }),
+ );
+
+ const completion = getProfileCompletion(profileQuery.data);
+
+ // Reliably step aside once the user has genuinely finished onboarding.
+ if (!profileQuery.isPending && completion.isComplete) return null;
+
+ const pct = Math.round((completion.completed / completion.total) * 100);
+ const { title, subtitle, cta } = getCopy(
+ completion,
+ pct,
+ profileQuery.isPending,
+ );
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {title}
+
+
+ {subtitle}
+
+
+
+
+
+ );
+}
diff --git a/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx b/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx
index 3628d7403..b25f6f12e 100644
--- a/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx
+++ b/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx
@@ -355,7 +355,7 @@ export default function OnboardingWizardDialog({
withCloseButton
closeOnClickOutside={false}
closeOnEscape
- size={1040}
+ size={720}
radius="lg"
padding="xl"
centered
diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx
index 0a4dcb6ce..13ab980b8 100644
--- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx
+++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/MyPortalPage.tsx
@@ -7,7 +7,6 @@ import {
HelloSection,
InvoicesSection,
RecentActivitySection,
- SetupPrompt,
ShipmentsSection,
StatsSection,
} from "./components";
@@ -16,7 +15,6 @@ import { useMyPortalData } from "./hooks";
export default function MyPortalPage() {
const navigate = useNavigate();
const {
- customer,
bookingsQuery,
dashboardQuery,
allBookings,
@@ -40,8 +38,6 @@ export default function MyPortalPage() {
-
-
!profile[field]);
-}
-
-interface SetupPromptProps {
- show: boolean;
-}
-
-export const SetupPrompt = memo(function SetupPrompt({ show }: SetupPromptProps) {
- const profileQuery = useQuery(
- api.companies.getProfile.queryOptions({ retry: false }),
- );
-
- const incomplete = !profileQuery.isPending && isProfileIncomplete(profileQuery.data);
-
- if (!show && !incomplete) return null;
-
- return (
-
-
-
-
- {incomplete && }
-
- {incomplete ? "Complete Your Profile" : "Setup your Company Profile"}
-
-
-
- {incomplete
- ? "Your company profile is incomplete. Fill in the missing details to unlock all features."
- : "Complete your company information to unlock all features and start booking shipments."}
-
-
-
-
- {incomplete ? "Complete Profile" : "Complete Setup"}
-
-
-
-
-
-
-
-
-
-
- );
-});
diff --git a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/index.ts b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/index.ts
index de3ddb448..4bf94f45f 100644
--- a/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/index.ts
+++ b/apps/edr-freight-web/portal/src/pages/MyPortalPage/components/index.ts
@@ -6,8 +6,8 @@ export { FreightVolumeSection } from "./FreightVolumeSection";
export { HelloSection } from "./HelloSection";
export { InvoicesSection } from "./InvoicesSection";
export { RecentActivitySection } from "./RecentActivitySection";
-export { SetupPrompt } from "./SetupPrompt";
export { ShipmentsSection } from "./ShipmentsSection";
export { StatKpi } from "./StatKpi";
export { StatsSection } from "./StatsSection";
export { Stepper } from "./Stepper";
+
diff --git a/apps/edr-freight-web/portal/src/utils/profileCompletion.ts b/apps/edr-freight-web/portal/src/utils/profileCompletion.ts
new file mode 100644
index 000000000..5e95af40d
--- /dev/null
+++ b/apps/edr-freight-web/portal/src/utils/profileCompletion.ts
@@ -0,0 +1,56 @@
+import type { ProfileResponse } from "@/types/profile";
+
+/**
+ * Company-profile fields that must be filled before onboarding is considered
+ * finished. Shared between the portal SetupPrompt and the onboarding banner so
+ * both agree on what "done" means.
+ */
+export const REQUIRED_PROFILE_FIELDS: (keyof ProfileResponse)[] = [
+ "companyEmail",
+ "companyPhone",
+ "companyAddress",
+ "fanNumber",
+ "contactPersonName",
+ "contactPersonPhone",
+ "generalManagerName",
+ "generalManagerEmail",
+ "generalManagerPhone",
+];
+
+export interface ProfileCompletion {
+ /** Number of required fields that are filled in. */
+ completed: number;
+ /** Total number of required fields. */
+ total: number;
+ /** Required fields still missing a value. */
+ missing: (keyof ProfileResponse)[];
+ /** True when every required field is filled. */
+ isComplete: boolean;
+}
+
+/** Breaks a profile down into how much of the required setup is complete. */
+export function getProfileCompletion(
+ profile?: ProfileResponse | null,
+): ProfileCompletion {
+ const total = REQUIRED_PROFILE_FIELDS.length;
+ if (!profile) {
+ return {
+ completed: 0,
+ total,
+ missing: [...REQUIRED_PROFILE_FIELDS],
+ isComplete: false,
+ };
+ }
+ const missing = REQUIRED_PROFILE_FIELDS.filter((field) => !profile[field]);
+ return {
+ completed: total - missing.length,
+ total,
+ missing,
+ isComplete: missing.length === 0,
+ };
+}
+
+/** Convenience predicate kept for existing call sites. */
+export function isProfileIncomplete(profile?: ProfileResponse | null): boolean {
+ return !getProfileCompletion(profile).isComplete;
+}