diff --git a/apps/edr-freight-web/portal/src/pages/settings/CompanyRolesCard.tsx b/apps/edr-freight-web/portal/src/pages/settings/CompanyRolesCard.tsx
index fc0407d5e..974af456e 100644
--- a/apps/edr-freight-web/portal/src/pages/settings/CompanyRolesCard.tsx
+++ b/apps/edr-freight-web/portal/src/pages/settings/CompanyRolesCard.tsx
@@ -1,6 +1,6 @@
import { useMemo, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
-import { Building2, CheckCircle2, Save, XCircle } from "lucide-react";
+import { Building2, CheckCircle2, RefreshCw, Save, XCircle } from "lucide-react";
import {
Button,
Card,
@@ -87,6 +87,22 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
},
});
+ // Resubmit a rejected role for review. Flips it back to Pending server-side
+ // and pings the backoffice, so the fix-and-resubmit loop can happen entirely
+ // from settings instead of only from the contract page's rejection banner.
+ const reapplyMutation = useMutation({
+ mutationFn: (profileId: string) =>
+ api.companies.reapplyProfile.call({ profileId }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({
+ queryKey: api.companies.getProfile.queryKey(),
+ });
+ queryClient.invalidateQueries({
+ queryKey: api.companies.getInfo.queryKey(),
+ });
+ },
+ });
+
const handleSave = () => {
if (selected.size === 0) return;
mutation.mutate(Array.from(selected));
@@ -113,6 +129,7 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
{options.map((opt) => {
const existing = profileByType.get(opt.type);
const view = existing ? roleStatusView(existing) : undefined;
+ const rejected = existing?.status === "rejected";
return (
}
+ loading={
+ reapplyMutation.isPending &&
+ reapplyMutation.variables === existing.id
+ }
+ onClick={() => reapplyMutation.mutate(existing.id)}
+ >
+ Resubmit for approval
+
+ ) : undefined
+ }
onClick={() => toggle(opt.type)}
/>
);
diff --git a/apps/edr-freight-web/portal/src/pages/settings/RoleCard.tsx b/apps/edr-freight-web/portal/src/pages/settings/RoleCard.tsx
index 0a6b98dc4..0061d3318 100644
--- a/apps/edr-freight-web/portal/src/pages/settings/RoleCard.tsx
+++ b/apps/edr-freight-web/portal/src/pages/settings/RoleCard.tsx
@@ -18,6 +18,14 @@ export interface RoleCardProps {
lockedNote?: string;
/** Mantine color for {@link lockedNote}; matches the role's status. */
lockedNoteColor?: string;
+ /** Extra muted line under {@link lockedNote}, e.g. the reviewer's note. */
+ detail?: string;
+ /**
+ * Interactive content (e.g. a resubmit button) rendered inside the card.
+ * Only honoured on a locked card — the interactive variant is itself a
+ * button, and buttons cannot nest.
+ */
+ action?: React.ReactNode;
onClick?: () => void;
}
@@ -35,54 +43,70 @@ export default function RoleCard({
approved = false,
lockedNote,
lockedNoteColor = "edr-green",
+ detail,
+ action,
onClick,
}: RoleCardProps) {
const highlighted = selected || approved;
- return (
-
-
-
- {icon}
-
-
-
- {label}
+ const 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!"
+ } ${
+ locked
+ ? "cursor-default"
+ : "hover:-translate-y-0.5 hover:border-[var(--mantine-color-edr-green-5)]! hover:bg-edr-soft"
+ }`;
+
+ const content = (
+
+
+ {icon}
+
+
+
+ {label}
+
+
+ {description}
+
+ {lockedNote && (
+
+ {lockedNote}
-
- {description}
-
- {lockedNote && (
-
- {lockedNote}
-
- )}
-
- {highlighted && (
-
)}
-
+ {locked && detail && (
+
+ {detail}
+
+ )}
+ {locked && action && {action}}
+
+ {highlighted && (
+
+ )}
+
+ );
+
+ // A locked card is display-only, so it renders as a plain box — which also
+ // lets `action` hold real buttons without nesting them inside a button.
+ if (locked) {
+ return {content};
+ }
+
+ return (
+
+ {content}
);
}