mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
add profile creation flow with license upload in NewContractPage
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
FileInput,
|
||||||
Group,
|
Group,
|
||||||
Modal,
|
Modal,
|
||||||
Stack,
|
Stack,
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Send,
|
Send,
|
||||||
|
Upload,
|
||||||
XCircle,
|
XCircle,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
@@ -36,15 +38,17 @@ import {
|
|||||||
contractFormSchema,
|
contractFormSchema,
|
||||||
contractStepFields,
|
contractStepFields,
|
||||||
initialContractFormValues,
|
initialContractFormValues,
|
||||||
|
OPERATION_TYPES,
|
||||||
type ContractFormValues,
|
type ContractFormValues,
|
||||||
type OperationType,
|
type OperationType,
|
||||||
} from "./new-contract-form/schema";
|
} from "./new-contract-form/schema";
|
||||||
import {
|
import {
|
||||||
allowedOperationsForProfiles,
|
|
||||||
getRouteDirection,
|
getRouteDirection,
|
||||||
operationToProfileType,
|
operationToProfileType,
|
||||||
operationToTradeDirection,
|
operationToTradeDirection,
|
||||||
} from "./new-contract-form/helpers";
|
} 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 { StepIndicator } from "./new-contract-form/StepIndicator";
|
||||||
import {
|
import {
|
||||||
clearContractDraft,
|
clearContractDraft,
|
||||||
@@ -240,19 +244,92 @@ export default function NewContractPage() {
|
|||||||
[auth.company],
|
[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[]>(
|
const allowedOperations = useMemo<OperationType[]>(
|
||||||
() => allowedOperationsForProfiles(profileTypes),
|
() => [...OPERATION_TYPES],
|
||||||
[profileTypes],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 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) => {
|
const handleOperationSelect = (op: OperationType) => {
|
||||||
|
// Intercity (domestic) runs on any existing customer profile — no switch.
|
||||||
if (op === "intercity") return;
|
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) {
|
if (auth.activeProfileType !== target) {
|
||||||
void auth.switchMode(target as never);
|
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 onboardingDocs = useMemo(() => {
|
||||||
const profiles = auth.company?.company?.companyProfiles ?? [];
|
const profiles = auth.company?.company?.companyProfiles ?? [];
|
||||||
const active =
|
const active =
|
||||||
@@ -811,6 +888,54 @@ export default function NewContractPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Modal>
|
</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>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,7 +239,7 @@ export function Step3CargoScope({
|
|||||||
label={`${size} cap (containers)`}
|
label={`${size} cap (containers)`}
|
||||||
placeholder="0 = unlimited"
|
placeholder="0 = unlimited"
|
||||||
min={0}
|
min={0}
|
||||||
value={field.value ?? 0}
|
value={Number(field.value ?? 0)}
|
||||||
onChange={(v) => field.onChange(Number(v) || 0)}
|
onChange={(v) => field.onChange(Number(v) || 0)}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
@@ -258,7 +258,7 @@ export function Step3CargoScope({
|
|||||||
label="Total cap (tons / items)"
|
label="Total cap (tons / items)"
|
||||||
placeholder="0 = unlimited"
|
placeholder="0 = unlimited"
|
||||||
min={0}
|
min={0}
|
||||||
value={field.value ?? 0}
|
value={Number(field.value ?? 0)}
|
||||||
onChange={(v) => field.onChange(Number(v) || 0)}
|
onChange={(v) => field.onChange(Number(v) || 0)}
|
||||||
radius={10}
|
radius={10}
|
||||||
styles={fieldStyles}
|
styles={fieldStyles}
|
||||||
|
|||||||
Reference in New Issue
Block a user