mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 01:48:12 +00:00
fix: the issue in the sheets file
This commit is contained in:
@@ -217,7 +217,7 @@ const sidebarItems: SidebarItem[] = [
|
||||
const App = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user, company, companyType, createProfileAndSwitch, isAuthenticated } =
|
||||
const { user, company, companyType, createProfile, isAuthenticated } =
|
||||
useAuth();
|
||||
|
||||
// Keep the server session alive while a user is logged in. Runs after
|
||||
@@ -274,7 +274,7 @@ const App = () => {
|
||||
userEmail={userEmail}
|
||||
companyProfiles={companyProfiles}
|
||||
companyType={companyType}
|
||||
onCreateProfile={createProfileAndSwitch}
|
||||
onCreateProfile={createProfile}
|
||||
>
|
||||
<OnboardingGate />
|
||||
</AppLayout>
|
||||
|
||||
@@ -344,6 +344,15 @@ export default function OnboardingWizardDialog({
|
||||
[finishMutation],
|
||||
);
|
||||
|
||||
// Closing from the congratulations panel also clears the completed flag so a
|
||||
// future reopen (shouldn't happen once onboarded) starts clean.
|
||||
// Must stay above the `!user` early return: `user` flips to null while
|
||||
// useAuth refetches, and skipping a hook on that render breaks hook order.
|
||||
const handleClose = useCallback(() => {
|
||||
if (completed) setCompleted(false);
|
||||
onClose();
|
||||
}, [completed, onClose]);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
// Any non-empty combination of importer/exporter/freight-forwarder is valid.
|
||||
@@ -365,13 +374,6 @@ export default function OnboardingWizardDialog({
|
||||
const stepMeta = STEP_META[activeStep];
|
||||
const activeIdx = WIZARD_STEPS.indexOf(activeStep);
|
||||
|
||||
// Closing from the congratulations panel also clears the completed flag so a
|
||||
// future reopen (shouldn't happen once onboarded) starts clean.
|
||||
const handleClose = useCallback(() => {
|
||||
if (completed) setCompleted(false);
|
||||
onClose();
|
||||
}, [completed, onClose]);
|
||||
|
||||
// Prefer the backend-resolved document code; fall back to the local mapping
|
||||
// only until the requirements query lands (the documents step is reached well
|
||||
// after the draft — and thus the requirements — exist).
|
||||
|
||||
@@ -211,7 +211,11 @@ const useAuth = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const createProfileAndSwitch = async (
|
||||
/**
|
||||
* Add an operational role. The new role starts pending review, so the active
|
||||
* mode is left untouched — the user keeps working under their approved role.
|
||||
*/
|
||||
const createProfile = async (
|
||||
type: ProfileTypeValue,
|
||||
licenseFiles: File[],
|
||||
): Promise<Result<void>> => {
|
||||
@@ -278,7 +282,7 @@ const useAuth = () => {
|
||||
onboardingCompleted,
|
||||
onboardingStep,
|
||||
switchMode,
|
||||
createProfileAndSwitch,
|
||||
createProfile,
|
||||
reapplyProfile,
|
||||
login,
|
||||
signup,
|
||||
|
||||
@@ -83,7 +83,11 @@ const TABS: { id: SettingsTab; label: string; icon: React.ReactNode }[] = [
|
||||
function ProfileHeader({ profile }: { profile: ProfileResponse }) {
|
||||
const refByType = new Map(profile.companyProfiles.map((p) => [p.type, p]));
|
||||
const roleOptions = rolesForCompanyType(profile.companyType);
|
||||
const activeRoles = roleOptions.filter((o) => refByType.has(o.type));
|
||||
// Only an approved role is a role the company actually operates as. A pending
|
||||
// one carries no reference yet, and must not read as granted.
|
||||
const activeRoles = roleOptions.filter(
|
||||
(o) => refByType.get(o.type)?.status === "active",
|
||||
);
|
||||
|
||||
return (
|
||||
<Card
|
||||
|
||||
@@ -248,9 +248,8 @@ export default function CompanyProfileForm({
|
||||
} | null>(null);
|
||||
|
||||
const handleETradeDataLoaded = (data: CompanyRegistrationData) => {
|
||||
// Company name comes from the eTrade manager/owner name on the license.
|
||||
if (data.managerName) {
|
||||
setValue("companyName", data.managerName, { shouldValidate: true });
|
||||
if (data.companyName) {
|
||||
setValue("companyName", data.companyName, { shouldValidate: true });
|
||||
}
|
||||
setValue("licenceNumber", data.licenceNumber);
|
||||
setValue("statusDescription", data.statusDescription);
|
||||
|
||||
@@ -392,7 +392,7 @@ export default function NewContractPage({
|
||||
[profileStatusByType, profileTypes],
|
||||
);
|
||||
|
||||
// Create-profile modal state (license upload → createProfileAndSwitch).
|
||||
// Create-profile modal state (license upload → createProfile).
|
||||
const [createTarget, setCreateTarget] = useState<ProfileTypeValue | null>(
|
||||
null,
|
||||
);
|
||||
@@ -416,7 +416,7 @@ export default function NewContractPage({
|
||||
type: ProfileTypeValue;
|
||||
files: File[];
|
||||
}) => {
|
||||
const res = await auth.createProfileAndSwitch(type, files);
|
||||
const res = await auth.createProfile(type, files);
|
||||
if (!res.success) {
|
||||
throw new Error(res.error?.message ?? "Failed to create profile");
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Title,
|
||||
} from "@mantine/core";
|
||||
import { api } from "@/services/api";
|
||||
import type { CompanyProfileResponse } from "@/services/companies.service";
|
||||
import type { ProfileResponse } from "@/types/profile";
|
||||
import RoleCard from "./RoleCard";
|
||||
import { rolesForCompanyType } from "./companyRoles";
|
||||
@@ -18,6 +19,32 @@ interface CompanyRolesCardProps {
|
||||
profile: ProfileResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* How an existing role reads on the card. Only an `active` role is "approved" —
|
||||
* anything else is locked (a profile already exists, so it cannot be re-added)
|
||||
* but must never be presented as granted.
|
||||
*/
|
||||
function roleStatusView(p: CompanyProfileResponse): {
|
||||
note: string;
|
||||
color: string;
|
||||
approved: boolean;
|
||||
} {
|
||||
switch (p.status) {
|
||||
case "active":
|
||||
return { note: `Active · ${p.reference}`, color: "edr-green", approved: true };
|
||||
case "pending":
|
||||
return { note: "Pending review", color: "yellow.7", approved: false };
|
||||
case "rejected":
|
||||
return { note: "Rejected", color: "red.7", approved: false };
|
||||
case "suspended":
|
||||
return { note: "Suspended", color: "orange.7", approved: false };
|
||||
case "blacklisted":
|
||||
return { note: "Blacklisted", color: "red.7", approved: false };
|
||||
default:
|
||||
return { note: p.status, color: "edr-muted", approved: false };
|
||||
}
|
||||
}
|
||||
|
||||
export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -26,17 +53,18 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
|
||||
[profile.companyType],
|
||||
);
|
||||
|
||||
// Roles already persisted (active + locked), keyed by type -> reference.
|
||||
const activeByType = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
for (const p of profile.companyProfiles) map.set(p.type, p.reference);
|
||||
// Roles already persisted, keyed by type. Existence locks the card; the
|
||||
// profile's own status decides how it is labelled.
|
||||
const profileByType = useMemo(() => {
|
||||
const map = new Map<string, CompanyProfileResponse>();
|
||||
for (const p of profile.companyProfiles) map.set(p.type, p);
|
||||
return map;
|
||||
}, [profile.companyProfiles]);
|
||||
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
|
||||
const toggle = (type: string) => {
|
||||
if (activeByType.has(type)) return; // add-only: active roles are locked
|
||||
if (profileByType.has(type)) return; // add-only: existing roles are locked
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(type)) next.delete(type);
|
||||
@@ -83,7 +111,8 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
|
||||
) : (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
{options.map((opt) => {
|
||||
const isActive = activeByType.has(opt.type);
|
||||
const existing = profileByType.get(opt.type);
|
||||
const view = existing ? roleStatusView(existing) : undefined;
|
||||
return (
|
||||
<RoleCard
|
||||
key={opt.type}
|
||||
@@ -91,10 +120,10 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
|
||||
description={opt.description}
|
||||
icon={opt.icon}
|
||||
selected={selected.has(opt.type)}
|
||||
locked={isActive}
|
||||
lockedNote={
|
||||
isActive ? `Active · ${activeByType.get(opt.type)}` : undefined
|
||||
}
|
||||
locked={Boolean(existing)}
|
||||
approved={view?.approved}
|
||||
lockedNote={view?.note}
|
||||
lockedNoteColor={view?.color}
|
||||
onClick={() => toggle(opt.type)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -7,10 +7,17 @@ export interface RoleCardProps {
|
||||
icon: React.ReactNode;
|
||||
/** Highlighted because the user just selected it (toggleable). */
|
||||
selected?: boolean;
|
||||
/** Highlighted and non-interactive because it is already persisted. */
|
||||
/** Non-interactive because a profile for this role already exists. */
|
||||
locked?: boolean;
|
||||
/**
|
||||
* Approved by a reviewer. Drives the green "granted" treatment, which a
|
||||
* merely-locked role (e.g. still pending review) must not receive.
|
||||
*/
|
||||
approved?: boolean;
|
||||
/** Small note under the description, e.g. "Active · IM-00001". */
|
||||
lockedNote?: string;
|
||||
/** Mantine color for {@link lockedNote}; matches the role's status. */
|
||||
lockedNoteColor?: string;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
@@ -25,10 +32,12 @@ export default function RoleCard({
|
||||
icon,
|
||||
selected = false,
|
||||
locked = false,
|
||||
approved = false,
|
||||
lockedNote,
|
||||
lockedNoteColor = "edr-green",
|
||||
onClick,
|
||||
}: RoleCardProps) {
|
||||
const highlighted = selected || locked;
|
||||
const highlighted = selected || approved;
|
||||
|
||||
return (
|
||||
<UnstyledButton
|
||||
@@ -37,8 +46,12 @@ export default function RoleCard({
|
||||
className={`group block rounded-lg border! p-5! text-left transition-all duration-200 ${
|
||||
highlighted
|
||||
? "border-[var(--mantine-color-edr-green-5)]! bg-edr-soft!"
|
||||
: "border-edr-border! bg-edr-card! hover:-translate-y-0.5 hover:border-[var(--mantine-color-edr-green-5)]! hover:bg-edr-soft"
|
||||
} ${locked ? "cursor-default" : ""}`}
|
||||
: "border-edr-border! bg-edr-card!"
|
||||
} ${
|
||||
locked
|
||||
? "cursor-default"
|
||||
: "hover:-translate-y-0.5 hover:border-[var(--mantine-color-edr-green-5)]! hover:bg-edr-soft"
|
||||
}`}
|
||||
>
|
||||
<Group gap="md" wrap="nowrap" align="start">
|
||||
<ThemeIcon
|
||||
@@ -58,7 +71,7 @@ export default function RoleCard({
|
||||
{description}
|
||||
</Text>
|
||||
{lockedNote && (
|
||||
<Text size="xs" c="edr-green" mt={6} fw={600}>
|
||||
<Text size="xs" c={lockedNoteColor} mt={6} fw={600}>
|
||||
{lockedNote}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
@@ -385,7 +385,8 @@ function ProfileLicenseRow({
|
||||
<Stack gap="sm">
|
||||
<Group justify="space-between" align="center">
|
||||
<Text size="sm" fw={700} c="edr-text">
|
||||
{ROLE_LABELS[profile.type] ?? profile.type} · {profile.reference}
|
||||
{ROLE_LABELS[profile.type] ?? profile.type}
|
||||
{profile.reference ? ` · ${profile.reference}` : ""}
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
|
||||
Reference in New Issue
Block a user