diff --git a/frontend/src/history.ts b/frontend/src/history.ts index d8a8fd792..96dac7631 100644 --- a/frontend/src/history.ts +++ b/frontend/src/history.ts @@ -683,8 +683,8 @@ function providerCell(p: HistoryPurchase): string { function upfrontCell(p: HistoryPurchase, amortize: boolean): string { const amount = formatCurrency(p.upfront_cost); const folded = amortize && p.monthly_cost != null && p.upfront_cost != null && p.upfront_cost > 0 && p.term > 0; - if (!folded) return `${amount}`; - return `${amount}
(in monthly)
`; + if (!folded) return `${amount}`; + return `${amount}
(in monthly)
`; } // canCancelPendingRow returns true when the current session is permitted @@ -1408,18 +1408,18 @@ function renderHistoryList(purchases: HistoryPurchase[]): void { const dashOr = (html: string): string => (empty ? MUTED_DASH : html); return ` - ${statusCell} - ${formatDate(p.timestamp)} - ${providerCell(p)} - ${dashOr(escapeHtml(p.service))} - ${empty ? NO_RECS_HTML : escapeHtml(p.resource_type)} - ${dashOr(escapeHtml(p.region))} - ${dashOr(escapeHtml(String(p.count ?? '')))} - ${termCell(p)} - ${empty ? `${MUTED_DASH}` : upfrontCell(p, amortize)} - ${dashOr(monthlyCostCell)} - ${dashOr(formatCurrency(p.estimated_savings))} - ${planCellContent} + ${statusCell} + ${formatDate(p.timestamp)} + ${providerCell(p)} + ${dashOr(escapeHtml(p.service))} + ${empty ? NO_RECS_HTML : escapeHtml(p.resource_type)} + ${dashOr(escapeHtml(p.region))} + ${dashOr(escapeHtml(String(p.count ?? '')))} + ${termCell(p)} + ${empty ? `${MUTED_DASH}` : upfrontCell(p, amortize)} + ${dashOr(monthlyCostCell)} + ${dashOr(formatCurrency(p.estimated_savings))} + ${planCellContent} `; }).join(''); @@ -1430,7 +1430,7 @@ function renderHistoryList(purchases: HistoryPurchase[]): void { ); const markup = ` ${buildStatusChipRowHTML(purchases, activeStatusFilter)} - +
@@ -2046,18 +2046,18 @@ export function renderApprovalQueue(purchases: HistoryPurchase[]): void { const execIdAttr = p.purchase_id ? ` data-execution-id="${escapeHtmlAttr(p.purchase_id)}"` : ''; return ` - - - - - - - - - ${empty ? `` : upfrontCell(p, amortize)} - - - + + + + + + + + + ${empty ? `` : upfrontCell(p, amortize)} + + + `; }).join(''); @@ -2069,7 +2069,7 @@ export function renderApprovalQueue(purchases: HistoryPurchase[]): void { col, APPROVAL_QUEUE_LABELS[col], colFilters[col] != null, APPROVAL_QUEUE_FILTER_SCOPE, ); container.innerHTML = ` -
Status${formatDate(p.timestamp)}${accountCell}${providerCell(p)}${empty ? NO_RECS_HTML : escapeHtml(p.service)}${dashOr(escapeHtml(String(p.count ?? '')))}${termCell(p)}${dashOr(paymentCell)}${dashOr(monthlyCostCell)}${MUTED_DASH}${dashOr(formatCurrency(p.estimated_savings))}${createdBy}${actionsCell}${formatDate(p.timestamp)}${accountCell}${providerCell(p)}${empty ? NO_RECS_HTML : escapeHtml(p.service)}${dashOr(escapeHtml(String(p.count ?? '')))}${termCell(p)}${dashOr(paymentCell)}${dashOr(monthlyCostCell)}${MUTED_DASH}${dashOr(formatCurrency(p.estimated_savings))}${createdBy}${actionsCell}
+
diff --git a/frontend/src/styles/tables.css b/frontend/src/styles/tables.css index 62d38b3a4..5a3b051a9 100644 --- a/frontend/src/styles/tables.css +++ b/frontend/src/styles/tables.css @@ -699,3 +699,121 @@ th .history-column-filter-btn:focus-visible { .savings-stripe-swatch--medium { background: var(--cudly-warn); } + +/* Phones: the approval queue and purchase history (.table-cards-mobile) + * turn each row into a card of label / value lines (labels from the + * cells' data-label), so nothing hides behind a sideways scroll. The + * header row stays as a wrap of chips for the columns that have a filter + * funnel, so filtering still works; the other header cells are hidden. */ +@media (max-width: 640px) { + .table-cards-mobile, + .table-cards-mobile thead, + .table-cards-mobile tbody, + .table-cards-mobile tr, + .table-cards-mobile td { + display: block; + } + + .table-cards-mobile { + margin-top: var(--cudly-sp-2); + background: transparent; + box-shadow: none; + border-radius: 0; + overflow: visible; + } + + .table-cards-mobile thead tr { + display: flex; + flex-wrap: wrap; + gap: var(--cudly-sp-1); + margin-bottom: var(--cudly-sp-2); + } + + .table-cards-mobile thead tr:hover { + background: transparent; + } + + .table-cards-mobile thead th { + display: inline-flex; + align-items: center; + position: static; + padding: 0 0 0 var(--cudly-sp-2); + border: 1px solid var(--cudly-border); + border-radius: var(--cudly-r-full); + background: var(--cudly-surface); + } + + .table-cards-mobile thead th:not(:has(button)) { + display: none; + } + + /* Each card is a three-column grid of label-over-value fields; long + * values (account, creator, type) take two columns. */ + .table-cards-mobile.table-compact tbody tr { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-auto-flow: row dense; + gap: var(--cudly-sp-2) var(--cudly-sp-3); + margin-bottom: var(--cudly-sp-2); + padding: var(--cudly-sp-3); + border: 1px solid var(--cudly-border); + border-radius: var(--cudly-r-md); + background: var(--cudly-surface); + } + + .table-cards-mobile.table-compact tbody td { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0; + position: static; + min-width: 0; + overflow-wrap: anywhere; + padding: 0; + border: 0; + background: transparent; + text-align: left; + white-space: normal; + } + + .table-cards-mobile.table-compact tbody td::before { + content: attr(data-label); + position: static; + width: auto; + opacity: 1; + background: none; + max-width: 100%; + overflow: hidden; + color: var(--cudly-text-subtle); + font-size: var(--cudly-fs-xs); + text-overflow: ellipsis; + white-space: nowrap; + } + + /* Status (history) leads the card and the actions / plan cell closes + * it, each across the full width. */ + .table-cards-mobile.table-compact tbody td:is([data-label="Status"], :last-child) { + grid-column: 1 / -1; + } + + .table-cards-mobile.table-compact tbody td:is([data-label="Account"], [data-label="Created By"], [data-label="Type"]) { + grid-column: span 2; + } + + .table-cards-mobile.table-compact tbody td[data-label="Status"]::before { + content: none; + } + + .table-cards-mobile.table-compact tbody td:last-child { + padding-top: var(--cudly-sp-2); + border-top: 1px solid var(--cudly-border); + } + + .table-cards-mobile.table-compact tbody td:last-child::before { + content: none; + } + + .table-cards-mobile .cell-truncate { + max-width: 100%; + } +}
Date