mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
add profile creation flow with license upload in NewContractPage
This commit is contained in:
@@ -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<OperationType[]>(
|
||||
() => allowedOperationsForProfiles(profileTypes),
|
||||
[profileTypes],
|
||||
() => [...OPERATION_TYPES],
|
||||
[],
|
||||
);
|
||||
|
||||
// Create-profile modal state (license upload → createProfileAndSwitch).
|
||||
const [createTarget, setCreateTarget] = useState<ProfileTypeValue | null>(
|
||||
null,
|
||||
);
|
||||
const [pendingOperation, setPendingOperation] =
|
||||
useState<OperationType | null>(null);
|
||||
const [licenseFiles, setLicenseFiles] = useState<File[]>([]);
|
||||
const [createError, setCreateError] = useState<string | null>(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() {
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
{/* 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). */}
|
||||
<Modal
|
||||
opened={createTarget !== null}
|
||||
onClose={() => {
|
||||
if (!createProfileMutation.isPending) handleCreateProfileCancel();
|
||||
}}
|
||||
title={`Set up your ${createTargetLabel} profile`}
|
||||
centered
|
||||
radius="lg"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
You don't have a {createTargetLabel.toLowerCase()} profile yet. Add
|
||||
your business license to create one and continue this contract as{" "}
|
||||
{createTargetLabel.toLowerCase()}.
|
||||
</Text>
|
||||
<FileInput
|
||||
label="Business license"
|
||||
multiple
|
||||
clearable
|
||||
accept="application/pdf,image/png,image/jpeg"
|
||||
leftSection={<Upload size={16} />}
|
||||
placeholder="Select license file(s)"
|
||||
value={licenseFiles}
|
||||
onChange={(files) => setLicenseFiles(files ?? [])}
|
||||
error={createError ?? undefined}
|
||||
/>
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={handleCreateProfileCancel}
|
||||
disabled={createProfileMutation.isPending}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
onClick={handleCreateProfileConfirm}
|
||||
loading={createProfileMutation.isPending}
|
||||
>
|
||||
Create & continue
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user