fix(ui): humanize enum labels, format dates, resolve actor names, loading skeleton

- Documents hub signer status now renders via a label map (`Pending`,
  `Signed`, `Declined`, …) instead of the raw lowercase enum value.
- Invoice detail formats `dueDate` and `paymentDate` as `MMM d, yyyy`
  via `date-fns` instead of leaking raw `2025-03-14` ISO strings, and
  swaps the "Payment Method" free-text input for a `Select` of labelled
  options (`Bank transfer`, `Credit card`, …) so we never store
  `bank_transfer` from a hand-typed field again.
- Interest tabs `MilestoneSection` status badge uses a `humanizeStatus`
  helper so values like `waiting_for_signatures` show as
  `Waiting For Signatures` (correctly title-cased) instead of being a
  lower-snake-case fragment inside an ALL-CAPS pill.
- `OUTCOME_BADGE` in the interest header now has a fall-through that
  renders any unknown outcome as a closed-state badge, preventing a
  closed interest from looking open just because its enum was added
  upstream without a matching label entry.
- Interest timeline route joins the `user` table and returns
  `userName` alongside `userId`; the client renders the resolved name
  instead of a 36-char UUID. Falls back to `'a teammate'` if the user
  row was deleted.
- Invoice "New / Step 3 — Review" replaces the truncated UUID display
  with a server-resolved client/company name via a small `useQuery`,
  so users can confirm they picked the right billing entity before
  submitting.
- New `loading.tsx` for client detail renders a header / tab strip /
  card skeleton during the server-component / initial-query window
  that previously flashed empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Ciaccio
2026-05-02 23:01:35 +02:00
parent a391934b73
commit 57a099acc4
8 changed files with 204 additions and 19 deletions

View File

@@ -36,6 +36,20 @@ const OUTCOME_BADGE: Record<string, { label: string; className: string }> = {
cancelled: { label: 'Cancelled', className: 'bg-slate-200 text-slate-700' },
};
// Catch-all so an unknown outcome (e.g. a future `lost_no_berth` enum) still
// renders as a closed-state badge instead of falling back to the open-state
// stage picker. Lost-* gets a rose tint; everything else gets neutral slate.
function resolveOutcomeBadge(outcome: string | null | undefined) {
if (!outcome) return null;
const known = OUTCOME_BADGE[outcome];
if (known) return known;
const isLoss = outcome.startsWith('lost');
return {
label: outcome.replace(/_/g, ' ').replace(/\b\w/g, (m) => m.toUpperCase()),
className: isLoss ? 'bg-rose-100 text-rose-700' : 'bg-slate-200 text-slate-700',
};
}
const CATEGORY_LABELS: Record<string, string> = {
general_interest: 'General',
specific_qualified: 'Specific Qualified',
@@ -90,7 +104,7 @@ export function InterestDetailHeader({ portSlug, interest }: InterestDetailHeade
const [outcomeDialog, setOutcomeDialog] = useState<null | 'won' | 'lost'>(null);
const isArchived = !!interest.archivedAt;
const outcomeBadge = interest.outcome ? OUTCOME_BADGE[interest.outcome] : null;
const outcomeBadge = resolveOutcomeBadge(interest.outcome);
const isClosed = !!interest.outcome;
// Contact deep-links — resolved from the linked client's primary channels.