From 4cc5fbb2c4af20b477afd6059b528aa51dd87a14 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Mon, 1 Jun 2026 09:46:03 -0300 Subject: [PATCH 01/11] chore: change grid looks and feel - WIP --- .../components/TransactionType.js | 28 +++ src/components/mui/SponsorOrderGrid/index.js | 220 ++++++++++-------- .../mui/table/extra-rows/DiscountRow.jsx | 32 ++- .../mui/table/extra-rows/FeeRow.jsx | 15 +- .../mui/table/extra-rows/PaymentRow.jsx | 31 ++- .../mui/table/extra-rows/RefundRow.jsx | 35 +-- .../mui/table/extra-rows/TotalRow.jsx | 30 ++- src/i18n/en.json | 3 +- src/utils/constants.js | 8 + 9 files changed, 255 insertions(+), 147 deletions(-) create mode 100644 src/components/mui/SponsorOrderGrid/components/TransactionType.js diff --git a/src/components/mui/SponsorOrderGrid/components/TransactionType.js b/src/components/mui/SponsorOrderGrid/components/TransactionType.js new file mode 100644 index 00000000..76650200 --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/TransactionType.js @@ -0,0 +1,28 @@ +import React from "react"; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import DoNotDisturbIcon from '@mui/icons-material/DoNotDisturb'; + +import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; +import {Box} from "@mui/material"; + +const iconMap = { + [SPONSOR_ORDER_GRID_ITEM_TYPES.CHARGE]: {icon: ArrowUpwardIcon, color: "warning.main"}, + [SPONSOR_ORDER_GRID_ITEM_TYPES.PAYMENT]: {icon: ArrowDownwardIcon, color: "success.main"}, + [SPONSOR_ORDER_GRID_ITEM_TYPES.DISCOUNT]: {icon: ArrowDownwardIcon, color: "success.main"}, + [SPONSOR_ORDER_GRID_ITEM_TYPES.REFUND]: {icon: RefreshIcon, color: "error.main"}, + [SPONSOR_ORDER_GRID_ITEM_TYPES.CANCELLED]: {icon: DoNotDisturbIcon, color: "default"}, +} + +const TransactionType = ({type, children}) => { + const Icon = iconMap[type].icon; + return ( + + + {children || type} + + ); +} + +export default TransactionType; \ No newline at end of file diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js index adf695a6..a335f89a 100644 --- a/src/components/mui/SponsorOrderGrid/index.js +++ b/src/components/mui/SponsorOrderGrid/index.js @@ -26,7 +26,8 @@ import TableCell from "@mui/material/TableCell"; import Table from "@mui/material/Table"; import TableHead from "@mui/material/TableHead"; import {currencyAmountFromCents} from "../../../utils/money"; -import {SPONSOR_FORMS_METAFIELD_CLASS} from "../../../utils/constants"; +import {SPONSOR_FORMS_METAFIELD_CLASS, SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../utils/constants"; +import TransactionType from "./components/TransactionType"; const mapOrderData = (forms, showItemDescription) => { if (!forms) return []; @@ -69,16 +70,17 @@ const mapOrderData = (forms, showItemDescription) => { const amount = currencyAmountFromCents(it.amount || 0); const lineId = it.line_id; const cancelled = !!it.canceled_by_id; - const rate = currencyAmountFromCents(it.current_rate || 0); + const type = cancelled ? SPONSOR_ORDER_GRID_ITEM_TYPES.CANCELLED : SPONSOR_ORDER_GRID_ITEM_TYPES.CHARGE; return { id: lineId, code: form.code, name: form.name, - rate, + type, addon_name: form.addon_name, item_name: itemDetails, amount, + amountValue: it.amount, cancelled }; }) @@ -92,7 +94,6 @@ const SponsorOrderGrid = ({ refunds, fees, total, - amountDue, withDescription = false, onCancelForm, onUndoCancelForm @@ -100,6 +101,16 @@ const SponsorOrderGrid = ({ const data = mapOrderData(lines, withDescription); const showActionCol = onCancelForm && onUndoCancelForm; const trailingCols = showActionCol ? 1 : 0; + let balance = 0; + + console.log("DATA: ", data, fees, payments, refunds, total); + + const calculateBalance = (rowAmount, op = 1) => { + console.log("CALCULATE BALANCE: ", balance, rowAmount, op); + balance = balance + (rowAmount * op); + return balance; + // return currencyAmountFromCents(balance); + } const columns = [ { @@ -107,50 +118,22 @@ const SponsorOrderGrid = ({ header: T.translate("sponsor_order_grid.code") }, { - columnKey: "name", - header: T.translate("sponsor_order_grid.contents") + columnKey: "type", + header: T.translate("sponsor_order_grid.type"), + render: (row) => () }, { - columnKey: "addon_name", - header: T.translate("sponsor_order_grid.addon") - }, - { - columnKey: "item_name", + columnKey: "name", header: T.translate("sponsor_order_grid.details") }, - { - columnKey: "rate", - header: T.translate("sponsor_order_grid.rate") - }, { columnKey: "amount", - header: T.translate("sponsor_order_grid.amount") + header: T.translate("sponsor_order_grid.amount"), + align: "right" } ]; - if (showActionCol) { - columns.push({ - columnKey: "actions", - header: T.translate("sponsor_order_grid.action"), - align: "center", - render: (row) => { - if (row.cancelled) { - return ( - onUndoCancelForm(row)}> - {" "} - {T.translate("general.undo").toUpperCase()} - - ); - } - - return ( - onCancelForm(row)}> - - - ); - } - }); - } + const colCount = columns.length + 1 + trailingCols; // 1 for balance, 1 for action col return ( @@ -168,13 +151,21 @@ const SponsorOrderGrid = ({ {col.header} ))} + + {T.translate("sponsor_order_grid.balance")} + + {showActionCol && ( + + {T.translate("sponsor_order_grid.action")} + + )} {data.map((form) => { const rows = form.items.map((row) => ( - {columns.map((col) => ( - - {col.render ? ( - col.render(row) - ) : ( - + {(() => { + const cols = columns.map((col) => ( + + {col.render ? ( + col.render(row) + ) : ( + {row[col.columnKey]} - )} - - ))} + )} + + )); + + console.log("ROW: ", row); + + // BALANCE COLUMN + cols.push( + + + {calculateBalance(row.cancelled ? 0 : row.amountValue)} + + + ) + + // ACTION COLUMN + if (showActionCol) { + cols.push( + + + {row.cancelled ? ( + onUndoCancelForm(row)}> + {" "} + {T.translate("general.undo").toUpperCase()} + + ) : ( + onCancelForm(row)}> + + + )} + + + ) + } + + return cols; + })()} + )); rows.push( ); return rows; })} - {fees && - fees.map((fee) => ( - - ))} - {refunds && - refunds.map((refund) => ( - - ))} - {payments && - payments.map((payment) => ( - - ))} - {notes && - notes.map((note) => ( - - ))} + {fees && fees.map((fee) => ( + + ))} + {refunds && refunds.map((refund) => ( + + ))} + {payments && payments.map((payment) => ( + + ))} + {notes && notes.map((note) => ( + + ))} + {data.length === 0 && ( - + {T.translate("mui_table.no_items")} diff --git a/src/components/mui/table/extra-rows/DiscountRow.jsx b/src/components/mui/table/extra-rows/DiscountRow.jsx index 4e0d84b5..e2368ba9 100644 --- a/src/components/mui/table/extra-rows/DiscountRow.jsx +++ b/src/components/mui/table/extra-rows/DiscountRow.jsx @@ -17,9 +17,11 @@ import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; import Typography from "@mui/material/Typography"; import { currencyAmountFromCents } from "../../../../utils/money"; +import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; +import TransactionType from "../../SponsorOrderGrid/components/TransactionType"; -const DiscountRow = ({ discount, discountTotal, colGap = 2, trailing = 0 }) => { +const DiscountRow = ({ discount, discountTotal, balance, colGap = 0, trailing = 0 }) => { if (discountTotal === 0) return null; @@ -27,11 +29,18 @@ const DiscountRow = ({ discount, discountTotal, colGap = 2, trailing = 0 }) => { {T.translate("mui_table.dis")} - - {T.translate("mui_table.discount")} + + + {T.translate("mui_table.discount")} + + + + + + {discount} {[...Array(colGap)].map((_, i) => ( @@ -39,16 +48,19 @@ const DiscountRow = ({ discount, discountTotal, colGap = 2, trailing = 0 }) => { ))} - - {discount} + + {currencyAmountFromCents(discountTotal)} - -{currencyAmountFromCents(discountTotal)} + {balance} {[...Array(trailing)].map((_, i) => ( diff --git a/src/components/mui/table/extra-rows/FeeRow.jsx b/src/components/mui/table/extra-rows/FeeRow.jsx index e4914acd..66e8c6c2 100644 --- a/src/components/mui/table/extra-rows/FeeRow.jsx +++ b/src/components/mui/table/extra-rows/FeeRow.jsx @@ -17,13 +17,18 @@ import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; import Typography from "@mui/material/Typography"; import { currencyAmountFromCents } from "../../../../utils/money"; +import TransactionType from "../../SponsorOrderGrid/components/TransactionType"; +import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; -const FeeRow = ({ fee, colGap = 3, trailing = 0 }) => { +const FeeRow = ({ fee, balance, colGap = 0, trailing = 0 }) => { if (!fee) return null; return ( {T.translate("mui_table.payfee")} + + + {fee.title} @@ -41,6 +46,14 @@ const FeeRow = ({ fee, colGap = 3, trailing = 0 }) => { {currencyAmountFromCents(fee.amount)} + + + {balance} + + {[...Array(trailing)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/src/components/mui/table/extra-rows/PaymentRow.jsx b/src/components/mui/table/extra-rows/PaymentRow.jsx index dc61d88f..5e432a62 100644 --- a/src/components/mui/table/extra-rows/PaymentRow.jsx +++ b/src/components/mui/table/extra-rows/PaymentRow.jsx @@ -18,9 +18,10 @@ import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; import Typography from "@mui/material/Typography"; import { currencyAmountFromCents } from "../../../../utils/money"; -import { DATETIME_FORMAT, MILLISECONDS_IN_SECOND } from "../../../../utils/constants"; +import {DATETIME_FORMAT, MILLISECONDS_IN_SECOND, SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; +import TransactionType from "../../SponsorOrderGrid/components/TransactionType"; -const PaymentRow = ({ payment, colGap = 1, trailing = 0 }) => { +const PaymentRow = ({ payment, balance, colGap = 0, trailing = 0 }) => { if (!payment) return null; @@ -28,19 +29,19 @@ const PaymentRow = ({ payment, colGap = 1, trailing = 0 }) => { {T.translate("mui_table.pay")} - - {T.translate("mui_table.payment")} - + + + {T.translate("mui_table.payment")} + + {T.translate("mui_table.paid_via")} {payment.method} - - {moment(payment.created * MILLISECONDS_IN_SECOND).format(DATETIME_FORMAT)} @@ -54,7 +55,15 @@ const PaymentRow = ({ payment, colGap = 1, trailing = 0 }) => { variant="body2" sx={{ color: "success.main", fontWeight: 500 }} > - -{currencyAmountFromCents(payment.amount)} + {currencyAmountFromCents(payment.amount)} + + + + + {balance} {[...Array(trailing)].map((_, i) => ( diff --git a/src/components/mui/table/extra-rows/RefundRow.jsx b/src/components/mui/table/extra-rows/RefundRow.jsx index 9b3e9e27..131bf149 100644 --- a/src/components/mui/table/extra-rows/RefundRow.jsx +++ b/src/components/mui/table/extra-rows/RefundRow.jsx @@ -17,8 +17,10 @@ import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; import Typography from "@mui/material/Typography"; import { currencyAmountFromCents } from "../../../../utils/money"; +import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; +import TransactionType from "../../SponsorOrderGrid/components/TransactionType"; -const RefundRow = ({ refund, colGap = 1, trailing = 0 }) => { +const RefundRow = ({ refund, balance, colGap = 0, trailing = 0 }) => { if (!refund) return null; @@ -26,21 +28,18 @@ const RefundRow = ({ refund, colGap = 1, trailing = 0 }) => { {T.translate("mui_table.ref")} - - {T.translate("mui_table.refund")} - + + + {T.translate("mui_table.refund")} + + - {refund.reason} - - - - - {refund.status} + {refund.reason} {refund.status} {[...Array(colGap)].map((_, i) => ( @@ -52,7 +51,15 @@ const RefundRow = ({ refund, colGap = 1, trailing = 0 }) => { variant="body2" sx={{ color: "warning.main", fontWeight: 500 }} > - -{currencyAmountFromCents(refund.amount)} + {currencyAmountFromCents(refund.amount)} + + + + + {balance} {[...Array(trailing)].map((_, i) => ( diff --git a/src/components/mui/table/extra-rows/TotalRow.jsx b/src/components/mui/table/extra-rows/TotalRow.jsx index 41bceb6c..3f5deb74 100644 --- a/src/components/mui/table/extra-rows/TotalRow.jsx +++ b/src/components/mui/table/extra-rows/TotalRow.jsx @@ -15,31 +15,27 @@ import React from "react"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; import T from "i18n-react/dist/i18n-react"; +import {currencyAmountFromCents} from "../../../../utils/money"; -const TotalRow = ({ columns, targetCol, total, trailing = 0, label = null, rowSx = {} }) => { +const TotalRow = ({ total, colGap = 3, trailing = 0, label = null, rowSx = {} }) => { const totalLabel = label || T.translate("mui_table.total"); return ( - {columns.map((col, i) => { - if (i === 0) - return ( - - {totalLabel} - - ); - if (col.columnKey === targetCol) - return ( - - {total} - - ); - return ; - })} + + {totalLabel} + + {[...Array(colGap)].map((_, i) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + {currencyAmountFromCents(total)} + {[...Array(trailing)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key - + ))} ); diff --git a/src/i18n/en.json b/src/i18n/en.json index 4c3f4abd..07bbb2eb 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -129,9 +129,10 @@ }, "sponsor_order_grid": { "code": "Code", - "contents": "Contents", + "type": "Type", "addon": "Add-on", "details": "Details", + "balance": "Balance", "discount": "Discount", "amount": "Amount", "amount_due": "AMOUNT DUE", diff --git a/src/utils/constants.js b/src/utils/constants.js index 85e190fc..02d6d521 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -85,4 +85,12 @@ export const DATETIME_FORMAT = "MM/DD/YYYY hh:mm a"; export const SPONSOR_FORMS_METAFIELD_CLASS = { FORM: "Form", ITEM: "Item" +}; + +export const SPONSOR_ORDER_GRID_ITEM_TYPES = { + CHARGE: "Charge", + PAYMENT: "Payment", + REFUND: "Refund", + DISCOUNT: "Discount", + CANCELLED: "Cancelled" }; \ No newline at end of file From 898fe237f35a375f3c082ea610707441b47b0abb Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 2 Jun 2026 12:37:18 -0300 Subject: [PATCH 02/11] chore: style tweaks --- src/components/mui/InfoNote/index.jsx | 2 +- .../components/BalanceValue.jsx | 25 ++++ .../components/TransactionType.js | 8 +- src/components/mui/SponsorOrderGrid/index.js | 132 ++++++++---------- .../mui/table/extra-rows/DiscountRow.jsx | 28 ++-- .../mui/table/extra-rows/FeeRow.jsx | 19 +-- .../mui/table/extra-rows/NotesRow.jsx | 4 +- .../mui/table/extra-rows/PaymentRow.jsx | 24 ++-- .../mui/table/extra-rows/RefundRow.jsx | 30 ++-- .../mui/table/extra-rows/TotalRow.jsx | 20 +-- src/i18n/en.json | 3 +- 11 files changed, 150 insertions(+), 145 deletions(-) create mode 100644 src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx diff --git a/src/components/mui/InfoNote/index.jsx b/src/components/mui/InfoNote/index.jsx index 413735fa..de97eaf1 100644 --- a/src/components/mui/InfoNote/index.jsx +++ b/src/components/mui/InfoNote/index.jsx @@ -8,7 +8,7 @@ const InfoNote = ({ message, sx }) => ( - + {message} diff --git a/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx b/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx new file mode 100644 index 00000000..66eeec88 --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx @@ -0,0 +1,25 @@ +import React from "react"; +import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; +import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; +import RefreshIcon from '@mui/icons-material/Refresh'; +import DoNotDisturbIcon from '@mui/icons-material/DoNotDisturb'; + +import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; +import {Box} from "@mui/material"; +import Typography from "@mui/material/Typography"; +import {currencyAmountFromCents} from "../../../../utils/money"; + +const BalanceValue = ({value}) => { + const isNegative = value < 0; + const sign = isNegative ? "-" : ""; + const color = isNegative ? "primary.dark" : "text.disabled"; + const balance = `${sign}${currencyAmountFromCents(Math.abs(value))}`; + + return ( + + {balance} + + ); +} + +export default BalanceValue; \ No newline at end of file diff --git a/src/components/mui/SponsorOrderGrid/components/TransactionType.js b/src/components/mui/SponsorOrderGrid/components/TransactionType.js index 76650200..01c6473a 100644 --- a/src/components/mui/SponsorOrderGrid/components/TransactionType.js +++ b/src/components/mui/SponsorOrderGrid/components/TransactionType.js @@ -8,10 +8,10 @@ import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; import {Box} from "@mui/material"; const iconMap = { - [SPONSOR_ORDER_GRID_ITEM_TYPES.CHARGE]: {icon: ArrowUpwardIcon, color: "warning.main"}, - [SPONSOR_ORDER_GRID_ITEM_TYPES.PAYMENT]: {icon: ArrowDownwardIcon, color: "success.main"}, - [SPONSOR_ORDER_GRID_ITEM_TYPES.DISCOUNT]: {icon: ArrowDownwardIcon, color: "success.main"}, - [SPONSOR_ORDER_GRID_ITEM_TYPES.REFUND]: {icon: RefreshIcon, color: "error.main"}, + [SPONSOR_ORDER_GRID_ITEM_TYPES.CHARGE]: {icon: ArrowUpwardIcon, color: "warning.light"}, + [SPONSOR_ORDER_GRID_ITEM_TYPES.PAYMENT]: {icon: ArrowDownwardIcon, color: "success.light"}, + [SPONSOR_ORDER_GRID_ITEM_TYPES.DISCOUNT]: {icon: ArrowDownwardIcon, color: "success.light"}, + [SPONSOR_ORDER_GRID_ITEM_TYPES.REFUND]: {icon: RefreshIcon, color: "warning.light"}, [SPONSOR_ORDER_GRID_ITEM_TYPES.CANCELLED]: {icon: DoNotDisturbIcon, color: "default"}, } diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js index a335f89a..cf2a5084 100644 --- a/src/components/mui/SponsorOrderGrid/index.js +++ b/src/components/mui/SponsorOrderGrid/index.js @@ -16,7 +16,7 @@ import T from "i18n-react/dist/i18n-react"; import {DiscountRow, FeeRow, NotesRow, PaymentRow, RefundRow, TotalRow} from "../table/extra-rows"; import IconButton from "@mui/material/IconButton"; import DeleteIcon from "@mui/icons-material/Delete"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import UndoIcon from '@mui/icons-material/Undo'; import Box from "@mui/material/Box"; import Paper from "@mui/material/Paper"; import TableContainer from "@mui/material/TableContainer"; @@ -26,10 +26,13 @@ import TableCell from "@mui/material/TableCell"; import Table from "@mui/material/Table"; import TableHead from "@mui/material/TableHead"; import {currencyAmountFromCents} from "../../../utils/money"; -import {SPONSOR_FORMS_METAFIELD_CLASS, SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../utils/constants"; +import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../utils/constants"; import TransactionType from "./components/TransactionType"; +import BalanceValue from "./components/BalanceValue"; +import InfoNote from "../InfoNote"; +import Typography from "@mui/material/Typography"; -const mapOrderData = (forms, showItemDescription) => { +const mapOrderData = (forms) => { if (!forms) return []; return forms.map((form) => ({ @@ -37,36 +40,14 @@ const mapOrderData = (forms, showItemDescription) => { items: form.items .filter((it) => it.quantity) .map((it) => { - const formMetaFields = it.meta_fields.filter( - (mf) => mf.class_field === SPONSOR_FORMS_METAFIELD_CLASS.FORM - ); - - const itemDetails = [it.type?.name]; - - // item details - if (showItemDescription) { - itemDetails.push( - ...formMetaFields.map((mf) => { - const val = - mf.values?.length > 0 - ? mf.values.find((v) => v.id === mf.current_value)?.name - : mf.current_value; - return ( -
- {mf.name}: {val} -
- ); - }) - ); - - itemDetails.push(
); // spacer - itemDetails.push( -
- {T.translate("sponsor_order_grid.total")}: {it.quantity} -
- ); - } - + const itemDetails = [ + + {form.name} - {it.type?.name} + , + + {T.translate("sponsor_order_grid.total")}: {it.quantity} + + ]; const amount = currencyAmountFromCents(it.amount || 0); const lineId = it.line_id; const cancelled = !!it.canceled_by_id; @@ -75,10 +56,10 @@ const mapOrderData = (forms, showItemDescription) => { return { id: lineId, code: form.code, - name: form.name, + name: `${form.name} - ${it.type?.name}`, type, addon_name: form.addon_name, - item_name: itemDetails, + details: itemDetails, amount, amountValue: it.amount, cancelled @@ -94,22 +75,17 @@ const SponsorOrderGrid = ({ refunds, fees, total, - withDescription = false, onCancelForm, onUndoCancelForm }) => { - const data = mapOrderData(lines, withDescription); - const showActionCol = onCancelForm && onUndoCancelForm; - const trailingCols = showActionCol ? 1 : 0; + const data = mapOrderData(lines); + const canCancel = onCancelForm && onUndoCancelForm; + const trailingCols = canCancel ? 1 : 0; let balance = 0; - console.log("DATA: ", data, fees, payments, refunds, total); - const calculateBalance = (rowAmount, op = 1) => { - console.log("CALCULATE BALANCE: ", balance, rowAmount, op); balance = balance + (rowAmount * op); return balance; - // return currencyAmountFromCents(balance); } const columns = [ @@ -123,7 +99,7 @@ const SponsorOrderGrid = ({ render: (row) => () }, { - columnKey: "name", + columnKey: "details", header: T.translate("sponsor_order_grid.details") }, { @@ -136,11 +112,30 @@ const SponsorOrderGrid = ({ const colCount = columns.length + 1 + trailingCols; // 1 for balance, 1 for action col return ( - - + + {canCancel && ( + + )} + {/* TABLE HEADER */} @@ -154,23 +149,20 @@ const SponsorOrderGrid = ({ {T.translate("sponsor_order_grid.balance")} - {showActionCol && ( + {canCancel && ( {T.translate("sponsor_order_grid.action")} )} - + {data.map((form) => { const rows = form.items.map((row) => ( {(() => { @@ -178,53 +170,50 @@ const SponsorOrderGrid = ({ {col.render ? ( col.render(row) ) : ( - - {row[col.columnKey]} - + row[col.columnKey] )} )); - console.log("ROW: ", row); - // BALANCE COLUMN cols.push( - - {calculateBalance(row.cancelled ? 0 : row.amountValue)} - + ) // ACTION COLUMN - if (showActionCol) { + if (canCancel) { cols.push( - {row.cancelled ? ( onUndoCancelForm(row)}> - {" "} - {T.translate("general.undo").toUpperCase()} + ) : ( onCancelForm(row)}> )} - ) } @@ -238,7 +227,8 @@ const SponsorOrderGrid = ({ rows.push( @@ -283,7 +273,7 @@ const SponsorOrderGrid = ({ total={total} label={T.translate("sponsor_order_grid.amount_due")} trailing={trailingCols} - rowSx={{backgroundColor: "#F1F3F5"}} + rowSx={{backgroundColor: "#F1F3F5", "& td": {borderBottom: "none"}}} /> {data.length === 0 && ( diff --git a/src/components/mui/table/extra-rows/DiscountRow.jsx b/src/components/mui/table/extra-rows/DiscountRow.jsx index e2368ba9..0a18759b 100644 --- a/src/components/mui/table/extra-rows/DiscountRow.jsx +++ b/src/components/mui/table/extra-rows/DiscountRow.jsx @@ -19,11 +19,12 @@ import Typography from "@mui/material/Typography"; import { currencyAmountFromCents } from "../../../../utils/money"; import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; import TransactionType from "../../SponsorOrderGrid/components/TransactionType"; +import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue"; -const DiscountRow = ({ discount, discountTotal, balance, colGap = 0, trailing = 0 }) => { +const DiscountRow = ({ discount, discountCents, balance, colGap = 0, trailing = 0 }) => { - if (discountTotal === 0) return null; + if (discountCents === 0) return null; return ( @@ -31,15 +32,15 @@ const DiscountRow = ({ discount, discountTotal, balance, colGap = 0, trailing = {T.translate("mui_table.discount")} - + {discount} @@ -47,21 +48,16 @@ const DiscountRow = ({ discount, discountTotal, balance, colGap = 0, trailing = // eslint-disable-next-line react/no-array-index-key ))} - + - {currencyAmountFromCents(discountTotal)} + {currencyAmountFromCents(discountCents)} - - - {balance} - + + {[...Array(trailing)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/src/components/mui/table/extra-rows/FeeRow.jsx b/src/components/mui/table/extra-rows/FeeRow.jsx index 66e8c6c2..7d2a9899 100644 --- a/src/components/mui/table/extra-rows/FeeRow.jsx +++ b/src/components/mui/table/extra-rows/FeeRow.jsx @@ -19,6 +19,7 @@ import Typography from "@mui/material/Typography"; import { currencyAmountFromCents } from "../../../../utils/money"; import TransactionType from "../../SponsorOrderGrid/components/TransactionType"; import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; +import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue"; const FeeRow = ({ fee, balance, colGap = 0, trailing = 0 }) => { if (!fee) return null; @@ -30,7 +31,7 @@ const FeeRow = ({ fee, balance, colGap = 0, trailing = 0 }) => { - + {fee.title} @@ -38,21 +39,13 @@ const FeeRow = ({ fee, balance, colGap = 0, trailing = 0 }) => { // eslint-disable-next-line react/no-array-index-key ))} - - + + {currencyAmountFromCents(fee.amount)} - - - {balance} - + + {[...Array(trailing)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/src/components/mui/table/extra-rows/NotesRow.jsx b/src/components/mui/table/extra-rows/NotesRow.jsx index a71d4d4e..8951b6d4 100644 --- a/src/components/mui/table/extra-rows/NotesRow.jsx +++ b/src/components/mui/table/extra-rows/NotesRow.jsx @@ -24,8 +24,8 @@ const NotesRow = ({ colCount, note, showCode = false }) => { {showCode && ( {T.translate("mui_table.note")} )} - - + + {note} diff --git a/src/components/mui/table/extra-rows/PaymentRow.jsx b/src/components/mui/table/extra-rows/PaymentRow.jsx index 5e432a62..a151c0d2 100644 --- a/src/components/mui/table/extra-rows/PaymentRow.jsx +++ b/src/components/mui/table/extra-rows/PaymentRow.jsx @@ -20,6 +20,7 @@ import Typography from "@mui/material/Typography"; import { currencyAmountFromCents } from "../../../../utils/money"; import {DATETIME_FORMAT, MILLISECONDS_IN_SECOND, SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; import TransactionType from "../../SponsorOrderGrid/components/TransactionType"; +import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue"; const PaymentRow = ({ payment, balance, colGap = 0, trailing = 0 }) => { @@ -31,18 +32,18 @@ const PaymentRow = ({ payment, balance, colGap = 0, trailing = 0 }) => { {T.translate("mui_table.payment")} - + {T.translate("mui_table.paid_via")} {payment.method} - + {moment(payment.created * MILLISECONDS_IN_SECOND).format(DATETIME_FORMAT)} @@ -50,21 +51,16 @@ const PaymentRow = ({ payment, balance, colGap = 0, trailing = 0 }) => { // eslint-disable-next-line react/no-array-index-key ))} - + {currencyAmountFromCents(payment.amount)} - - - {balance} - + + {[...Array(trailing)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/src/components/mui/table/extra-rows/RefundRow.jsx b/src/components/mui/table/extra-rows/RefundRow.jsx index 131bf149..8970cd4a 100644 --- a/src/components/mui/table/extra-rows/RefundRow.jsx +++ b/src/components/mui/table/extra-rows/RefundRow.jsx @@ -17,8 +17,10 @@ import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; import Typography from "@mui/material/Typography"; import { currencyAmountFromCents } from "../../../../utils/money"; -import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; +import {DATETIME_FORMAT, MILLISECONDS_IN_SECOND, SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; import TransactionType from "../../SponsorOrderGrid/components/TransactionType"; +import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue"; +import moment from "moment-timezone"; const RefundRow = ({ refund, balance, colGap = 0, trailing = 0 }) => { @@ -30,37 +32,35 @@ const RefundRow = ({ refund, balance, colGap = 0, trailing = 0 }) => { {T.translate("mui_table.refund")} - - {refund.reason} {refund.status} + + {refund.reason} + + + {refund.status} {[...Array(colGap)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key ))} - + {currencyAmountFromCents(refund.amount)} - - - {balance} - + + {[...Array(trailing)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key diff --git a/src/components/mui/table/extra-rows/TotalRow.jsx b/src/components/mui/table/extra-rows/TotalRow.jsx index 3f5deb74..e38120ad 100644 --- a/src/components/mui/table/extra-rows/TotalRow.jsx +++ b/src/components/mui/table/extra-rows/TotalRow.jsx @@ -14,28 +14,32 @@ import React from "react"; import TableCell from "@mui/material/TableCell"; import TableRow from "@mui/material/TableRow"; +import Typography from "@mui/material/Typography"; import T from "i18n-react/dist/i18n-react"; import {currencyAmountFromCents} from "../../../../utils/money"; -const TotalRow = ({ total, colGap = 3, trailing = 0, label = null, rowSx = {} }) => { - +const TotalRow = ({total, colGap = 3, trailing = 0, label = null, rowSx = {}}) => { const totalLabel = label || T.translate("mui_table.total"); + const isNegative = total < 0; + const sign = isNegative ? "-" : ""; + const color = isNegative ? "primary.dark" : (total === 0 ? "text.primary" : "error.main"); + const totalStr = `${sign}${currencyAmountFromCents(Math.abs(total))}`; return ( - - {totalLabel} + + {totalLabel} {[...Array(colGap)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key - + ))} - - {currencyAmountFromCents(total)} + + {totalStr} {[...Array(trailing)].map((_, i) => ( // eslint-disable-next-line react/no-array-index-key - + ))} ); diff --git a/src/i18n/en.json b/src/i18n/en.json index 07bbb2eb..8f0a3246 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -138,7 +138,8 @@ "amount_due": "AMOUNT DUE", "total": "Total", "rate": "Rate", - "action": "Action" + "action": "Action", + "cancel_info_note": "Active order items can be canceled. Canceled items show an undo action to restore them. Refund and payment rows are display-only." }, "upload_input_v3": { "no_post_url": "No Post URL", From 0765ba9cc19a69f86bfe517df24c965bc7377a6b Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 3 Jun 2026 17:43:03 -0300 Subject: [PATCH 03/11] chore: more changes --- .../components/BalanceValue.jsx | 20 +- .../components/CancelledItems.jsx | 44 ++ .../components/ReconciliationBox.jsx | 61 +++ .../components/TotalFooter.jsx | 50 +++ .../components/TransactionType.js | 16 +- src/components/mui/SponsorOrderGrid/index.js | 397 ++++++++++-------- src/i18n/en.json | 10 +- 7 files changed, 409 insertions(+), 189 deletions(-) create mode 100644 src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx create mode 100644 src/components/mui/SponsorOrderGrid/components/ReconciliationBox.jsx create mode 100644 src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx diff --git a/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx b/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx index 66eeec88..fd5da470 100644 --- a/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx +++ b/src/components/mui/SponsorOrderGrid/components/BalanceValue.jsx @@ -1,11 +1,17 @@ -import React from "react"; -import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; -import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import DoNotDisturbIcon from '@mui/icons-material/DoNotDisturb'; +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ -import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; -import {Box} from "@mui/material"; +import React from "react"; import Typography from "@mui/material/Typography"; import {currencyAmountFromCents} from "../../../../utils/money"; diff --git a/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx b/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx new file mode 100644 index 00000000..81e54e6c --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx @@ -0,0 +1,44 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +import React from "react"; +import T from "i18n-react/dist/i18n-react"; +import Typography from "@mui/material/Typography"; +import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb"; +import Box from "@mui/material/Box"; +import Link from "@mui/material/Link"; + +const CancelledItems = ({cancelledItems, sx = {}}) => { + + if (cancelledItems.length === 0) return null; + + return ( + + + + {T.translate("sponsor_order_grid.cancelled_items", {count: cancelledItems.length})} + + {cancelledItems.map((item) => ( + + {item.formCode} - {item.itemCode} + + ))} + + ); +} + +export default CancelledItems; \ No newline at end of file diff --git a/src/components/mui/SponsorOrderGrid/components/ReconciliationBox.jsx b/src/components/mui/SponsorOrderGrid/components/ReconciliationBox.jsx new file mode 100644 index 00000000..bdc95db2 --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/ReconciliationBox.jsx @@ -0,0 +1,61 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +import React from "react"; +import Typography from "@mui/material/Typography"; +import T from "i18n-react/dist/i18n-react"; +import Divider from "@mui/material/Divider"; +import Box from "@mui/material/Box"; +import {currencyAmountFromCents} from "../../../../utils/money"; + +const ReconciliationBox = ({cancelledTotal, refundsTotal, retained, credited}) => { + const totalColor = retained > 0 ? "error.dark" : "success.dark"; + const totalLabel = retained > 0 ? "retained" : (credited > 0 ? "credited" : "balance"); + + return ( + + + {T.translate("sponsor_order_grid.reconciliation")} + + + + {T.translate("sponsor_order_grid.cancelled")} + + + {currencyAmountFromCents(cancelledTotal)} + + + + + {T.translate("sponsor_order_grid.refunded")} + + + {currencyAmountFromCents(refundsTotal)} + + + + + + {T.translate(`sponsor_order_grid.${totalLabel}`)} + + + {currencyAmountFromCents(retained)} + + + + + + ); +} + +export default ReconciliationBox; \ No newline at end of file diff --git a/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx b/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx new file mode 100644 index 00000000..0d12c57e --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx @@ -0,0 +1,50 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +import React from "react"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import T from "i18n-react/dist/i18n-react"; +import {currencyAmountFromCents} from "../../../../utils/money"; + +const TotalFooter = ({total}) => { + const isNegative = total < 0; + const sign = isNegative ? "-" : ""; + const color = isNegative ? "primary.dark" : (total === 0 ? "text.primary" : "error.main"); + const totalStr = `${sign}${currencyAmountFromCents(Math.abs(total))}`; + + return ( + + + {T.translate("sponsor_order_grid.amount_due")} + + + {totalStr} + + + ); +} + +export default TotalFooter; \ No newline at end of file diff --git a/src/components/mui/SponsorOrderGrid/components/TransactionType.js b/src/components/mui/SponsorOrderGrid/components/TransactionType.js index 01c6473a..e979cf3c 100644 --- a/src/components/mui/SponsorOrderGrid/components/TransactionType.js +++ b/src/components/mui/SponsorOrderGrid/components/TransactionType.js @@ -1,11 +1,23 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + import React from "react"; import ArrowUpwardIcon from '@mui/icons-material/ArrowUpward'; import ArrowDownwardIcon from '@mui/icons-material/ArrowDownward'; import RefreshIcon from '@mui/icons-material/Refresh'; import DoNotDisturbIcon from '@mui/icons-material/DoNotDisturb'; - import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../../utils/constants"; -import {Box} from "@mui/material"; +import Box from "@mui/material/Box"; const iconMap = { [SPONSOR_ORDER_GRID_ITEM_TYPES.CHARGE]: {icon: ArrowUpwardIcon, color: "warning.light"}, diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js index cf2a5084..9af5b30c 100644 --- a/src/components/mui/SponsorOrderGrid/index.js +++ b/src/components/mui/SponsorOrderGrid/index.js @@ -13,24 +13,28 @@ import React from "react"; import T from "i18n-react/dist/i18n-react"; -import {DiscountRow, FeeRow, NotesRow, PaymentRow, RefundRow, TotalRow} from "../table/extra-rows"; -import IconButton from "@mui/material/IconButton"; -import DeleteIcon from "@mui/icons-material/Delete"; -import UndoIcon from '@mui/icons-material/Undo'; import Box from "@mui/material/Box"; -import Paper from "@mui/material/Paper"; import TableContainer from "@mui/material/TableContainer"; import TableRow from "@mui/material/TableRow"; import TableBody from "@mui/material/TableBody"; import TableCell from "@mui/material/TableCell"; import Table from "@mui/material/Table"; import TableHead from "@mui/material/TableHead"; -import {currencyAmountFromCents} from "../../../utils/money"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import IconButton from "@mui/material/IconButton"; +import UndoIcon from "@mui/icons-material/Undo"; +import DeleteIcon from "@mui/icons-material/Delete"; +import {DiscountRow, FeeRow, NotesRow, PaymentRow, RefundRow, TotalRow} from "../table/extra-rows"; import {SPONSOR_ORDER_GRID_ITEM_TYPES} from "../../../utils/constants"; +import InfoNote from "../InfoNote"; +import {currencyAmountFromCents} from "../../../utils/money"; import TransactionType from "./components/TransactionType"; +import {formatEpoch} from "../../../utils/methods"; +import TotalFooter from "./components/TotalFooter"; +import ReconciliationBox from "./components/ReconciliationBox"; +import CancelledItems from "./components/CancelledItems"; import BalanceValue from "./components/BalanceValue"; -import InfoNote from "../InfoNote"; -import Typography from "@mui/material/Typography"; const mapOrderData = (forms) => { if (!forms) return []; @@ -40,14 +44,6 @@ const mapOrderData = (forms) => { items: form.items .filter((it) => it.quantity) .map((it) => { - const itemDetails = [ - - {form.name} - {it.type?.name} - , - - {T.translate("sponsor_order_grid.total")}: {it.quantity} - - ]; const amount = currencyAmountFromCents(it.amount || 0); const lineId = it.line_id; const cancelled = !!it.canceled_by_id; @@ -55,30 +51,46 @@ const mapOrderData = (forms) => { return { id: lineId, - code: form.code, - name: `${form.name} - ${it.type?.name}`, + formCode: form.code, + itemName: it.type?.name, + itemCode: it.type?.code, + quantity: it.quantity, type, - addon_name: form.addon_name, - details: itemDetails, amount, amountValue: it.amount, - cancelled + cancelled, + cancelledBy: T.translate("sponsor_order_grid.cancelled_by", { + user: it.canceled_by_full_name, + date: formatEpoch(it.canceled_at) + }), }; }) })); }; const SponsorOrderGrid = ({ - lines, - notes, - payments, - refunds, - fees, - total, + title = T.translate("sponsor_order_grid.title"), + order, + withReconciliation = false, + withCancelledItemsHeader = false, onCancelForm, onUndoCancelForm }) => { - const data = mapOrderData(lines); + + const { + forms, + fees, + payments, + refunds, + notes, + total, + retained, + credited_to_payment_method: credited, + cancelled_total: cancelledTotal, + refunds_total: refundsTotal + } = order; + const data = mapOrderData(forms); + const cancelledItems = data.flatMap((form) => form.items.filter((it) => it.cancelled)); const canCancel = onCancelForm && onUndoCancelForm; const trailingCols = canCancel ? 1 : 0; let balance = 0; @@ -90,7 +102,7 @@ const SponsorOrderGrid = ({ const columns = [ { - columnKey: "code", + columnKey: "formCode", header: T.translate("sponsor_order_grid.code") }, { @@ -100,193 +112,220 @@ const SponsorOrderGrid = ({ }, { columnKey: "details", - header: T.translate("sponsor_order_grid.details") + header: T.translate("sponsor_order_grid.details"), + render: (row) => ( + <> + + {row.itemName} - {T.translate("sponsor_order_grid.total")}: {row.quantity} + + {row.cancelled && + + {row.cancelledBy} + + } + + ) }, { columnKey: "amount", header: T.translate("sponsor_order_grid.amount"), - align: "right" + align: "right", + strikethrough: true, } ]; const colCount = columns.length + 1 + trailingCols; // 1 for balance, 1 for action col return ( - + + + {title && ( + + {title} + + )} + {withCancelledItemsHeader && ( + + )} + + {canCancel && ( )} - - -
- {/* TABLE HEADER */} - - - {columns.map((col) => ( - - {col.header} - - ))} - - {T.translate("sponsor_order_grid.balance")} + +
+ {/* TABLE HEADER */} + + + {columns.map((col) => ( + + {col.header} - {canCancel && ( - - {T.translate("sponsor_order_grid.action")} - - )} - - - - {data.map((form) => { - const rows = form.items.map((row) => ( - - {(() => { - const cols = columns.map((col) => ( - - {col.render ? ( - col.render(row) - ) : ( - row[col.columnKey] - )} - - )); + ))} + + {T.translate("sponsor_order_grid.balance")} + + {canCancel && ( + + {T.translate("sponsor_order_grid.action")} + + )} + + + + {data.map((form) => { + const rows = form.items.map((row) => ( + + {(() => { + const cols = columns.map((col) => ( + + {col.render ? ( + col.render(row) + ) : ( + row[col.columnKey] + )} + + )); - // BALANCE COLUMN + // BALANCE COLUMN + cols.push( + + + + ) + + // ACTION COLUMN + if (canCancel) { cols.push( - + {row.cancelled ? ( + onUndoCancelForm(row)}> + + + ) : ( + onCancelForm(row)}> + + + )} ) + } - // ACTION COLUMN - if (canCancel) { - cols.push( - - {row.cancelled ? ( - onUndoCancelForm(row)}> - - - ) : ( - onCancelForm(row)}> - - - )} - - ) - } - - return cols; - })()} + return cols; + })()} - - )); - - rows.push( - - ); + + )); - return rows; - })} - {fees && fees.map((fee) => ( - - ))} - {refunds && refunds.map((refund) => ( - - ))} - {payments && payments.map((payment) => ( - - ))} - {notes && notes.map((note) => ( - - ))} + ); + return rows; + })} + {fees && fees.map((fee) => ( + + ))} + {refunds && refunds.map((refund) => ( + + ))} + {payments && payments.map((payment) => ( + + ))} + {notes && notes.map((note) => ( + + ))} + + {/* When using reconciliation, we show the total at the end */} + {!withReconciliation && + } + {data.length === 0 && ( + + + {T.translate("mui_table.no_items")} + + + )} + +
+
- {data.length === 0 && ( - - - {T.translate("mui_table.no_items")} - - - )} - - - -
+ {withReconciliation && + + + + + + }
); }; diff --git a/src/i18n/en.json b/src/i18n/en.json index 8f0a3246..b90b191e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -128,6 +128,7 @@ "payment_confirmation_error": "Payment confirmation failed" }, "sponsor_order_grid": { + "title": "Order Items Ledger", "code": "Code", "type": "Type", "addon": "Add-on", @@ -139,7 +140,14 @@ "total": "Total", "rate": "Rate", "action": "Action", - "cancel_info_note": "Active order items can be canceled. Canceled items show an undo action to restore them. Refund and payment rows are display-only." + "cancel_info_note": "Active order items can be canceled. Canceled items show an undo action to restore them. Refund and payment rows are display-only.", + "cancelled_by": "Cancelled {date} by {user}", + "cancelled_items": "Cancelled items ({count}):", + "reconciliation": "Reconciliation", + "cancelled": "Cancelled", + "refunded": "Refunded", + "retained": "Retained as cancellation fee", + "credited": "Credited to Payment Method" }, "upload_input_v3": { "no_post_url": "No Post URL", From 9ce9ee05b63eb43c47641786e59190c527eaa7fc Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 4 Jun 2026 12:39:13 -0300 Subject: [PATCH 04/11] v5.0.33-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index dfe67991..97dfc6b6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.32", + "version": "5.0.33-beta.0", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From 9399332ebdcf53a8702408ea0707334ba9090a86 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 4 Jun 2026 15:27:26 -0300 Subject: [PATCH 05/11] chore: PR review and fix tests --- .../__tests__/SponsorOrderGrid.test.js | 99 ++++++++++--------- .../components/CancelledItems.jsx | 1 + .../components/ReconciliationBox.jsx | 7 +- .../components/TotalFooter.jsx | 7 +- .../components/TransactionType.js | 6 +- .../__tests__/BalanceValue.test.jsx | 38 +++++++ .../__tests__/CancelledItems.test.jsx | 54 ++++++++++ .../__tests__/ReconciliationBox.test.jsx | 59 +++++++++++ .../components/__tests__/TotalFooter.test.jsx | 53 ++++++++++ .../__tests__/TransactionType.test.jsx | 55 +++++++++++ src/components/mui/SponsorOrderGrid/index.js | 7 +- .../mui/__tests__/payment-row.test.js | 2 +- .../mui/__tests__/refund-row.test.js | 2 +- .../mui/__tests__/total-row.test.js | 24 ++--- .../mui/table/extra-rows/DiscountRow.jsx | 2 +- .../mui/table/extra-rows/TotalRow.jsx | 7 +- 16 files changed, 345 insertions(+), 78 deletions(-) create mode 100644 src/components/mui/SponsorOrderGrid/components/__tests__/BalanceValue.test.jsx create mode 100644 src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx create mode 100644 src/components/mui/SponsorOrderGrid/components/__tests__/ReconciliationBox.test.jsx create mode 100644 src/components/mui/SponsorOrderGrid/components/__tests__/TotalFooter.test.jsx create mode 100644 src/components/mui/SponsorOrderGrid/components/__tests__/TransactionType.test.jsx diff --git a/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js b/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js index 763dde37..b4422a86 100644 --- a/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js +++ b/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js @@ -21,9 +21,14 @@ jest.mock("../../../../utils/money", () => ({ })); jest.mock("../../../../utils/constants", () => ({ + ...jest.requireActual("../../../../utils/constants"), SPONSOR_FORMS_METAFIELD_CLASS: { FORM: "Form", ITEM: "Item" } })); +jest.mock("../../../../utils/methods", () => ({ + formatEpoch: () => "2026-01-01" +})); + import React from "react"; import { render, screen, fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; @@ -33,9 +38,8 @@ const makeItem = (overrides = {}) => ({ line_id: 1, quantity: 1, amount: 10000, - current_rate: 5000, canceled_by_id: null, - type: { name: "Booth" }, + type: { name: "Booth", code: "BOOTH" }, meta_fields: [], ...overrides }); @@ -44,62 +48,63 @@ const makeForm = (overrides = {}) => ({ id: 10, code: "GOLD", name: "Gold Sponsor", - addon_name: "Premium", discount: null, - discount_total: null, + discount_in_cents: null, items: [makeItem()], ...overrides }); const defaultProps = { - lines: [makeForm()], - total: 10000 + order: { + forms: [makeForm()], + total: 10000 + } }; describe("SponsorOrderGrid", () => { test("renders column headers", () => { render(); expect(screen.getByText("sponsor_order_grid.code")).toBeInTheDocument(); - expect(screen.getByText("sponsor_order_grid.contents")).toBeInTheDocument(); - expect(screen.getByText("sponsor_order_grid.addon")).toBeInTheDocument(); + expect(screen.getByText("sponsor_order_grid.type")).toBeInTheDocument(); expect(screen.getByText("sponsor_order_grid.details")).toBeInTheDocument(); - expect(screen.getByText("sponsor_order_grid.rate")).toBeInTheDocument(); expect(screen.getByText("sponsor_order_grid.amount")).toBeInTheDocument(); + expect(screen.getByText("sponsor_order_grid.balance")).toBeInTheDocument(); }); - test("renders item code and name", () => { + test("renders item form code", () => { render(); expect(screen.getByText("GOLD")).toBeInTheDocument(); - expect(screen.getByText("Gold Sponsor")).toBeInTheDocument(); }); - test("renders formatted amount and rate", () => { + test("renders item name in details column", () => { + render(); + expect(screen.getByText(/Booth/)).toBeInTheDocument(); + }); + + test("renders formatted charge amount", () => { render(); - expect(screen.getByText("$100.00")).toBeInTheDocument(); - expect(screen.getByText("$50.00")).toBeInTheDocument(); + expect(screen.getAllByText("$100.00").length).toBeGreaterThan(0); }); - test("renders no-items message when lines is empty", () => { - render(); + test("renders no-items message when forms is empty", () => { + render(); expect(screen.getByText("mui_table.no_items")).toBeInTheDocument(); }); - test("renders no-items message when lines is undefined", () => { - render(); + test("renders no-items message when forms is undefined", () => { + render(); expect(screen.getByText("mui_table.no_items")).toBeInTheDocument(); }); test("filters out items with zero quantity", () => { - const lines = [makeForm({ items: [makeItem({ quantity: 0 })] })]; - render(); + const order = { forms: [makeForm({ items: [makeItem({ quantity: 0 })] })], total: 0 }; + render(); expect(screen.queryByText("$100.00")).not.toBeInTheDocument(); }); test("does not render action column when callbacks are absent", () => { render(); - expect( - screen.queryByText("sponsor_order_grid.action") - ).not.toBeInTheDocument(); + expect(screen.queryByText("sponsor_order_grid.action")).not.toBeInTheDocument(); }); test("renders action column header when both callbacks are provided", () => { @@ -122,21 +127,17 @@ describe("SponsorOrderGrid", () => { onUndoCancelForm={jest.fn()} /> ); - const deleteButton = screen.getByTestId - ? document.querySelector('[data-testid="DeleteIcon"]') - : null; - const button = document.querySelector("button[aria-label]") || document.querySelector("tbody button"); + const button = document.querySelector("tbody button"); fireEvent.click(button); expect(onCancelForm).toHaveBeenCalledTimes(1); }); test("renders undo button for cancelled item and calls onUndoCancelForm on click", () => { const onUndoCancelForm = jest.fn(); - const lines = [makeForm({ items: [makeItem({ canceled_by_id: 99 })] })]; + const order = { forms: [makeForm({ items: [makeItem({ canceled_by_id: 99 })] })], total: 0 }; render( @@ -146,26 +147,26 @@ describe("SponsorOrderGrid", () => { expect(onUndoCancelForm).toHaveBeenCalledTimes(1); }); - test("uses amountDue label when amountDue prop is provided", () => { - render(); - expect( - screen.getByText("sponsor_order_grid.amount_due") - ).toBeInTheDocument(); + test("renders amount_due label in total row", () => { + render(); + expect(screen.getByText("sponsor_order_grid.amount_due")).toBeInTheDocument(); + }); + + test("renders reconciliation section when withReconciliation is true", () => { + const order = { + forms: [], + total: 10000, + retained: 2000, + credited_to_payment_method: 0, + cancelled_total: 5000, + refunds_total: 3000 + }; + render(); + expect(screen.getByText("sponsor_order_grid.reconciliation")).toBeInTheDocument(); }); - test("renders meta_field values in item details", () => { - const item = makeItem({ - meta_fields: [ - { - id: 1, - name: "Booth Size", - class_field: "Form", - current_value: "Large", - values: [] - } - ] - }); - render(); - expect(screen.getByText(/Booth Size/)).toBeInTheDocument(); + test("does not render reconciliation section by default", () => { + render(); + expect(screen.queryByText("sponsor_order_grid.reconciliation")).not.toBeInTheDocument(); }); }); diff --git a/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx b/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx index 81e54e6c..583d7593 100644 --- a/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx +++ b/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx @@ -30,6 +30,7 @@ const CancelledItems = ({cancelledItems, sx = {}}) => { {cancelledItems.map((item) => ( { const totalColor = retained > 0 ? "error.dark" : "success.dark"; const totalLabel = retained > 0 ? "retained" : (credited > 0 ? "credited" : "balance"); + const totalValue = retained > 0 ? retained : (credited > 0 ? credited : retained); return ( @@ -32,7 +33,7 @@ const ReconciliationBox = ({cancelledTotal, refundsTotal, retained, credited}) = {T.translate("sponsor_order_grid.cancelled")} - {currencyAmountFromCents(cancelledTotal)} + {currencyAmountFromCents(cancelledTotal ?? 0)} @@ -40,7 +41,7 @@ const ReconciliationBox = ({cancelledTotal, refundsTotal, retained, credited}) = {T.translate("sponsor_order_grid.refunded")} - {currencyAmountFromCents(refundsTotal)} + {currencyAmountFromCents(refundsTotal ?? 0)} @@ -49,7 +50,7 @@ const ReconciliationBox = ({cancelledTotal, refundsTotal, retained, credited}) = {T.translate(`sponsor_order_grid.${totalLabel}`)} - {currencyAmountFromCents(retained)} + {currencyAmountFromCents(totalValue ?? 0)}
diff --git a/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx b/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx index 0d12c57e..86587a1f 100644 --- a/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx +++ b/src/components/mui/SponsorOrderGrid/components/TotalFooter.jsx @@ -18,10 +18,11 @@ import T from "i18n-react/dist/i18n-react"; import {currencyAmountFromCents} from "../../../../utils/money"; const TotalFooter = ({total}) => { - const isNegative = total < 0; + const safetotal = total ?? 0; + const isNegative = safetotal < 0; const sign = isNegative ? "-" : ""; - const color = isNegative ? "primary.dark" : (total === 0 ? "text.primary" : "error.main"); - const totalStr = `${sign}${currencyAmountFromCents(Math.abs(total))}`; + const color = isNegative ? "primary.dark" : (safetotal === 0 ? "text.primary" : "error.main"); + const totalStr = `${sign}${currencyAmountFromCents(Math.abs(safetotal))}`; return ( { - const Icon = iconMap[type].icon; + const meta = iconMap[type]; + if (!meta) return null; + const Icon = meta.icon; return ( - + {children || type} ); diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/BalanceValue.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/BalanceValue.test.jsx new file mode 100644 index 00000000..029220e3 --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/__tests__/BalanceValue.test.jsx @@ -0,0 +1,38 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +jest.mock("../../../../../utils/money", () => ({ + currencyAmountFromCents: (amount) => `$${(amount / 100).toFixed(2)}` +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import BalanceValue from "../BalanceValue"; + +describe("BalanceValue", () => { + test("renders a positive balance", () => { + render(); + expect(screen.getByText("$100.00")).toBeInTheDocument(); + }); + + test("renders a negative balance with a leading dash", () => { + render(); + expect(screen.getByText("-$50.00")).toBeInTheDocument(); + }); + + test("renders zero balance", () => { + render(); + expect(screen.getByText("$0.00")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx new file mode 100644 index 00000000..b96f0b26 --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx @@ -0,0 +1,54 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key, params) => `${key}(${JSON.stringify(params)})` } +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import CancelledItems from "../CancelledItems"; + +describe("CancelledItems", () => { + test("renders nothing when cancelledItems is empty", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + test("renders a link for each cancelled item", () => { + const items = [ + { id: 1, formCode: "GOLD", itemCode: "BOOTH" }, + { id: 2, formCode: "SILVER", itemCode: "TABLE" } + ]; + render(); + expect(screen.getByText("GOLD - BOOTH")).toBeInTheDocument(); + expect(screen.getByText("SILVER - TABLE")).toBeInTheDocument(); + }); + + test("each link href anchors to the item id", () => { + const items = [{ id: 42, formCode: "G", itemCode: "B" }]; + render(); + expect(screen.getByRole("link")).toHaveAttribute("href", "#item-42"); + }); + + test("item count is shown in the label", () => { + const items = [ + { id: 1, formCode: "A", itemCode: "X" }, + { id: 2, formCode: "B", itemCode: "Y" } + ]; + render(); + expect(screen.getByText(/cancelled_items/)).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/ReconciliationBox.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/ReconciliationBox.test.jsx new file mode 100644 index 00000000..e1f05f66 --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/__tests__/ReconciliationBox.test.jsx @@ -0,0 +1,59 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock("../../../../../utils/money", () => ({ + currencyAmountFromCents: (amount) => `$${(amount / 100).toFixed(2)}` +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import ReconciliationBox from "../ReconciliationBox"; + +const base = { cancelledTotal: 20000, refundsTotal: 5000 }; + +describe("ReconciliationBox", () => { + test("renders cancelled and refunded totals", () => { + render(); + expect(screen.getByText("$200.00")).toBeInTheDocument(); + expect(screen.getByText("$50.00")).toBeInTheDocument(); + }); + + test("shows retained label and retained amount when retained > 0", () => { + render(); + expect(screen.getByText("sponsor_order_grid.retained")).toBeInTheDocument(); + expect(screen.getByText("$30.00")).toBeInTheDocument(); + }); + + test("shows credited label and credited amount when retained is 0 and credited > 0", () => { + render(); + expect(screen.getByText("sponsor_order_grid.credited")).toBeInTheDocument(); + expect(screen.getByText("$80.00")).toBeInTheDocument(); + }); + + test("shows balance label when both retained and credited are 0", () => { + render(); + expect(screen.getByText("sponsor_order_grid.balance")).toBeInTheDocument(); + }); + + test("defaults totals to $0.00 when nullish", () => { + render(); + const zeros = screen.getAllByText("$0.00"); + expect(zeros.length).toBeGreaterThanOrEqual(2); + }); +}); diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/TotalFooter.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/TotalFooter.test.jsx new file mode 100644 index 00000000..c262a5ad --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/__tests__/TotalFooter.test.jsx @@ -0,0 +1,53 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +jest.mock("i18n-react/dist/i18n-react", () => ({ + __esModule: true, + default: { translate: (key) => key } +})); + +jest.mock("../../../../../utils/money", () => ({ + currencyAmountFromCents: (amount) => `$${(amount / 100).toFixed(2)}` +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import TotalFooter from "../TotalFooter"; + +describe("TotalFooter", () => { + test("renders amount_due label", () => { + render(); + expect(screen.getByText("sponsor_order_grid.amount_due")).toBeInTheDocument(); + }); + + test("renders formatted total", () => { + render(); + expect(screen.getByText("$100.00")).toBeInTheDocument(); + }); + + test("renders negative total with leading dash", () => { + render(); + expect(screen.getByText("-$50.00")).toBeInTheDocument(); + }); + + test("renders $0.00 when total is undefined", () => { + render(); + expect(screen.getByText("$0.00")).toBeInTheDocument(); + }); + + test("renders $0.00 for a zero total", () => { + render(); + expect(screen.getByText("$0.00")).toBeInTheDocument(); + }); +}); diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/TransactionType.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/TransactionType.test.jsx new file mode 100644 index 00000000..c1b87b1a --- /dev/null +++ b/src/components/mui/SponsorOrderGrid/components/__tests__/TransactionType.test.jsx @@ -0,0 +1,55 @@ +/** + * Copyright 2026 OpenStack Foundation + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * */ + +jest.mock("../../../../../utils/constants", () => ({ + ...jest.requireActual("../../../../../utils/constants") +})); + +import React from "react"; +import { render, screen } from "@testing-library/react"; +import "@testing-library/jest-dom"; +import TransactionType from "../TransactionType"; +import { SPONSOR_ORDER_GRID_ITEM_TYPES } from "../../../../../utils/constants"; + +describe("TransactionType", () => { + test("renders null for an unknown type", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + test("renders null when type is undefined", () => { + const { container } = render(); + expect(container.firstChild).toBeNull(); + }); + + test("renders children text when type is known", () => { + render( + + Charge label + + ); + expect(screen.getByText("Charge label")).toBeInTheDocument(); + }); + + test("falls back to rendering the type string when no children provided", () => { + render(); + expect(screen.getByText(SPONSOR_ORDER_GRID_ITEM_TYPES.PAYMENT)).toBeInTheDocument(); + }); + + test("renders for every known type without crashing", () => { + Object.values(SPONSOR_ORDER_GRID_ITEM_TYPES).forEach((type) => { + const { unmount } = render(); + unmount(); + }); + }); +}); diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js index 9af5b30c..103abc87 100644 --- a/src/components/mui/SponsorOrderGrid/index.js +++ b/src/components/mui/SponsorOrderGrid/index.js @@ -227,7 +227,7 @@ const SponsorOrderGrid = ({ if (canCancel) { cols.push( {row.cancelled ? ( @@ -249,13 +249,14 @@ const SponsorOrderGrid = ({ )); + const discountCents = form.discount_in_cents ?? 0; rows.push( ); diff --git a/src/components/mui/__tests__/payment-row.test.js b/src/components/mui/__tests__/payment-row.test.js index 754d7f99..08adb812 100644 --- a/src/components/mui/__tests__/payment-row.test.js +++ b/src/components/mui/__tests__/payment-row.test.js @@ -45,7 +45,7 @@ describe("PaymentRow", () => { test("renders the formatted payment amount", () => { renderInTable({ payment: { method: "Visa", amount: 2000, created: PAYMENT_TIMESTAMP } }); - expect(screen.getByText("-$20.00")).toBeInTheDocument(); + expect(screen.getByText("$20.00")).toBeInTheDocument(); }); test("renders the payment method", () => { diff --git a/src/components/mui/__tests__/refund-row.test.js b/src/components/mui/__tests__/refund-row.test.js index 594e852e..11bc5496 100644 --- a/src/components/mui/__tests__/refund-row.test.js +++ b/src/components/mui/__tests__/refund-row.test.js @@ -42,7 +42,7 @@ describe("RefundRow", () => { test("renders the formatted refund amount", () => { renderInTable({ refund: { reason: "Duplicate", status: "completed", amount: 3000 } }); - expect(screen.getByText("-$30.00")).toBeInTheDocument(); + expect(screen.getByText("$30.00")).toBeInTheDocument(); }); test("renders the refund reason", () => { diff --git a/src/components/mui/__tests__/total-row.test.js b/src/components/mui/__tests__/total-row.test.js index 5fcc3100..a278922f 100644 --- a/src/components/mui/__tests__/total-row.test.js +++ b/src/components/mui/__tests__/total-row.test.js @@ -31,38 +31,38 @@ const renderInTable = (props) => render( - +
); describe("TotalRow", () => { test("renders 'TOTAL' label key in first column", () => { - renderInTable({ targetCol: "quantity", total: 42 }); + renderInTable({ total: 4200 }); expect(screen.getByText("mui_table.total")).toBeInTheDocument(); }); - test("renders total value in the targetCol", () => { - renderInTable({ targetCol: "quantity", total: 42 }); - expect(screen.getByText("42")).toBeInTheDocument(); + test("renders formatted total amount in cents", () => { + renderInTable({ total: 4200 }); + expect(screen.getByText("$42.00")).toBeInTheDocument(); }); - test("renders correct number of cells (one per column)", () => { - const { container } = renderInTable({ targetCol: "quantity", total: 10 }); + test("renders correct number of cells based on colGap", () => { + const { container } = renderInTable({ total: 1000, colGap: columns.length - 2 }); expect(container.querySelectorAll("td")).toHaveLength(columns.length); }); test("renders extra trailing cells when trailing prop is provided", () => { const { container } = renderInTable({ - targetCol: "quantity", - total: 10, + total: 1000, + colGap: columns.length - 2, trailing: 2 }); expect(container.querySelectorAll("td")).toHaveLength(columns.length + 2); }); - test("renders string totals", () => { - renderInTable({ targetCol: "price", total: "$1,234" }); - expect(screen.getByText("$1,234")).toBeInTheDocument(); + test("renders negative total with sign", () => { + renderInTable({ total: -5000 }); + expect(screen.getByText("-$50.00")).toBeInTheDocument(); }); }); diff --git a/src/components/mui/table/extra-rows/DiscountRow.jsx b/src/components/mui/table/extra-rows/DiscountRow.jsx index 0a18759b..27fe1a79 100644 --- a/src/components/mui/table/extra-rows/DiscountRow.jsx +++ b/src/components/mui/table/extra-rows/DiscountRow.jsx @@ -24,7 +24,7 @@ import BalanceValue from "../../SponsorOrderGrid/components/BalanceValue"; const DiscountRow = ({ discount, discountCents, balance, colGap = 0, trailing = 0 }) => { - if (discountCents === 0) return null; + if (!discountCents) return null; return ( diff --git a/src/components/mui/table/extra-rows/TotalRow.jsx b/src/components/mui/table/extra-rows/TotalRow.jsx index e38120ad..f2a2b5c1 100644 --- a/src/components/mui/table/extra-rows/TotalRow.jsx +++ b/src/components/mui/table/extra-rows/TotalRow.jsx @@ -20,10 +20,11 @@ import {currencyAmountFromCents} from "../../../../utils/money"; const TotalRow = ({total, colGap = 3, trailing = 0, label = null, rowSx = {}}) => { const totalLabel = label || T.translate("mui_table.total"); - const isNegative = total < 0; + const safetotal = total ?? 0; + const isNegative = safetotal < 0; const sign = isNegative ? "-" : ""; - const color = isNegative ? "primary.dark" : (total === 0 ? "text.primary" : "error.main"); - const totalStr = `${sign}${currencyAmountFromCents(Math.abs(total))}`; + const color = isNegative ? "primary.dark" : (safetotal === 0 ? "text.primary" : "error.main"); + const totalStr = `${sign}${currencyAmountFromCents(Math.abs(safetotal))}`; return ( From 11cfaf851599cf8f23bfab6594ea71e33d8c4d5b Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 4 Jun 2026 15:34:08 -0300 Subject: [PATCH 06/11] chore: change currencyAmountFromCents to return ERROR instead of exception --- src/utils/__tests__/money.test.js | 16 ++++++++-------- src/utils/money.js | 17 +++++++++++++---- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/utils/__tests__/money.test.js b/src/utils/__tests__/money.test.js index 2b1c8b4a..b186ba9d 100644 --- a/src/utils/__tests__/money.test.js +++ b/src/utils/__tests__/money.test.js @@ -120,15 +120,15 @@ describe("parsePrice()", () => { }); describe("currencyAmountFromCents (integration, no mocks)", () => { - it("throws if cents is not a number", () => { - expect(() => currencyAmountFromCents("10")).toThrow("cents must be an integer number"); - expect(() => currencyAmountFromCents(null)).toThrow("cents must be an integer number"); - expect(() => currencyAmountFromCents(undefined)).toThrow("cents must be an integer number"); + it("returns error string if cents is not a number", () => { + expect(currencyAmountFromCents("10")).toBe("!ERROR"); + expect(currencyAmountFromCents(null)).toBe("!ERROR"); + expect(currencyAmountFromCents(undefined)).toBe("!ERROR"); }); - it("throws if cents is not an integer", () => { - expect(() => currencyAmountFromCents(10.5)).toThrow("cents must be an integer number"); - expect(() => currencyAmountFromCents(NaN)).toThrow("cents must be an integer number"); + it("returns error string if cents is not an integer", () => { + expect(currencyAmountFromCents(10.5)).toBe("!ERROR"); + expect(currencyAmountFromCents(NaN)).toBe("!ERROR"); }); it("formats USD by default", () => { @@ -153,6 +153,6 @@ describe("currencyAmountFromCents (integration, no mocks)", () => { }); it("handles negative cents", () => { - expect( () => currencyAmountFromCents(-123, "USD")).toThrow("cents must be non-negative."); + expect(currencyAmountFromCents(-123, "USD")).toBe("!ERROR"); }); }); diff --git a/src/utils/money.js b/src/utils/money.js index e4295cdf..7ec987a8 100644 --- a/src/utils/money.js +++ b/src/utils/money.js @@ -159,13 +159,22 @@ export function amountFromCents(cents) { * @returns {string} */ export function currencyAmountFromCents(cents, currency = "USD") { + let result = "!ERROR"; + if (typeof cents !== "number" || !Number.isInteger(cents)) { - throw new Error("cents must be an integer number"); + console.error("ERROR - currencyAmountFromCents: cents must be an integer number"); + return result; + } + + try { + const amount = amountFromCents(cents); // "12.34" + const symbol = CURRENCY_SYMBOL[currency] ?? "$"; + result = `${symbol}${amount}`; + } catch (e) { + console.error(`ERROR - currencyAmountFromCents: ${e}`); } - const amount = amountFromCents(cents); // "12.34" - const symbol = CURRENCY_SYMBOL[currency] ?? "$"; - return `${symbol}${amount}`; + return result; } /** From 8477ed9efe2cd701c89e432c5ba4dad1cfb1af73 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 4 Jun 2026 15:35:21 -0300 Subject: [PATCH 07/11] v5.0.33-beta.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 97dfc6b6..ad48be5a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.33-beta.0", + "version": "5.0.33-beta.1", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From 3b321741a2691db8d30d28733e7fd9717b03d8fd Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 5 Jun 2026 14:51:44 -0300 Subject: [PATCH 08/11] fix: balance multiplier in refunds should be positive, sort refunds and payments by created --- src/components/mui/SponsorOrderGrid/index.js | 45 +++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js index 103abc87..f5c2d06a 100644 --- a/src/components/mui/SponsorOrderGrid/index.js +++ b/src/components/mui/SponsorOrderGrid/index.js @@ -136,6 +136,11 @@ const SponsorOrderGrid = ({ const colCount = columns.length + 1 + trailingCols; // 1 for balance, 1 for action col + const paymentsAndRefundsOrdered = [ + ...payments?.map((payment) => ({...payment, type: "payment"})), + ...refunds?.map((refund) => ({...refund, type: "refund"})) + ].sort((a, b) => a.created - b.created); + return ( @@ -262,6 +267,7 @@ const SponsorOrderGrid = ({ return rows; })} + {fees && fees.map((fee) => ( ))} - {refunds && refunds.map((refund) => ( - - ))} - {payments && payments.map((payment) => ( - - ))} + + {paymentsAndRefundsOrdered.map((item) => { + if (item.type === "payment") { + return ( + + ) + } else if (item.type === "refund") { + return ( + + ) + } + })} + {notes && notes.map((note) => ( Date: Fri, 5 Jun 2026 14:52:43 -0300 Subject: [PATCH 09/11] v5.0.33-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad48be5a..17c66f30 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.33-beta.1", + "version": "5.0.33-beta.2", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": { From 4aaf79e12936b56338d029d295110a10aa40404e Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 5 Jun 2026 15:05:57 -0300 Subject: [PATCH 10/11] fix: escape payments and refunds in case not set --- src/components/mui/SponsorOrderGrid/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js index f5c2d06a..ac344ea9 100644 --- a/src/components/mui/SponsorOrderGrid/index.js +++ b/src/components/mui/SponsorOrderGrid/index.js @@ -137,8 +137,8 @@ const SponsorOrderGrid = ({ const colCount = columns.length + 1 + trailingCols; // 1 for balance, 1 for action col const paymentsAndRefundsOrdered = [ - ...payments?.map((payment) => ({...payment, type: "payment"})), - ...refunds?.map((refund) => ({...refund, type: "refund"})) + ...payments?.map((payment) => ({...payment, type: "payment"})) || [], + ...refunds?.map((refund) => ({...refund, type: "refund"})) || [] ].sort((a, b) => a.created - b.created); return ( From bc10316e156472ceb0e222a92c916b1ec589f9a5 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Fri, 5 Jun 2026 15:11:34 -0300 Subject: [PATCH 11/11] v5.0.33-beta.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 17c66f30..7f154a02 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "openstack-uicore-foundation", - "version": "5.0.33-beta.2", + "version": "5.0.33-beta.3", "description": "ui reactjs components for openstack marketing site", "main": "lib/openstack-uicore-foundation.js", "scripts": {