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} ); }