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:
@@ -6,12 +6,20 @@ import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { Loader2, Send, CreditCard } from 'lucide-react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { InvoicePdfPreview } from './invoice-pdf-preview';
|
||||
import { apiFetch } from '@/lib/api/client';
|
||||
@@ -26,6 +34,40 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
cancelled: 'bg-gray-100 text-gray-500 border-gray-200',
|
||||
};
|
||||
|
||||
// Display labels for snake_case enum values stored in the DB.
|
||||
const PAYMENT_METHOD_LABELS: Record<string, string> = {
|
||||
bank_transfer: 'Bank transfer',
|
||||
credit_card: 'Credit card',
|
||||
cash: 'Cash',
|
||||
cheque: 'Cheque',
|
||||
check: 'Check',
|
||||
wire: 'Wire',
|
||||
other: 'Other',
|
||||
};
|
||||
|
||||
const PAYMENT_METHOD_OPTIONS: Array<{ value: string; label: string }> = [
|
||||
{ value: 'bank_transfer', label: 'Bank transfer' },
|
||||
{ value: 'credit_card', label: 'Credit card' },
|
||||
{ value: 'cash', label: 'Cash' },
|
||||
{ value: 'cheque', label: 'Cheque' },
|
||||
{ value: 'wire', label: 'Wire' },
|
||||
{ value: 'other', label: 'Other' },
|
||||
];
|
||||
|
||||
function formatPaymentMethod(method: string | null | undefined): string {
|
||||
if (!method) return '—';
|
||||
return PAYMENT_METHOD_LABELS[method] ?? method.replace(/_/g, ' ');
|
||||
}
|
||||
|
||||
function formatDateOnly(value: string | null | undefined): string {
|
||||
if (!value) return '—';
|
||||
// Stored values are typically YYYY-MM-DD or ISO. Treat as date-only to avoid TZ shift.
|
||||
const isoDate = value.length === 10 ? value + 'T00:00:00' : value;
|
||||
const d = new Date(isoDate);
|
||||
if (Number.isNaN(d.getTime())) return value;
|
||||
return format(d, 'MMM d, yyyy');
|
||||
}
|
||||
|
||||
interface InvoiceDetailProps {
|
||||
invoiceId: string;
|
||||
}
|
||||
@@ -155,7 +197,7 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
|
||||
<CardTitle className="text-sm font-medium">Due Date</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm">{invoice.dueDate}</p>
|
||||
<p className="text-sm">{formatDateOnly(invoice.dueDate)}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card>
|
||||
@@ -291,11 +333,11 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<span className="text-muted-foreground">Payment Date</span>
|
||||
<p className="mt-0.5">{invoice.paymentDate ?? '—'}</p>
|
||||
<p className="mt-0.5">{formatDateOnly(invoice.paymentDate)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Method</span>
|
||||
<p className="mt-0.5 capitalize">{invoice.paymentMethod ?? '—'}</p>
|
||||
<p className="mt-0.5">{formatPaymentMethod(invoice.paymentMethod)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-muted-foreground">Reference</span>
|
||||
@@ -325,11 +367,23 @@ export function InvoiceDetail({ invoiceId }: InvoiceDetailProps) {
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="paymentMethod">Payment Method</Label>
|
||||
<Input
|
||||
id="paymentMethod"
|
||||
placeholder="e.g. bank_transfer, credit_card"
|
||||
{...paymentForm.register('paymentMethod')}
|
||||
/>
|
||||
<Select
|
||||
value={paymentForm.watch('paymentMethod') ?? ''}
|
||||
onValueChange={(v) =>
|
||||
paymentForm.setValue('paymentMethod', v, { shouldValidate: true })
|
||||
}
|
||||
>
|
||||
<SelectTrigger id="paymentMethod">
|
||||
<SelectValue placeholder="Select a method" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{PAYMENT_METHOD_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="paymentReference">Reference / Transaction ID</Label>
|
||||
|
||||
Reference in New Issue
Block a user