From 6be461bb2f8fd2d856be771c122d3c45bb101f3b Mon Sep 17 00:00:00 2001 From: Marshal Date: Mon, 29 Jun 2026 10:25:06 +0000 Subject: [PATCH] add profile creation flow with license upload in NewContractPage --- .../src/pages/contracts/NewContractPage.tsx | 133 +++++++++++++++++- .../new-contract-form/step3-cargo-scope.tsx | 4 +- 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx index 8845b13cd..5a623e296 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx @@ -11,6 +11,7 @@ import { Alert, Box, Button, + FileInput, Group, Modal, Stack, @@ -24,6 +25,7 @@ import { ChevronLeft, ChevronRight, Send, + Upload, XCircle, } from "lucide-react"; import { useMemo, useState } from "react"; @@ -36,15 +38,17 @@ import { contractFormSchema, contractStepFields, initialContractFormValues, + OPERATION_TYPES, type ContractFormValues, type OperationType, } from "./new-contract-form/schema"; import { - allowedOperationsForProfiles, getRouteDirection, operationToProfileType, operationToTradeDirection, } from "./new-contract-form/helpers"; +import { PROFILE_TYPE_LABELS } from "@/constants/profileMode"; +import type { ProfileTypeValue } from "@/services/companies.service"; import { StepIndicator } from "./new-contract-form/StepIndicator"; import { clearContractDraft, @@ -240,19 +244,92 @@ export default function NewContractPage() { [auth.company], ); + // All operation types are always selectable. Picking one the company has no + // profile for prompts a license upload that creates the profile on the fly + // (mirrors the header "Add service" flow). const allowedOperations = useMemo( - () => allowedOperationsForProfiles(profileTypes), - [profileTypes], + () => [...OPERATION_TYPES], + [], ); + // Create-profile modal state (license upload → createProfileAndSwitch). + const [createTarget, setCreateTarget] = useState( + null, + ); + const [pendingOperation, setPendingOperation] = + useState(null); + const [licenseFiles, setLicenseFiles] = useState([]); + const [createError, setCreateError] = useState(null); + + const createProfileMutation = useMutation({ + mutationFn: async ({ + type, + files, + }: { + type: ProfileTypeValue; + files: File[]; + }) => { + const res = await auth.createProfileAndSwitch(type, files); + if (!res.success) { + throw new Error(res.error?.message ?? "Failed to create profile"); + } + }, + onSuccess: () => { + // The operation was already set on the form when the modal opened. + setCreateTarget(null); + setPendingOperation(null); + setLicenseFiles([]); + setCreateError(null); + }, + onError: (err) => { + setCreateError( + err instanceof Error ? err.message : "Failed to create profile", + ); + }, + }); + const handleOperationSelect = (op: OperationType) => { + // Intercity (domestic) runs on any existing customer profile — no switch. if (op === "intercity") return; - const target = operationToProfileType(op, profileTypes); + const target = operationToProfileType(op, profileTypes) as ProfileTypeValue; + const hasProfile = profileTypes.includes(target); + if (!hasProfile) { + // No matching profile — collect a license and create one. + setPendingOperation(op); + setCreateTarget(target); + setLicenseFiles([]); + setCreateError(null); + return; + } if (auth.activeProfileType !== target) { void auth.switchMode(target as never); } }; + const handleCreateProfileConfirm = () => { + if (!createTarget) return; + if (licenseFiles.length === 0) { + setCreateError("Please upload at least one business license file."); + return; + } + createProfileMutation.mutate({ type: createTarget, files: licenseFiles }); + }; + + const handleCreateProfileCancel = () => { + // Roll back the operation selection that triggered the modal. + if (pendingOperation) { + form.setValue("operationType", undefined as never, { shouldDirty: true }); + } + setCreateTarget(null); + setPendingOperation(null); + setLicenseFiles([]); + setCreateError(null); + }; + + const createTargetLabel = createTarget + ? (PROFILE_TYPE_LABELS[createTarget] ?? createTarget) + : ""; + const onboardingDocs = useMemo(() => { const profiles = auth.company?.company?.companyProfiles ?? []; const active = @@ -811,6 +888,54 @@ export default function NewContractPage() { )} + + {/* Create-profile modal — opens when the chosen operation type has no + matching company profile yet. Collects a license, creates the profile, + and switches to it (mirrors the header "Add service" flow). */} + { + if (!createProfileMutation.isPending) handleCreateProfileCancel(); + }} + title={`Set up your ${createTargetLabel} profile`} + centered + radius="lg" + > + + + You don't have a {createTargetLabel.toLowerCase()} profile yet. Add + your business license to create one and continue this contract as{" "} + {createTargetLabel.toLowerCase()}. + + } + placeholder="Select license file(s)" + value={licenseFiles} + onChange={(files) => setLicenseFiles(files ?? [])} + error={createError ?? undefined} + /> + + + + + + ); } diff --git a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step3-cargo-scope.tsx b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step3-cargo-scope.tsx index ebf6b0bd8..9cda1985d 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step3-cargo-scope.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step3-cargo-scope.tsx @@ -239,7 +239,7 @@ export function Step3CargoScope({ label={`${size} cap (containers)`} placeholder="0 = unlimited" min={0} - value={field.value ?? 0} + value={Number(field.value ?? 0)} onChange={(v) => field.onChange(Number(v) || 0)} radius={10} styles={fieldStyles} @@ -258,7 +258,7 @@ export function Step3CargoScope({ label="Total cap (tons / items)" placeholder="0 = unlimited" min={0} - value={field.value ?? 0} + value={Number(field.value ?? 0)} onChange={(v) => field.onChange(Number(v) || 0)} radius={10} styles={fieldStyles}