diff --git a/package.json b/package.json
index 735d65a5..f3e0b1c3 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "openstack-uicore-foundation",
- "version": "5.0.51",
+ "version": "5.0.53-beta.2",
"description": "ui reactjs components for openstack marketing site",
"main": "lib/openstack-uicore-foundation.js",
"scripts": {
diff --git a/src/components/mui/MuiBaseCustomTheme.js b/src/components/mui/MuiBaseCustomTheme.js
index b8689919..7f2f1228 100644
--- a/src/components/mui/MuiBaseCustomTheme.js
+++ b/src/components/mui/MuiBaseCustomTheme.js
@@ -44,6 +44,11 @@ export const MuiBaseCustomTheme = {
height: "24px"
}
}
- }
+ },
+ MuiTooltip: {
+ styleOverrides: {
+ tooltip: { fontSize: "1em", fontWeight: "400" }
+ }
+ },
}
};
diff --git a/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js b/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js
index 2579de42..b66ffe7c 100644
--- a/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js
+++ b/src/components/mui/SponsorOrderGrid/__tests__/SponsorOrderGrid.test.js
@@ -30,15 +30,18 @@ jest.mock("../../../../utils/methods", () => ({
}));
import React from "react";
-import { render, screen, fireEvent } from "@testing-library/react";
+import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import "@testing-library/jest-dom";
+import T from "i18n-react/dist/i18n-react";
import SponsorOrderGrid from "../index";
const makeItem = (overrides = {}) => ({
line_id: 841,
quantity: 1,
+ canceled_quantity: 0,
amount: 10000,
- canceled_by_id: null,
+ cancellations: [],
type: { id: 146, name: "Booth", code: "BOOTH" },
meta_fields: [],
...overrides
@@ -118,53 +121,205 @@ describe("SponsorOrderGrid", () => {
expect(screen.getByText("sponsor_order_grid.action")).toBeInTheDocument();
});
- test("renders delete button for active item and calls onCancelForm on click", () => {
+ test("clicking the action icon opens the change-quantity modal instead of calling callbacks directly", () => {
const onCancelForm = jest.fn();
+ const onUndoCancelForm = jest.fn();
render(
+ );
+ const button = document.querySelector("tbody button");
+ fireEvent.click(button);
+ expect(
+ screen.getByText("sponsor_order_grid.change_quantity_modal.title")
+ ).toBeInTheDocument();
+ expect(onCancelForm).not.toHaveBeenCalled();
+ expect(onUndoCancelForm).not.toHaveBeenCalled();
+ });
+
+ test("reset is disabled in the modal when nothing has been cancelled yet", () => {
+ render(
+
);
const button = document.querySelector("tbody button");
fireEvent.click(button);
- expect(onCancelForm).toHaveBeenCalledTimes(1);
+ expect(
+ screen.getByRole("button", {
+ name: "sponsor_order_grid.change_quantity_modal.reset"
+ })
+ ).toBeDisabled();
});
- test("renders undo button for cancelled item and calls onUndoCancelForm on click", () => {
- const onUndoCancelForm = jest.fn();
- const order = { forms: [makeForm({ items: [makeItem({ canceled_by_id: 99 })] })], total: 0 };
+ test("lowering the quantity and applying calls onCancelForm with the delta, item and reason, then closes the dialog", async () => {
+ const onCancelForm = jest.fn(() => Promise.resolve());
+ const order = {
+ forms: [makeForm({ items: [makeItem({ line_id: 841, quantity: 5, canceled_quantity: 0 })] })],
+ total: 0
+ };
render(
Promise.resolve())}
/>
);
const button = document.querySelector("tbody button");
fireEvent.click(button);
- expect(onUndoCancelForm).toHaveBeenCalledTimes(1);
+
+ const quantityField = screen.getByLabelText(
+ "sponsor_order_grid.change_quantity_modal.quantity"
+ );
+ fireEvent.change(quantityField, { target: { value: "2" } });
+ const reasonField = screen.getByLabelText(
+ "sponsor_order_grid.change_quantity_modal.reason"
+ );
+ fireEvent.change(reasonField, { target: { value: "Damaged" } });
+
+ fireEvent.click(screen.getByRole("button", { name: "general.apply" }));
+
+ await waitFor(() =>
+ expect(onCancelForm).toHaveBeenCalledWith(
+ expect.objectContaining({ id: 841 }),
+ 3,
+ "Damaged"
+ )
+ );
+ await waitFor(() =>
+ expect(
+ screen.queryByText("sponsor_order_grid.change_quantity_modal.title")
+ ).not.toBeInTheDocument()
+ );
});
- test("passes the order line id to onCancelForm", () => {
- const onCancelForm = jest.fn();
+ test("dialog stays open and the typed reason is preserved when onCancelForm rejects", async () => {
+ const onCancelForm = jest.fn(() => Promise.reject(new Error("Cannot cancel 3 units; only 2 remain")));
+ const order = {
+ forms: [makeForm({ items: [makeItem({ line_id: 841, quantity: 5, canceled_quantity: 0 })] })],
+ total: 0
+ };
render(
Promise.resolve())}
+ />
+ );
+ const button = document.querySelector("tbody button");
+ fireEvent.click(button);
+
+ const quantityField = screen.getByLabelText(
+ "sponsor_order_grid.change_quantity_modal.quantity"
+ );
+ fireEvent.change(quantityField, { target: { value: "2" } });
+ const reasonField = screen.getByLabelText(
+ "sponsor_order_grid.change_quantity_modal.reason"
+ );
+ fireEvent.change(reasonField, { target: { value: "Damaged" } });
+
+ fireEvent.click(screen.getByRole("button", { name: "general.apply" }));
+
+ await waitFor(() => expect(onCancelForm).toHaveBeenCalled());
+ expect(
+ screen.getByText("sponsor_order_grid.change_quantity_modal.title")
+ ).toBeInTheDocument();
+ expect(screen.getByLabelText("sponsor_order_grid.change_quantity_modal.reason")).toHaveValue(
+ "Damaged"
+ );
+ });
+
+ test("clicking reset then applying calls onUndoCancelForm with the item, then closes the dialog", async () => {
+ const onUndoCancelForm = jest.fn(() => Promise.resolve());
+ const order = {
+ forms: [
+ makeForm({
+ items: [makeItem({ line_id: 841, quantity: 5, canceled_quantity: 2 })]
+ })
+ ],
+ total: 0
+ };
+ render(
+ Promise.resolve())}
+ onUndoCancelForm={onUndoCancelForm}
+ />
+ );
+ const button = document.querySelector("tbody button");
+ fireEvent.click(button);
+
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "sponsor_order_grid.change_quantity_modal.reset"
+ })
+ );
+ fireEvent.click(screen.getByRole("button", { name: "general.apply" }));
+
+ await waitFor(() =>
+ expect(onUndoCancelForm).toHaveBeenCalledWith(
+ expect.objectContaining({ id: 841 })
+ )
+ );
+ await waitFor(() =>
+ expect(
+ screen.queryByText("sponsor_order_grid.change_quantity_modal.title")
+ ).not.toBeInTheDocument()
+ );
+ });
+
+ test("clicking reset disables the reason field, typing a lower quantity re-enables it", () => {
+ const order = {
+ forms: [
+ makeForm({
+ items: [makeItem({ line_id: 841, quantity: 5, canceled_quantity: 2 })]
+ })
+ ],
+ total: 0
+ };
+ render(
+
);
const button = document.querySelector("tbody button");
fireEvent.click(button);
- expect(onCancelForm).toHaveBeenCalledWith(expect.objectContaining({ id: 841 }));
+
+ const reasonField = screen.getByLabelText(
+ "sponsor_order_grid.change_quantity_modal.reason"
+ );
+ expect(reasonField).toBeEnabled();
+
+ fireEvent.click(
+ screen.getByRole("button", {
+ name: "sponsor_order_grid.change_quantity_modal.reset"
+ })
+ );
+ expect(reasonField).toBeDisabled();
+
+ const quantityField = screen.getByLabelText(
+ "sponsor_order_grid.change_quantity_modal.quantity"
+ );
+ fireEvent.change(quantityField, { target: { value: "1" } });
+ expect(reasonField).toBeEnabled();
});
- test("passes the order line id to onUndoCancelForm", () => {
+ test("a fully cancelled line keeps the quantity field disabled, so it cannot be typed back to a non-zero value that would trigger a silent restore", async () => {
+ const user = userEvent.setup();
const onUndoCancelForm = jest.fn();
const order = {
- forms: [makeForm({ items: [makeItem({ line_id: 841, canceled_by_id: 99 })] })],
+ forms: [
+ makeForm({
+ items: [makeItem({ line_id: 841, quantity: 5, canceled_quantity: 5 })]
+ })
+ ],
total: 0
};
render(
@@ -176,7 +331,95 @@ describe("SponsorOrderGrid", () => {
);
const button = document.querySelector("tbody button");
fireEvent.click(button);
- expect(onUndoCancelForm).toHaveBeenCalledWith(expect.objectContaining({ id: 841 }));
+
+ const quantityField = screen.getByLabelText(
+ "sponsor_order_grid.change_quantity_modal.quantity"
+ );
+ await user.type(quantityField, "4");
+
+ const applyButton = screen.getByRole("button", { name: "general.apply" });
+ await act(async () => {
+ fireEvent.click(applyButton);
+ // flush the formik async submit pipeline so a call would have landed by now
+ await Promise.resolve();
+ await Promise.resolve();
+ });
+
+ expect(onUndoCancelForm).not.toHaveBeenCalled();
+ });
+
+ test("does not strikethrough or show Cancelled type when only partially cancelled", () => {
+ const order = {
+ forms: [makeForm({ items: [makeItem({ quantity: 5, canceled_quantity: 2 })] })],
+ total: 0
+ };
+ render();
+ expect(screen.queryByText("Cancelled")).not.toBeInTheDocument();
+ expect(screen.getByText(/Booth/).closest("p")).not.toHaveStyle({
+ textDecoration: "line-through"
+ });
+ });
+
+ test("strikes through and shows Cancelled type only when fully cancelled", () => {
+ const order = {
+ forms: [makeForm({ items: [makeItem({ quantity: 5, canceled_quantity: 5 })] })],
+ total: 0
+ };
+ render();
+ expect(screen.getByText("Cancelled")).toBeInTheDocument();
+ expect(screen.getByText(/Booth/).closest("p")).toHaveStyle({
+ textDecoration: "line-through"
+ });
+ });
+
+ test("renders a list of all cancellations with date, author and reason", () => {
+ const order = {
+ forms: [
+ makeForm({
+ items: [
+ makeItem({
+ quantity: 5,
+ canceled_quantity: 3,
+ cancellations: [
+ {
+ id: 1,
+ quantity: 2,
+ amount: 200,
+ reason: "Too many",
+ canceled_by_id: 5,
+ canceled_by_email: "a@test.com",
+ canceled_by_full_name: "Alice Admin",
+ created: 1000
+ },
+ {
+ id: 2,
+ quantity: 1,
+ amount: 100,
+ reason: "",
+ canceled_by_id: 6,
+ canceled_by_email: "b@test.com",
+ canceled_by_full_name: "Bob Admin",
+ created: 2000
+ }
+ ]
+ })
+ ]
+ })
+ ],
+ total: 0
+ };
+ const translateSpy = jest.spyOn(T, "translate");
+ render();
+
+ expect(translateSpy).toHaveBeenCalledWith(
+ "sponsor_order_grid.cancelled_by",
+ expect.objectContaining({ x: 2, y: 5, user: "Alice Admin", date: "2026-01-01" })
+ );
+ expect(translateSpy).toHaveBeenCalledWith(
+ "sponsor_order_grid.cancelled_by",
+ expect.objectContaining({ x: 1, y: 5, user: "Bob Admin", date: "2026-01-01" })
+ );
+ expect(screen.getByText(/Too many/)).toBeInTheDocument();
});
test("gives rows from different forms with the same item type distinct ids", () => {
diff --git a/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx b/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx
index 583d7593..2ffd5724 100644
--- a/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx
+++ b/src/components/mui/SponsorOrderGrid/components/CancelledItems.jsx
@@ -18,26 +18,30 @@ import DoNotDisturbIcon from "@mui/icons-material/DoNotDisturb";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
-const CancelledItems = ({cancelledItems, sx = {}}) => {
-
+const CancelledItems = ({ cancelledItems, sx = {} }) => {
if (cancelledItems.length === 0) return null;
return (
-
-
-
- {T.translate("sponsor_order_grid.cancelled_items", {count: cancelledItems.length})}
+
+
+
+ {T.translate("sponsor_order_grid.cancelled_items")}
- {cancelledItems.map((item) => (
-
- {item.formCode} - {item.itemCode}
-
- ))}
+ {cancelledItems.map((item) => {
+ const fullyCanceled = item.canceled_quantity === item.quantity;
+ const fontWeight = fullyCanceled ? "bold" : "normal";
+
+ return (
+
+ {item.formCode} - {item.itemCode} ({item.canceled_quantity}/{item.quantity})
+
+ )
+ })}
);
}
diff --git a/src/components/mui/SponsorOrderGrid/components/ChangeQuantityModal.jsx b/src/components/mui/SponsorOrderGrid/components/ChangeQuantityModal.jsx
new file mode 100644
index 00000000..310b0d4a
--- /dev/null
+++ b/src/components/mui/SponsorOrderGrid/components/ChangeQuantityModal.jsx
@@ -0,0 +1,123 @@
+/**
+ * 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 { FormikProvider, useFormik } from "formik";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import CustomDialog from "../../CustomDialog";
+import MuiFormikQuantityField from "../../formik-inputs/mui-formik-quantity-field";
+import MuiFormikTextField from "../../formik-inputs/mui-formik-textfield";
+
+const ChangeQuantityModal = ({
+ open,
+ onClose,
+ item,
+ onCancelForm,
+ onUndoCancelForm
+}) => {
+ const originalQuantity = item.quantity;
+ const currentQuantity = originalQuantity - (item.canceled_quantity ?? 0);
+
+ const formik = useFormik({
+ initialValues: {
+ quantity: currentQuantity,
+ reason: ""
+ },
+ onSubmit: ({ quantity, reason }) => {
+ if (quantity < currentQuantity) {
+ return onCancelForm(item, currentQuantity - quantity, reason).then(onClose);
+ }
+ // can only call undo if quantity restored to original
+ if (quantity === originalQuantity) {
+ return onUndoCancelForm(item).then(onClose);
+ }
+ onClose();
+ },
+ enableReinitialize: true
+ });
+
+ const handleClose = () => {
+ formik.resetForm();
+ onClose();
+ };
+
+ const handleReset = () => {
+ formik.setFieldValue("quantity", originalQuantity);
+ formik.setFieldValue("reason", "");
+ };
+
+ const canReset = originalQuantity !== currentQuantity;
+ const hasChanged = formik.values.quantity !== currentQuantity;
+ const isRestoring = formik.values.quantity === originalQuantity;
+ // between currentQuantity and originalQuantity there is no valid action
+ const isInGap = !isRestoring && formik.values.quantity > currentQuantity;
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ChangeQuantityModal;
diff --git a/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx b/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx
index b96f0b26..175b356f 100644
--- a/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx
+++ b/src/components/mui/SponsorOrderGrid/components/__tests__/CancelledItems.test.jsx
@@ -29,12 +29,12 @@ describe("CancelledItems", () => {
test("renders a link for each cancelled item", () => {
const items = [
- { id: 1, formCode: "GOLD", itemCode: "BOOTH" },
- { id: 2, formCode: "SILVER", itemCode: "TABLE" }
+ { id: 1, formCode: "GOLD", itemCode: "BOOTH", canceled_quantity: 1, quantity: 2 },
+ { id: 2, formCode: "SILVER", itemCode: "TABLE", canceled_quantity: 3, quantity: 3 }
];
render();
- expect(screen.getByText("GOLD - BOOTH")).toBeInTheDocument();
- expect(screen.getByText("SILVER - TABLE")).toBeInTheDocument();
+ expect(screen.getByText("GOLD - BOOTH (1/2)")).toBeInTheDocument();
+ expect(screen.getByText("SILVER - TABLE (3/3)")).toBeInTheDocument();
});
test("each link href anchors to the item id", () => {
diff --git a/src/components/mui/SponsorOrderGrid/index.js b/src/components/mui/SponsorOrderGrid/index.js
index 5069ba3f..e509fd8d 100644
--- a/src/components/mui/SponsorOrderGrid/index.js
+++ b/src/components/mui/SponsorOrderGrid/index.js
@@ -23,18 +23,19 @@ import TableHead from "@mui/material/TableHead";
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 Tooltip from "@mui/material/Tooltip";
+import RuleIcon from "@mui/icons-material/Rule";
+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 { currencyAmountFromCents } from "../../../utils/money";
import TransactionType from "./components/TransactionType";
-import {formatEpoch} from "../../../utils/methods";
+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 ChangeQuantityModal from "./components/ChangeQuantityModal";
const mapOrderData = (forms) => {
if (!forms) return [];
@@ -46,7 +47,8 @@ const mapOrderData = (forms) => {
.map((it, i) => {
const amount = currencyAmountFromCents(it.amount || 0);
const itemId = it.line_id ?? `${form.id}-${i}`;
- const cancelled = !!it.canceled_by_id;
+ const canceledQuantity = it.canceled_quantity ?? 0;
+ const cancelled = canceledQuantity > 0 && canceledQuantity === it.quantity;
const type = cancelled ? SPONSOR_ORDER_GRID_ITEM_TYPES.CANCELLED : SPONSOR_ORDER_GRID_ITEM_TYPES.CHARGE;
return {
@@ -55,27 +57,25 @@ const mapOrderData = (forms) => {
itemName: it.type?.name,
itemCode: it.type?.code,
quantity: it.quantity,
+ canceled_quantity: canceledQuantity,
type,
amount,
amountValue: it.amount,
cancelled,
- cancelledBy: T.translate("sponsor_order_grid.cancelled_by", {
- user: it.canceled_by_full_name,
- date: formatEpoch(it.canceled_at)
- }),
+ cancellations: it.cancellations ?? []
};
})
}));
};
const SponsorOrderGrid = ({
- title = T.translate("sponsor_order_grid.title"),
- order,
- withReconciliation = false,
- withCancelledItemsHeader = false,
- onCancelForm,
- onUndoCancelForm
- }) => {
+ title = T.translate("sponsor_order_grid.title"),
+ order,
+ withReconciliation = false,
+ withCancelledItemsHeader = false,
+ onCancelForm,
+ onUndoCancelForm
+}) => {
const {
forms = [],
@@ -90,9 +90,10 @@ const SponsorOrderGrid = ({
refunds_total: refundsTotal = 0
} = order || {};
const data = mapOrderData(forms);
- const cancelledItems = data.flatMap((form) => form.items.filter((it) => it.cancelled));
+ const cancelledItems = data.flatMap((form) => form.items.filter((it) => it.canceled_quantity > 0));
const canCancel = onCancelForm && onUndoCancelForm;
const trailingCols = canCancel ? 1 : 0;
+ const [changeQuantityRow, setChangeQuantityRow] = React.useState(null);
let balance = 0;
const calculateBalance = (rowAmount, op = 1) => {
@@ -108,21 +109,33 @@ const SponsorOrderGrid = ({
{
columnKey: "type",
header: T.translate("sponsor_order_grid.type"),
- render: (row) => ()
+ render: (row) => ()
},
{
columnKey: "details",
header: T.translate("sponsor_order_grid.details"),
render: (row) => (
<>
-
- {row.itemName} - {T.translate("sponsor_order_grid.total")}: {row.quantity}
+
+ {row.itemName} - {T.translate("sponsor_order_grid.total")}: {row.quantity - row.canceled_quantity}
- {row.cancelled &&
-
- {row.cancelledBy}
-
- }
+ {row.cancellations.map((cancellation) => (
+
+
+ {T.translate("sponsor_order_grid.cancelled_by", {
+ x: cancellation.quantity,
+ y: row.quantity,
+ user: cancellation.canceled_by_full_name,
+ date: formatEpoch(cancellation.created, "M/D/YY [@] h:mm A")
+ })}
+
+ {cancellation.reason &&
+
+ ↳ {cancellation.reason}
+
+ }
+
+ ))}
>
)
},
@@ -130,33 +143,32 @@ const SponsorOrderGrid = ({
columnKey: "amount",
header: T.translate("sponsor_order_grid.amount"),
align: "right",
- strikethrough: true,
}
];
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 (
-
-
+
+
{title && (
-
+
{title}
)}
{withCancelledItemsHeader && (
-
+
)}
{canCancel && (
{/* TABLE HEADER */}
-
+
{columns.map((col) => (
@@ -185,27 +197,20 @@ const SponsorOrderGrid = ({
)}
-
+
{data.map((form) => {
const rows = form.items.map((row) => (
{(() => {
const cols = columns.map((col) => (
{col.render ? (
col.render(row)
@@ -221,10 +226,10 @@ const SponsorOrderGrid = ({
key={`grid-col-${row.id}-balance`}
align="right"
sx={{
- ...(row.cancelled && {color: "text.disabled"})
+ ...(row.cancelled && { color: "text.disabled" })
}}
>
-
+
)
@@ -235,15 +240,11 @@ const SponsorOrderGrid = ({
key="action"
align="right"
>
- {row.cancelled ? (
- onUndoCancelForm(row)}>
-
+
+ setChangeQuantityRow(row)}>
+
- ) : (
- onCancelForm(row)}>
-
-
- )}
+
)
}
@@ -314,7 +315,7 @@ const SponsorOrderGrid = ({
total={total}
label={T.translate("sponsor_order_grid.amount_due")}
trailing={trailingCols}
- rowSx={{bgcolor: "#F1F3F5", "& td": {borderBottom: "none"}}}
+ rowSx={{ bgcolor: "#F1F3F5", "& td": { borderBottom: "none" } }}
/>
}
{data.length === 0 && (
@@ -329,17 +330,26 @@ const SponsorOrderGrid = ({
{withReconciliation &&
-
-
+
+
-
+
}
+ {changeQuantityRow && (
+ setChangeQuantityRow(null)}
+ item={changeQuantityRow}
+ onCancelForm={onCancelForm}
+ onUndoCancelForm={onUndoCancelForm}
+ />
+ )}
);
};
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 1fca0c25..a8da85c6 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -31,6 +31,7 @@
"edit": "Edit",
"delete": "Delete",
"cancel": "Cancel",
+ "apply": "Apply",
"n_a": "N/A",
"not_available": "N/A",
"notes": "Notes",
@@ -148,9 +149,16 @@
"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.",
- "cancelled_by": "Cancelled {date} by {user}",
- "cancelled_items": "Cancelled items ({count}):",
+ "change_quantity_tooltip": "restore or remove items",
+ "change_quantity_modal": {
+ "title": "Change Quantity",
+ "quantity": "New Quantity",
+ "reset": "Reset to Original",
+ "reason": "Reason"
+ },
+ "cancel_info_note": "Active order items can be partially or fully canceled, or reverted to original quantity. Refund and payment rows are display-only.",
+ "cancelled_by": "Cancelled ({x} of {y}) • {date} • {user}",
+ "cancelled_items": "Cancelled items:",
"reconciliation": "Reconciliation",
"cancelled": "Cancelled",
"refunded": "Refunded",