diff --git a/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js b/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js
index 67dbfb264..6816e36c1 100644
--- a/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js
+++ b/src/components/mui/FormItemTable/__tests__/FormItemTable.test.js
@@ -19,18 +19,7 @@ import FormItemTable from "../index";
import ItemTableField from "../components/ItemTableField";
/* eslint-enable import/first */
-const EARLY_BIRD_DATE = 1751035704;
-const STANDARD_DATE = 1851035704;
-const ONSITE_DATE = 1951035704;
-const MOCK_TIME_BEFORE_EARLY_BIRD = 1650000000000;
-const MILLISECONDS_MULTIPLIER = 1000;
-const TIME_OFFSET = 100;
const TWO_ITEMS = 4;
-const MOCK_RATE_DATES = {
- early_bird_end_date: EARLY_BIRD_DATE,
- standard_price_end_date: STANDARD_DATE,
- onsite_end_date: ONSITE_DATE
-};
// Mock form data
const MOCK_FORM_A = {
@@ -135,6 +124,34 @@ const MOCK_FORM_A = {
]
};
+const MOCK_ITEMS_WITH_NULL_RATES = [
+ {
+ form_item_id: 5,
+ code: "ITEM-NA",
+ name: "Item With N/A Rates",
+ rates: {
+ early_bird: null,
+ standard: null,
+ onsite: null
+ },
+ meta_fields: []
+ }
+];
+
+const MOCK_ITEMS_WITH_MIXED_RATES = [
+ {
+ form_item_id: 6,
+ code: "ITEM-PARTIAL",
+ name: "Item Partial Rates",
+ rates: {
+ early_bird: 10000,
+ standard: null,
+ onsite: null
+ },
+ meta_fields: []
+ }
+];
+
jest.mock("../../formik-inputs/mui-formik-textfield", () => {
const { useField } = require("formik");
return {
@@ -188,7 +205,7 @@ jest.mock("../../formik-inputs/mui-formik-datepicker", () => ({
jest.mock("../../formik-inputs/mui-formik-select", () => ({
__esModule: true,
- default: ({ name, label, options }) => (
+ default: ({ name, label, options, children }) => (
)
}));
@@ -247,12 +265,56 @@ jest.mock("../../formik-inputs/mui-formik-dropdown-radio", () => ({
)
}));
+jest.mock("../../formik-inputs/mui-formik-pricefield", () => {
+ const { useField } = require("formik");
+ return {
+ __esModule: true,
+ default: ({ name, label, ...props }) => {
+ const [field] = useField(name);
+ return (
+
+ );
+ }
+ };
+});
+
+jest.mock("../../formik-inputs/mui-formik-discountfield", () => {
+ const { useField } = require("formik");
+ return {
+ __esModule: true,
+ default: ({ name, label, discountType, ...props }) => {
+ const [field] = useField(name);
+ return (
+
+ );
+ }
+ };
+});
+
// ---- Helpers ----
// Wrapper component with Formik
const FormItemTableWrapper = ({
data,
- rateDates,
+ currentApplicableRate,
timeZone,
initialValues,
onNotesClick,
@@ -273,7 +335,7 @@ const FormItemTableWrapper = ({
{
beforeEach(() => {
jest.clearAllMocks();
- jest
- .spyOn(Date, "now")
- .mockImplementation(() => MOCK_TIME_BEFORE_EARLY_BIRD);
});
afterEach(() => {
@@ -318,7 +378,7 @@ describe("FormItemTable Component", () => {
render(
{
render(
{
render(
{
render(
{
render(
{
});
});
+ describe("N/A Rates", () => {
+ it("displays N/A for items with null rates", () => {
+ render(
+
+ );
+
+ expect(screen.getAllByText("general.n_a").length).toBeGreaterThan(0);
+ });
+
+ it("displays N/A for null rates and dollar amounts for non-null rates on the same item", () => {
+ render(
+
+ );
+
+ expect(screen.getByText("$100.00")).toBeInTheDocument();
+ expect(screen.getAllByText("general.n_a").length).toBe(2);
+ });
+
+ it("shows $0.00 total for items with null applicable rate", () => {
+ render(
+
+ );
+
+ // Row total and grand total should both be $0.00 when rate is null
+ expect(screen.getAllByText("$0.00").length).toBeGreaterThan(0);
+ });
+
+ it("renders item with N/A rates without crashing", () => {
+ expect(() => {
+ render(
+
+ );
+ }).not.toThrow();
+ });
+ });
+
describe("ITEM Class Fields", () => {
it("shows warning icon for items with ITEM class fields", () => {
render(
{
const { container } = render(
{
const { container } = render(
{
render(
{
render(
{
render(
{
render(
{
render(
{
describe("Total Calculation", () => {
it("calculates item total correctly based on quantity and rate", () => {
+ // qty item1 = 2*4 = 8, standard rate = 18800 → 8 * 18800 = 150400 → $1504.00
+ // qty item2 = 1*2 = 2, standard rate = 18800 → 2 * 18800 = 37600 → $376.00
const initialValues = {
"i-1-c-Form-f-1": 2,
"i-1-c-Form-f-2": 4,
@@ -616,7 +739,7 @@ describe("FormItemTable Component", () => {
render(
{
});
it("calculates total amount for all items", () => {
+ // $1504.00 + $376.00 = $1880.00
const initialValues = {
"i-1-c-Form-f-1": 2,
"i-1-c-Form-f-2": 4,
@@ -642,7 +766,7 @@ describe("FormItemTable Component", () => {
render(
{
render(
{
});
describe("Rate Highlighting", () => {
- it("highlights early_bird rate when current time is before early_bird_rate", () => {
- jest
- .spyOn(Date, "now")
- .mockImplementation(() => MOCK_TIME_BEFORE_EARLY_BIRD);
-
+ it("highlights early_bird rate when currentApplicableRate is early_bird", () => {
render(
{
expect(screen.getAllByText("$150.00").length).toBeGreaterThan(0);
});
- it("highlights standard rate when current time is between early_bird and standard", () => {
- jest
- .spyOn(Date, "now")
- .mockImplementation(
- () =>
- (MOCK_RATE_DATES.early_bird_rate + TIME_OFFSET) *
- MILLISECONDS_MULTIPLIER
- );
-
+ it("highlights standard rate when currentApplicableRate is standard", () => {
render(
{
expect(screen.getAllByText("$188.00").length).toBeGreaterThan(0);
});
- it("highlights onsite rate when current time is after standard_rate", () => {
- jest
- .spyOn(Date, "now")
- .mockImplementation(
- () =>
- (MOCK_RATE_DATES.standard_rate + TIME_OFFSET) *
- MILLISECONDS_MULTIPLIER
- );
-
+ it("highlights onsite rate when currentApplicableRate is onsite", () => {
render(
{
const { container } = render(
{
const { container } = render(
{
const { container } = render(
{
+const GlobalQuantityField = ({
+ row,
+ extraColumns,
+ value,
+ disabled = false
+}) => {
const name = `i-${row.form_item_id}-c-global-f-quantity`;
// eslint-disable-next-line
const [field, meta, helpers] = useField(name);
@@ -35,6 +40,7 @@ const GlobalQuantityField = ({ row, extraColumns, value }) => {
fullWidth
size="small"
type="number"
+ disabled={disabled}
slotProps={{
htmlInput: {
readOnly: isReadOnly,
diff --git a/src/components/mui/FormItemTable/components/ItemTableField.js b/src/components/mui/FormItemTable/components/ItemTableField.js
index 2c668044c..2f5cdea12 100644
--- a/src/components/mui/FormItemTable/components/ItemTableField.js
+++ b/src/components/mui/FormItemTable/components/ItemTableField.js
@@ -21,17 +21,23 @@ import MuiFormikTimepicker from "../../formik-inputs/mui-formik-timepicker";
import MuiFormikTextField from "../../formik-inputs/mui-formik-textfield";
import MuiFormikSelect from "../../formik-inputs/mui-formik-select";
-const ItemTableField = ({ rowId, field, timeZone, label = "" }) => {
+const ItemTableField = ({
+ rowId,
+ field,
+ timeZone,
+ label = "",
+ disabled = false
+}) => {
const name = `i-${rowId}-c-${field.class_field}-f-${field.type_id}`;
+ const commonProps = { name, label, disabled };
switch (field.type) {
case "CheckBox":
- return ;
+ return ;
case "CheckBoxList":
return (
({ value: v.id, label: v.value }))}
/>
@@ -39,24 +45,20 @@ const ItemTableField = ({ rowId, field, timeZone, label = "" }) => {
case "RadioButtonList":
return (
({ value: v.id, label: v.value }))}
/>
);
case "DateTime":
- return ;
+ return ;
case "Time":
- return (
-
- );
+ return ;
case "Quantity":
return (
{
);
case "ComboBox":
return (
-
+
{field.values.map((v) => (
);
case "Text":
- return (
-
- );
+ return ;
case "TextArea":
return (
{
+ if (!showAdditionalItems) return null;
+
+ return (
+
+ {" "}
+ {T.translate("edit_sponsor.cart_tab.edit_form.additional_info")}
+
+ );
+};
+
+export default UnderlyingAlertNote;
diff --git a/src/components/mui/FormItemTable/helpers.js b/src/components/mui/FormItemTable/helpers.js
new file mode 100644
index 000000000..1e3b83cc2
--- /dev/null
+++ b/src/components/mui/FormItemTable/helpers.js
@@ -0,0 +1,30 @@
+import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods";
+import { MILLISECONDS_IN_SECOND } from "../../../utils/constants";
+
+export const getCurrentApplicableRate = (timeZone, rateDates) => {
+ const now = epochToMomentTimeZone(
+ Math.floor(new Date() / MILLISECONDS_IN_SECOND),
+ timeZone
+ );
+
+ const earlyBirdEnd = epochToMomentTimeZone(
+ rateDates.early_bird_end_date,
+ timeZone
+ )?.endOf("day");
+ const onsiteStart = epochToMomentTimeZone(
+ rateDates.onsite_price_start_date,
+ timeZone
+ )?.startOf("day");
+ const onsiteEnd = epochToMomentTimeZone(
+ rateDates.onsite_price_end_date,
+ timeZone
+ )?.endOf("day");
+
+ if (earlyBirdEnd && now.isSameOrBefore(earlyBirdEnd)) return "early_bird";
+ if (onsiteStart && now.isSameOrBefore(onsiteStart)) return "standard";
+ if (!onsiteEnd || now.isSameOrBefore(onsiteEnd)) return "onsite";
+ return "expired";
+};
+
+export const isItemAvailable = (item, currentApplicableRate) =>
+ item.rates?.[currentApplicableRate] != null;
diff --git a/src/components/mui/FormItemTable/index.js b/src/components/mui/FormItemTable/index.js
index db3ae210f..af126d93a 100644
--- a/src/components/mui/FormItemTable/index.js
+++ b/src/components/mui/FormItemTable/index.js
@@ -21,29 +21,23 @@ import {
TableCell,
TableContainer,
TableHead,
- TableRow,
- Typography
+ TableRow
} from "@mui/material";
import EditIcon from "@mui/icons-material/Edit";
import SettingsIcon from "@mui/icons-material/Settings";
-import ErrorIcon from "@mui/icons-material/Error";
import T from "i18n-react/dist/i18n-react";
import { currencyAmountFromCents } from "openstack-uicore-foundation/lib/utils/money";
-import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods";
-import {
- DISCOUNT_TYPES,
- MILLISECONDS_IN_SECOND,
- ONE_HUNDRED
-} from "../../../utils/constants";
+import { DISCOUNT_TYPES, ONE_HUNDRED } from "../../../utils/constants";
import GlobalQuantityField from "./components/GlobalQuantityField";
import ItemTableField from "./components/ItemTableField";
import MuiFormikSelect from "../formik-inputs/mui-formik-select";
import MuiFormikPriceField from "../formik-inputs/mui-formik-pricefield";
import MuiFormikDiscountField from "../formik-inputs/mui-formik-discountfield";
+import UnderlyingAlertNote from "./components/UnderlyingAlertNote";
const FormItemTable = ({
data,
- rateDates,
+ currentApplicableRate,
timeZone,
values,
onNotesClick,
@@ -55,33 +49,6 @@ const FormItemTable = ({
const fixedColumns = 10;
const totalColumns = extraColumns.length + fixedColumns;
- const currentApplicableRate = useMemo(() => {
- const now = epochToMomentTimeZone(
- Math.floor(new Date() / MILLISECONDS_IN_SECOND),
- timeZone
- );
-
- const earlyBirdEndOfDay = epochToMomentTimeZone(
- rateDates.early_bird_end_date,
- timeZone
- )?.endOf("day");
- const standardEndOfDay = epochToMomentTimeZone(
- rateDates.standard_price_end_date,
- timeZone
- )?.endOf("day");
- const onsiteEndOfDay = epochToMomentTimeZone(
- rateDates.onsite_price_end_date,
- timeZone
- )?.endOf("day");
-
- if (earlyBirdEndOfDay && now.isSameOrBefore(earlyBirdEndOfDay))
- return "early_bird";
- if (standardEndOfDay && now.isSameOrBefore(standardEndOfDay))
- return "standard";
- if (!onsiteEndOfDay || now.isSameOrBefore(onsiteEndOfDay)) return "onsite";
- return "expired";
- }, [rateDates, timeZone]);
-
const calculateQuantity = useCallback(
(row) => {
const qtyEXC = extraColumns.filter((exc) => exc.type === "Quantity");
@@ -105,10 +72,14 @@ const FormItemTable = ({
const qty =
values[`i-${row.form_item_id}-c-global-f-quantity`] ||
calculateQuantity(row);
+
if (currentApplicableRate === "expired") return 0;
+
const customRate = values[`i-${row.form_item_id}-c-global-f-custom_rate`];
const rate = customRate || row.rates[currentApplicableRate];
+ if (rate == null || qty == null) return 0;
+
return qty * rate;
};
@@ -127,6 +98,11 @@ const FormItemTable = ({
return requiredFields.length > 0 && hasMissingFields;
};
+ const formatRate = (rate) => {
+ if (rate == null) return T.translate("general.n_a");
+ return currencyAmountFromCents(rate);
+ };
+
const totalAmount = useMemo(() => {
const subtotal = data.reduce((acc, row) => acc + calculateRowTotal(row), 0);
const discount =
@@ -135,7 +111,7 @@ const FormItemTable = ({
: subtotal * (values.discount_amount / ONE_HUNDRED / ONE_HUNDRED); // bps to fraction
return subtotal - Math.round(discount);
- }, [data, valuesStr]);
+ }, [data, valuesStr, currentApplicableRate]);
const handleEdit = (row) => {
onNotesClick(row);
@@ -190,29 +166,11 @@ const FormItemTable = ({
{row.code}
{row.name}
- {hasItemFields(row) && itemFieldsIncomplete(row) && (
-
- {" "}
- {T.translate(
- "edit_sponsor.cart_tab.edit_form.additional_info"
- )}
-
- )}
+
- {currencyAmountFromCents(row.rates.early_bird)}
+ {formatRate(row.rates.early_bird)}
- {currencyAmountFromCents(row.rates.standard)}
+ {formatRate(row.rates.standard)}
- {currencyAmountFromCents(row.rates.onsite)}
+ {formatRate(row.rates.onsite)}
{extraColumns.map((exc) => (
diff --git a/src/components/mui/formik-inputs/mui-formik-datepicker.js b/src/components/mui/formik-inputs/mui-formik-datepicker.js
index 591ba6b24..b096cdddb 100644
--- a/src/components/mui/formik-inputs/mui-formik-datepicker.js
+++ b/src/components/mui/formik-inputs/mui-formik-datepicker.js
@@ -5,7 +5,13 @@ import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterMoment } from "@mui/x-date-pickers/AdapterMoment";
import { useField } from "formik";
-const MuiFormikDatepicker = ({ name, label, required, ...props }) => {
+const MuiFormikDatepicker = ({
+ name,
+ label,
+ required,
+ disabled = false,
+ ...props
+}) => {
const [field, meta, helpers] = useField(name);
const requiredLabel = `${label} *`;
const handleBlur = () => {
@@ -25,7 +31,8 @@ const MuiFormikDatepicker = ({ name, label, required, ...props }) => {
onBlur: handleBlur,
error: meta.touched && Boolean(meta.error),
helperText: meta.touched && meta.error,
- fullWidth: true
+ fullWidth: true,
+ disabled
},
day: {
sx: {
@@ -52,7 +59,8 @@ const MuiFormikDatepicker = ({ name, label, required, ...props }) => {
MuiFormikDatepicker.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string,
- required: PropTypes.bool
+ required: PropTypes.bool,
+ disabled: PropTypes.bool
};
export default MuiFormikDatepicker;
diff --git a/src/components/mui/formik-inputs/mui-formik-discountfield.js b/src/components/mui/formik-inputs/mui-formik-discountfield.js
index 4dc10e62e..81bde06c5 100644
--- a/src/components/mui/formik-inputs/mui-formik-discountfield.js
+++ b/src/components/mui/formik-inputs/mui-formik-discountfield.js
@@ -25,6 +25,7 @@ const MuiFormikDiscountField = ({
label,
discountType,
inCents = false,
+ disabled = false,
...props
}) => {
// eslint-disable-next-line no-unused-vars
@@ -84,6 +85,7 @@ const MuiFormikDiscountField = ({
value={getDisplayValue()}
onChange={handleChange}
type="number"
+ disabled={disabled}
slotProps={{
input: {
...adornment
diff --git a/src/components/mui/formik-inputs/mui-formik-timepicker.js b/src/components/mui/formik-inputs/mui-formik-timepicker.js
index bfc4af55e..5b98355b5 100644
--- a/src/components/mui/formik-inputs/mui-formik-timepicker.js
+++ b/src/components/mui/formik-inputs/mui-formik-timepicker.js
@@ -19,7 +19,13 @@ import { TimePicker } from "@mui/x-date-pickers/TimePicker";
import { useField } from "formik";
-const MuiFormikTimepicker = ({ name, minTime, maxTime, timeZone }) => {
+const MuiFormikTimepicker = ({
+ name,
+ minTime,
+ maxTime,
+ timeZone,
+ disabled = false
+}) => {
const [field, meta, helpers] = useField(name);
return (
@@ -39,6 +45,7 @@ const MuiFormikTimepicker = ({ name, minTime, maxTime, timeZone }) => {
helperText: meta.touched && meta.error,
size: "small",
fullWidth: true,
+ disabled,
sx: {
"& .MuiPickersSectionList-root": {
width: "100%"
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 0e8938082..ea516ecc1 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -80,6 +80,7 @@
"sort_by": "Sort By",
"sort_asc_label": "A-Z",
"sort_desc_label": "Z-A",
+ "n_a": "N/A",
"placeholders": {
"search_speakers": "Search Speakers by Name, Email, Speaker Id or Member Id",
"select_acceptance_criteria": "Select acceptance criteria",
diff --git a/src/pages/sponsors/sponsor-cart-tab/components/cart-view.js b/src/pages/sponsors/sponsor-cart-tab/components/cart-view.js
index 4d6924b8d..baca1c195 100644
--- a/src/pages/sponsors/sponsor-cart-tab/components/cart-view.js
+++ b/src/pages/sponsors/sponsor-cart-tab/components/cart-view.js
@@ -205,6 +205,7 @@ const CartView = ({
variant="contained"
color="primary"
style={{ minWidth: 250 }}
+ disabled={cart?.net_amount === 0}
onClick={handlePayCreditCard}
>
{T.translate("edit_sponsor.cart_tab.pay_cc")}
diff --git a/src/pages/sponsors/sponsor-cart-tab/components/edit-form/index.js b/src/pages/sponsors/sponsor-cart-tab/components/edit-form/index.js
index 13463473d..67331dfa6 100644
--- a/src/pages/sponsors/sponsor-cart-tab/components/edit-form/index.js
+++ b/src/pages/sponsors/sponsor-cart-tab/components/edit-form/index.js
@@ -21,12 +21,12 @@ import Box from "@mui/material/Box";
import moment from "moment-timezone";
import { epochToMomentTimeZone } from "openstack-uicore-foundation/lib/utils/methods";
import FormItemTable from "../../../../../components/mui/FormItemTable";
-import {
- DISCOUNT_TYPES,
- MILLISECONDS_IN_SECOND
-} from "../../../../../utils/constants";
+import { DISCOUNT_TYPES } from "../../../../../utils/constants";
import NotesModal from "../../../../../components/mui/NotesModal";
import ItemSettingsModal from "../../../../../components/mui/ItemSettingsModal";
+import {
+ getCurrentApplicableRate
+} from "../../../../../components/mui/FormItemTable/helpers";
const parseValue = (item, timeZone) => {
switch (item.type) {
@@ -204,18 +204,11 @@ const EditForm = ({
}) => {
const [notesItem, setNotesItem] = useState(null);
const [settingsItem, setSettingsItem] = useState(null);
- const hasRateExpired = useMemo(() => {
- const now = epochToMomentTimeZone(
- Math.floor(new Date() / MILLISECONDS_IN_SECOND),
- showTimeZone
- );
- const onsiteEndOfDay = epochToMomentTimeZone(
- showMetadata.onsite_price_end_date,
- showTimeZone
- )?.endOf("day");
- if (!onsiteEndOfDay || now.isSameOrBefore(onsiteEndOfDay)) return false;
- return true;
- }, [showMetadata, showTimeZone]);
+
+ const currentApplicableRate = useMemo(
+ () => getCurrentApplicableRate(showTimeZone, showMetadata),
+ [showMetadata, showTimeZone]
+ );
const handleCancel = () => {
onCancel();
@@ -295,7 +288,7 @@ const EditForm = ({
diff --git a/src/reducers/sponsors/sponsor-page-cart-list-reducer.js b/src/reducers/sponsors/sponsor-page-cart-list-reducer.js
index a984a68ed..c6199b3f0 100644
--- a/src/reducers/sponsors/sponsor-page-cart-list-reducer.js
+++ b/src/reducers/sponsors/sponsor-page-cart-list-reducer.js
@@ -175,6 +175,7 @@ const sponsorPageCartListReducer = (state = DEFAULT_STATE, action) => {
}
case RECEIVE_CART_FORM: {
const cartForm = payload.response;
+
return {
...state,
cartForm: {