Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions apps/web/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -10534,6 +10534,36 @@ html[data-density='compact'] .status-badge {
padding: var(--space-3) 0 0;
}

/* ── Document fields + provider region host card (#1462) ──────────────
* Reuses `.order-invoice-panel`'s card chrome (#1449) so the Document KV
* block and the nested `invoiceDetailSection` slot render as one framed
* region instead of the provider's reg-card tone fill stretching to the
* full width of `.invoice-detail__col--main`. */
.invoice-detail__card {
display: flex;
flex-direction: column;
gap: var(--space-4);
padding: var(--space-4);
background: var(--bg-surface);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xs);
}
.invoice-detail__card-divider {
border: none;
border-top: 1px solid var(--border-subtle);
margin: 0;
}
.invoice-detail__provider-label {
margin: 0 0 var(--space-2);
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 500;
letter-spacing: var(--tracking-caps);
text-transform: uppercase;
color: var(--text-muted);
}

/* ─ Invoice list retry banner ─ */
.invoice-list__retry-banner {
margin-bottom: var(--space-3);
Expand Down
171 changes: 103 additions & 68 deletions apps/web/src/pages/invoicing/invoice-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* extras slot, and order back-link.
*
* Layout: 2-column grid at ≥1024 px.
* LEFT: Failure alert (when applicable) + Contents / Document / Regulatory KV
* LEFT: Failure alert (when applicable) + one host card containing the
* Document/Regulatory KeyValueList and the nested provider region
* (#1462, mirrors #1449's OrderInvoicePanel card chrome)
* RIGHT: InvoiceTimeline + Actions (Retry / in-doubt)
*
* Lifecycle states:
Expand All @@ -28,25 +30,101 @@ import { Alert } from '../../shared/ui/alert';
import { Button } from '../../shared/ui/button';
import { ErrorState, EmptyState } from '../../shared/ui/feedback-state';
import { TimeDisplay } from '../../shared/ui/time-display';
import { KeyValueList, type KeyValueItem } from '../../shared/ui/key-value-list';
import { useTranslation } from '../../shared/i18n';
import { useToast } from '../../shared/ui/toast-provider';
import { ApiError } from '../../shared/api/api-error';
import { usePlatform } from '../../shared/plugins';
import { useConnectionsQuery } from '../../features/connections/hooks/use-connections-query';
import { useConnectionsQuery, type Connection } from '../../features/connections';
import { useInvoiceQuery } from '../../features/invoicing/hooks/use-invoice-query';
import { useIssueInvoiceMutation } from '../../features/invoicing/hooks/use-issue-invoice-mutation';
import {
deriveInvoiceDisplayStatus,
canRetryInvoice,
resolveFailureCopy,
} from '../../features/invoicing/lib/derive-invoice-display';
import type { InvoiceRecord } from '../../features/invoicing/api/invoicing.types';
import { InvoiceStatusBadge } from '../../features/invoicing/components/invoice-status-badge';
import { RegulatoryStatusBadge } from '../../features/invoicing/components/regulatory-status-badge';
import { InvoicePdfLink } from '../../features/invoicing/components/invoice-pdf-link';
import { InvoiceTimeline } from '../../features/invoicing/components/invoice-timeline';
import { DOCUMENT_TYPE_LABEL_FALLBACK } from '../../features/invoicing/components/document-type-select';
import { resolveIssueErrorMessage } from '../../features/invoicing/lib/issue-error-message';

/**
* Build the `KeyValueList` rows for the Document fields block — same field
* set the bespoke `<dl className="invoice-panel__kv">` rendered, only the
* wrapping markup changed to the shared primitive (#1462, mirrors #1449's
* `buildInvoiceFieldItems` on `OrderInvoicePanel`).
*/
function buildInvoiceDetailFieldItems(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION (DRY): buildInvoiceDetailFieldItems mirrors OrderInvoicePanel's local buildInvoiceFieldItems (#1449) closely. The two field sets genuinely differ though — this page adds a linked Invoiced via, a clearanceRef row, and a different field order/labels ("Document type" vs "Document", "Regulatory clearance" vs "Clearance") — so a straight extraction would need parameterizing all of that, and the issue explicitly scoped this as "mirror the pattern," not literal reuse. Fine to keep as-is. If a third invoice-field surface ever appears, that's the moment to lift a shared, parameterized builder into features/invoicing/lib/. No change required.

invoice: InvoiceRecord,
connection: Connection | null,
showRegulatoryBadge: boolean,
t: (key: string, fallback: string) => string,
): KeyValueItem[] {
const items: KeyValueItem[] = [
{
id: 'number',
label: t('invoice.field.number', 'Number'),
value: invoice.providerInvoiceNumber ? (
<InvoicePdfLink
invoiceNumber={invoice.providerInvoiceNumber}
pdfUrl={invoice.pdfUrl}
/>
) : (
<span className="text-muted">—</span>
),
},
{
id: 'document',
label: t('invoice.field.document', 'Document type'),
value: t(
`invoice.documentType.${invoice.documentType}`,
DOCUMENT_TYPE_LABEL_FALLBACK[invoice.documentType] ?? invoice.documentType,
),
},
{
id: 'issued',
label: t('invoice.field.issued', 'Issued at'),
value: invoice.issuedAt ? (
<TimeDisplay iso={invoice.issuedAt} format="datetime" className="mono-text" />
) : (
<span className="text-muted">—</span>
),
},
{
id: 'via',
label: t('invoice.field.via', 'Invoiced via'),
value: connection ? (
<Link to={`/connections/${connection.id}`} className="link">
{connection.name}
</Link>
) : (
<span className="mono-text">{invoice.connectionId}</span>
),
},
];

if (showRegulatoryBadge) {
items.push({
id: 'clearance',
label: t('invoice.field.clearance', 'Regulatory clearance'),
value: <RegulatoryStatusBadge status={invoice.regulatoryStatus} />,
});

if (invoice.clearanceReference) {
items.push({
id: 'clearanceRef',
label: t('invoice.field.clearanceRef', 'Reference'),
value: <span className="mono-text">{invoice.clearanceReference}</span>,
});
}
}

return items;
}

export function InvoiceDetailPage(): ReactElement {
const { invoiceId = '' } = useParams<{ invoiceId: string }>();
const { t } = useTranslation();
Expand Down Expand Up @@ -189,73 +267,30 @@ export function InvoiceDetailPage(): ReactElement {
</Alert>
) : null}

{/* Invoice KV */}
<section className="detail-section">
<h2 className="detail-section__title">
{t('invoice.detail.sectionDocument', 'Document')}
</h2>
<dl className="invoice-panel__kv">
<dt>{t('invoice.field.number', 'Number')}</dt>
<dd>
{invoice.providerInvoiceNumber ? (
<InvoicePdfLink
invoiceNumber={invoice.providerInvoiceNumber}
pdfUrl={invoice.pdfUrl}
/>
) : (
<span className="text-muted">—</span>
)}
</dd>
{/* Document fields + provider region — one host card (#1462) */}
<div className="invoice-detail__card">
<section className="detail-section">
<h2 className="detail-section__title">
{t('invoice.detail.sectionDocument', 'Document')}
</h2>
<KeyValueList
items={buildInvoiceDetailFieldItems(invoice, connection, showRegulatoryBadge, t)}
/>
</section>

<dt>{t('invoice.field.document', 'Document type')}</dt>
<dd>
{t(
`invoice.documentType.${invoice.documentType}`,
DOCUMENT_TYPE_LABEL_FALLBACK[invoice.documentType] ?? invoice.documentType,
)}
</dd>

<dt>{t('invoice.field.issued', 'Issued at')}</dt>
<dd>
{invoice.issuedAt ? (
<TimeDisplay iso={invoice.issuedAt} format="datetime" className="mono-text" />
) : (
<span className="text-muted">—</span>
)}
</dd>

<dt>{t('invoice.field.via', 'Invoiced via')}</dt>
<dd>
{connection ? (
<Link to={`/connections/${connection.id}`} className="link">
{connection.name}
</Link>
) : (
<span className="mono-text">{invoice.connectionId}</span>
)}
</dd>

{showRegulatoryBadge ? (
<>
<dt>{t('invoice.field.clearance', 'Regulatory clearance')}</dt>
<dd>
<RegulatoryStatusBadge status={invoice.regulatoryStatus} />
</dd>
{invoice.clearanceReference ? (
<>
<dt>{t('invoice.field.clearanceRef', 'Reference')}</dt>
<dd className="mono-text">{invoice.clearanceReference}</dd>
</>
) : null}
</>
) : null}
</dl>
</section>

{/* Provider extras slot */}
{InvoiceDetailSection && connection ? (
<InvoiceDetailSection invoice={invoice} connection={connection} />
) : null}
{/* Provider extras slot */}
{InvoiceDetailSection && connection ? (
<>
<hr className="invoice-detail__card-divider" />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION (cross-surface consistency): the redesigned OrderInvoicePanel renders the same invoiceDetailSection slot inline in .order-invoice-panel__body with no divider or label (order-invoice-panel.tsx:368-370), whereas this page adds an <hr className="invoice-detail__card-divider"> + a "Provider region" caps label. #1462 asks for "the same de-emphasised, host-owned framing as #1449," so the two surfaces now diverge slightly. This reads as defensible — the detail page's main column is much wider (3fr 2fr grid) than the panel card, so the label+divider earns its keep in de-emphasising a region that previously stretched full-width. Flagging only so the divergence is a conscious choice; no change required.

<div>
<p className="invoice-detail__provider-label">
{t('invoice.detail.providerRegion', 'Provider region')}
</p>
<InvoiceDetailSection invoice={invoice} connection={connection} />
</div>
</>
) : null}
</div>
</div>

{/* ── RIGHT COLUMN ── */}
Expand Down
Loading