mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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
This commit is contained in:
@@ -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 (
|
||||
<RoleCard
|
||||
key={opt.type}
|
||||
@@ -124,6 +141,28 @@ export default function CompanyRolesCard({ profile }: CompanyRolesCardProps) {
|
||||
approved={view?.approved}
|
||||
lockedNote={view?.note}
|
||||
lockedNoteColor={view?.color}
|
||||
detail={
|
||||
(rejected || existing?.status === "suspended") &&
|
||||
existing?.reviewNote
|
||||
? `Reviewer note: ${existing.reviewNote}`
|
||||
: undefined
|
||||
}
|
||||
action={
|
||||
rejected ? (
|
||||
<Button
|
||||
size="xs"
|
||||
variant="light"
|
||||
leftSection={<RefreshCw size={14} />}
|
||||
loading={
|
||||
reapplyMutation.isPending &&
|
||||
reapplyMutation.variables === existing.id
|
||||
}
|
||||
onClick={() => reapplyMutation.mutate(existing.id)}
|
||||
>
|
||||
Resubmit for approval
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
onClick={() => toggle(opt.type)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<UnstyledButton
|
||||
type="button"
|
||||
onClick={locked ? undefined : onClick}
|
||||
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"
|
||||
}`}
|
||||
>
|
||||
<Group gap="md" wrap="nowrap" align="start">
|
||||
<ThemeIcon
|
||||
size={56}
|
||||
radius="lg"
|
||||
variant={highlighted ? "filled" : "light"}
|
||||
color="edr-green"
|
||||
className="shrink-0"
|
||||
>
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
<Box className="min-w-0 flex-1">
|
||||
<Text fw={700} c="edr-text" fz={15}>
|
||||
{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 = (
|
||||
<Group gap="md" wrap="nowrap" align="start">
|
||||
<ThemeIcon
|
||||
size={56}
|
||||
radius="lg"
|
||||
variant={highlighted ? "filled" : "light"}
|
||||
color="edr-green"
|
||||
className="shrink-0"
|
||||
>
|
||||
{icon}
|
||||
</ThemeIcon>
|
||||
<Box className="min-w-0 flex-1">
|
||||
<Text fw={700} c="edr-text" fz={15}>
|
||||
{label}
|
||||
</Text>
|
||||
<Text size="xs" c="edr-muted" mt={4} lh={1.5}>
|
||||
{description}
|
||||
</Text>
|
||||
{lockedNote && (
|
||||
<Text size="xs" c={lockedNoteColor} mt={6} fw={600}>
|
||||
{lockedNote}
|
||||
</Text>
|
||||
<Text size="xs" c="edr-muted" mt={4} lh={1.5}>
|
||||
{description}
|
||||
</Text>
|
||||
{lockedNote && (
|
||||
<Text size="xs" c={lockedNoteColor} mt={6} fw={600}>
|
||||
{lockedNote}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
{highlighted && (
|
||||
<Check
|
||||
size={18}
|
||||
className="shrink-0 text-[var(--mantine-color-edr-green-6)]"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
{locked && detail && (
|
||||
<Text size="xs" c="edr-muted" mt={4} lh={1.5}>
|
||||
{detail}
|
||||
</Text>
|
||||
)}
|
||||
{locked && action && <Box mt="sm">{action}</Box>}
|
||||
</Box>
|
||||
{highlighted && (
|
||||
<Check
|
||||
size={18}
|
||||
className="shrink-0 text-[var(--mantine-color-edr-green-6)]"
|
||||
/>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
|
||||
// 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 <Box className={className}>{content}</Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<UnstyledButton type="button" onClick={onClick} className={className}>
|
||||
{content}
|
||||
</UnstyledButton>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user