mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
fix: prevent the backoffice from approve the user before he submits
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
ActionIcon,
|
||||
Alert,
|
||||
Anchor,
|
||||
Badge,
|
||||
Box,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
Download,
|
||||
Eye,
|
||||
FileText,
|
||||
Hourglass,
|
||||
IdCard,
|
||||
LayoutGrid,
|
||||
Package,
|
||||
@@ -60,6 +62,7 @@ import type {
|
||||
CustomerDocument,
|
||||
CustomerPayment,
|
||||
} from "@/types/customer";
|
||||
import { hasSubmittedOnboarding, isOnboardingDraft } from "@/types/customer";
|
||||
import type { Invoice } from "@/types/invoice";
|
||||
import {
|
||||
DataTable,
|
||||
@@ -166,6 +169,13 @@ export default function CustomerDetailPage() {
|
||||
);
|
||||
const paidCurrency = payments[0]?.currency ?? "ETB";
|
||||
|
||||
// The company row is created on the wizard's first click, so a draft reaches
|
||||
// this page with a placeholder name/TIN. `stillOnboarding` drives the banner
|
||||
// and badge; `canReview` gates the approve/reject buttons and mirrors the
|
||||
// API's rule exactly, so no button is offered that the server would reject.
|
||||
const stillOnboarding = company ? isOnboardingDraft(company) : false;
|
||||
const canReview = company ? hasSubmittedOnboarding(company) : true;
|
||||
|
||||
const profileColumns: ColumnDef<CompanyProfile>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -273,11 +283,12 @@ export default function CustomerDetailPage() {
|
||||
<ProfileApprovalActions
|
||||
profileId={row.original.id}
|
||||
status={row.original.status}
|
||||
locked={!canReview}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
[view],
|
||||
[view, canReview],
|
||||
);
|
||||
|
||||
const bookingColumns: ColumnDef<CustomerBooking>[] = useMemo(
|
||||
@@ -602,7 +613,13 @@ export default function CustomerDetailPage() {
|
||||
meta={
|
||||
<Group gap="xs" wrap="nowrap">
|
||||
<CompanyTypeBadge type={company.type} />
|
||||
<CompanyStatusBadge status={company.status} />
|
||||
{stillOnboarding ? (
|
||||
<Badge color="gray" variant="light" size="sm" radius="sm">
|
||||
Onboarding in progress
|
||||
</Badge>
|
||||
) : (
|
||||
<CompanyStatusBadge status={company.status} />
|
||||
)}
|
||||
<ChangeRequestPendingBadge companyId={company.id} />
|
||||
</Group>
|
||||
}
|
||||
@@ -631,6 +648,21 @@ export default function CustomerDetailPage() {
|
||||
{/* OVERVIEW */}
|
||||
<Tabs.Panel value="overview" pt="lg">
|
||||
<Stack gap="lg">
|
||||
{stillOnboarding && (
|
||||
<Alert
|
||||
color="gray"
|
||||
variant="light"
|
||||
radius="md"
|
||||
icon={<Hourglass size={18} />}
|
||||
title="This customer hasn't submitted their application yet"
|
||||
>
|
||||
They're still filling in the onboarding wizard, so the details
|
||||
below are an unfinished draft — the company name and TIN are
|
||||
placeholders until they reach those steps. Role profiles become
|
||||
reviewable once the application is submitted.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<ChangeRequestReview company={company} />
|
||||
|
||||
<KpiStrip
|
||||
@@ -642,10 +674,16 @@ export default function CustomerDetailPage() {
|
||||
color: "edr-green",
|
||||
},
|
||||
{
|
||||
label: "Pending approval",
|
||||
value: company.companyProfiles.filter(
|
||||
(p) => p.status === "pending",
|
||||
).length,
|
||||
// A draft's profiles are all `pending` by construction, which
|
||||
// would read as a review backlog that doesn't exist yet.
|
||||
label: stillOnboarding
|
||||
? "Awaiting submission"
|
||||
: "Pending approval",
|
||||
value: stillOnboarding
|
||||
? "—"
|
||||
: company.companyProfiles.filter(
|
||||
(p) => p.status === "pending",
|
||||
).length,
|
||||
icon: IdCard,
|
||||
color: "yellow",
|
||||
},
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
Hourglass,
|
||||
Mail,
|
||||
Phone,
|
||||
RefreshCw,
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
import { KpiStrip, PageContainer, PageHeader } from "@/components/page";
|
||||
import { api } from "@/services/api";
|
||||
import type { Company, CompanyStatus } from "@/types/customer";
|
||||
import { isOnboardingDraft } from "@/types/customer";
|
||||
import {
|
||||
DataTable,
|
||||
DataTableFooter,
|
||||
@@ -43,22 +45,39 @@ import {
|
||||
type ColumnDef,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
/**
|
||||
* The list's segmented views. "Pending approval" means submitted-and-awaiting-
|
||||
* review, so it excludes drafts — a company row exists from the onboarding
|
||||
* wizard's first click and would otherwise pad the review queue. Those drafts
|
||||
* get their own view instead of disappearing, so staff can still chase them.
|
||||
*/
|
||||
type CustomerView = "all" | "pending" | "onboarding" | "active";
|
||||
|
||||
const VIEW_FILTERS: Record<
|
||||
CustomerView,
|
||||
{ status?: CompanyStatus; onboardingCompleted?: boolean }
|
||||
> = {
|
||||
all: {},
|
||||
pending: { status: "pending", onboardingCompleted: true },
|
||||
onboarding: { onboardingCompleted: false },
|
||||
active: { status: "active" },
|
||||
};
|
||||
|
||||
export default function CustomersPage() {
|
||||
const navigate = useNavigate();
|
||||
const { pagination, setPagination } = usePagination({ pageSize: 10 });
|
||||
const [query, setQuery] = useState("");
|
||||
const [debouncedQuery] = useDebouncedValue(query, 300);
|
||||
// "" = all; otherwise a CompanyStatus to narrow the list (e.g. pending review).
|
||||
const [statusFilter, setStatusFilter] = useState<"" | CompanyStatus>("");
|
||||
const [view, setView] = useState<CustomerView>("all");
|
||||
|
||||
const filter = useMemo(
|
||||
() => ({
|
||||
page: pagination.pageIndex + 1,
|
||||
pageSize: pagination.pageSize,
|
||||
search: debouncedQuery,
|
||||
status: statusFilter || undefined,
|
||||
...VIEW_FILTERS[view],
|
||||
}),
|
||||
[pagination.pageIndex, pagination.pageSize, debouncedQuery, statusFilter],
|
||||
[pagination.pageIndex, pagination.pageSize, debouncedQuery, view],
|
||||
);
|
||||
|
||||
const { data: stats } = useQuery(api.customers.stats.queryOptions({ input: {} }));
|
||||
@@ -114,6 +133,17 @@ export default function CustomersPage() {
|
||||
id: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => {
|
||||
// A draft's profiles are all `pending` by construction, so the
|
||||
// "N pending" review hint would be a lie until they submit.
|
||||
if (isOnboardingDraft(row.original)) {
|
||||
return (
|
||||
<Tooltip label="Customer is still filling in the onboarding wizard">
|
||||
<Badge color="gray" variant="light" size="sm" radius="sm">
|
||||
Onboarding
|
||||
</Badge>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
const pending = (row.original.companyProfiles ?? []).filter(
|
||||
(p) => p.status === "pending",
|
||||
).length;
|
||||
@@ -206,6 +236,12 @@ export default function CustomersPage() {
|
||||
{ label: "Companies", value: stats?.total ?? "—", icon: Users, color: "edr-green" },
|
||||
{ label: "Active", value: stats?.active ?? "—", icon: CheckCircle2, color: "edr-green" },
|
||||
{ label: "Pending", value: stats?.pending ?? "—", icon: Clock, color: "yellow" },
|
||||
{
|
||||
label: "Onboarding",
|
||||
value: stats?.onboarding ?? "—",
|
||||
icon: Hourglass,
|
||||
color: "gray",
|
||||
},
|
||||
{
|
||||
label: "Blacklisted",
|
||||
value: stats?.blacklisted ?? "—",
|
||||
@@ -243,14 +279,15 @@ export default function CustomersPage() {
|
||||
<SegmentedControl
|
||||
size="sm"
|
||||
radius="md"
|
||||
value={statusFilter || "all"}
|
||||
value={view}
|
||||
onChange={(v) => {
|
||||
setStatusFilter(v === "all" ? "" : (v as CompanyStatus));
|
||||
setView(v as CustomerView);
|
||||
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
}}
|
||||
data={[
|
||||
{ label: "All", value: "all" },
|
||||
{ label: "Pending approval", value: "pending" },
|
||||
{ label: "Onboarding", value: "onboarding" },
|
||||
{ label: "Active", value: "active" },
|
||||
]}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user