audit: Tier 2/3/4 batch — reports math, portal copy, authz escalation guard
Tier 2.2: revenue PDF totalCompleted now filters on outcome='won' — setInterestOutcome forces stage='completed' for every outcome (incl. lost + cancelled), so the stage-only filter was including those toward "TOTAL COMPLETED REVENUE". Tier 2.3: fetchPipelineData stageCounts adds the missing .groupBy() — without it Postgres rejects the SELECT (per-stage breakdown was broken or coercing to ELSE-stage row). Tier 2.4: hot-deals widget rank ladder fixed two stage-name typos — 'in_comms' → 'in_communication', 'deposit_10' → 'deposit_10pct'. Both stages were collapsing to the ELSE 0 branch server-side AND rendering raw enum to the user in hot-deals-card.tsx. Tier 3.2: portal /portal/interests no longer renders raw enum to clients. New PORTAL_SIGNING_LABELS table maps every EOI/contract status to plain English (e.g. "waiting_for_signatures" → "Waiting for signatures"). Tier 4.1 (CRITICAL): permission-overrides PUT now requires caller- superset on every `true` write. Admins with only `admin.manage_users` could previously grant other users leaves they don't hold themselves (permanently_delete_clients, system_backup). Super-admins bypass. Tier 4.4: search graph-expansion re-gates every merged bucket by the destination's view permission. A user with berths.view but no interests.view searching "A12" no longer sees interest rows surfaced via expansion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,32 @@ import { stageLabel, safeStage, type PipelineStage } from '@/lib/constants';
|
||||
|
||||
export const metadata: Metadata = { title: 'Interests' };
|
||||
|
||||
// Portal-friendly labels for signing-process status fields. The audit
|
||||
// caught raw enum leak ("waiting_for_signatures" with underscores) at
|
||||
// the client-facing surface. Map every known value to plain English;
|
||||
// fall back to a Title-Case rendering for any new states.
|
||||
const PORTAL_SIGNING_LABELS: Record<string, string> = {
|
||||
not_started: 'Not started',
|
||||
draft: 'Drafted',
|
||||
awaiting_them: 'Awaiting their signature',
|
||||
awaiting_me: 'Awaiting your signature',
|
||||
waiting_for_signatures: 'Waiting for signatures',
|
||||
partially_signed: 'Partially signed',
|
||||
sent: 'Sent for signing',
|
||||
signed: 'Signed',
|
||||
completed: 'Signed',
|
||||
expired: 'Expired',
|
||||
cancelled: 'Cancelled',
|
||||
rejected: 'Rejected',
|
||||
};
|
||||
function portalSigningLabel(status: string): string {
|
||||
if (status in PORTAL_SIGNING_LABELS) return PORTAL_SIGNING_LABELS[status]!;
|
||||
return status
|
||||
.split('_')
|
||||
.map((p) => (p ? p[0]!.toUpperCase() + p.slice(1) : p))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
const STAGE_VARIANT: Record<PipelineStage, 'default' | 'secondary' | 'destructive' | 'outline'> = {
|
||||
open: 'secondary',
|
||||
details_sent: 'secondary',
|
||||
@@ -77,10 +103,10 @@ export default async function PortalInterestsPage() {
|
||||
</span>
|
||||
)}
|
||||
{interest.eoiStatus && (
|
||||
<span>EOI: {interest.eoiStatus.replace(/_/g, ' ')}</span>
|
||||
<span>EOI: {portalSigningLabel(interest.eoiStatus)}</span>
|
||||
)}
|
||||
{interest.contractStatus && (
|
||||
<span>Contract: {interest.contractStatus.replace(/_/g, ' ')}</span>
|
||||
<span>Contract: {portalSigningLabel(interest.contractStatus)}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user