From 4f6a559ae331bd566881a3aabc5da5020f9f9eff Mon Sep 17 00:00:00 2001 From: Nathnael Date: Tue, 21 Jul 2026 09:09:04 +0000 Subject: [PATCH] feat(portal): reviewer note and resubmit for rejected roles in settings A customer whose role was rejected saw only a bare "Rejected" chip in settings - the reviewer's note and the resubmit action existed solely inside the contract wizard's block modal, so fixing and reapplying from the profile page was impossible. The settings role card now shows the reviewer note (for suspended roles too) and offers "Resubmit for approval", which flips the role back to Pending and notifies the backoffice through the existing roleReapplied inbox event. RoleCard's locked variant now renders as a plain box instead of a button so it can host the action button (buttons cannot nest) and the new detail line. EDRFREIGHT-233 --- .../src/pages/settings/CompanyRolesCard.tsx | 41 ++++++- .../portal/src/pages/settings/RoleCard.tsx | 108 +++++++++++------- 2 files changed, 106 insertions(+), 43 deletions(-) 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} ); }