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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
230 changes: 167 additions & 63 deletions src/components/mui/FormItemTable/__tests__/FormItemTable.test.js

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ import React, { useEffect } from "react";
import { useField } from "formik";
import MuiFormikTextField from "../../formik-inputs/mui-formik-textfield";

const GlobalQuantityField = ({ row, extraColumns, value }) => {
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);
Expand All @@ -35,6 +40,7 @@ const GlobalQuantityField = ({ row, extraColumns, value }) => {
fullWidth
size="small"
type="number"
disabled={disabled}
slotProps={{
htmlInput: {
readOnly: isReadOnly,
Expand Down
35 changes: 17 additions & 18 deletions src/components/mui/FormItemTable/components/ItemTableField.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,42 +21,44 @@ 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 <MuiFormikCheckbox name={name} label={label} />;
return <MuiFormikCheckbox {...commonProps} />;
case "CheckBoxList":
return (
<MuiFormikDropdownCheckbox
name={name}
label={label}
{...commonProps}
size="small"
options={field.values.map((v) => ({ value: v.id, label: v.value }))}
/>
);
case "RadioButtonList":
return (
<MuiFormikDropdownRadio
name={name}
label={label}
{...commonProps}
size="small"
options={field.values.map((v) => ({ value: v.id, label: v.value }))}
/>
);
case "DateTime":
return <MuiFormikDatepicker name={name} label={label} />;
return <MuiFormikDatepicker {...commonProps} />;
case "Time":
return (
<MuiFormikTimepicker name={name} timeZone={timeZone} label={label} />
);
return <MuiFormikTimepicker {...commonProps} timeZone={timeZone} />;
case "Quantity":
return (
<MuiFormikTextField
name={name}
{...commonProps}
fullWidth
label={label}
size="small"
type="number"
slotProps={{
Expand All @@ -71,7 +73,7 @@ const ItemTableField = ({ rowId, field, timeZone, label = "" }) => {
);
case "ComboBox":
return (
<MuiFormikSelect name={name} label={label} size="small">
<MuiFormikSelect {...commonProps} size="small">
{field.values.map((v) => (
<MenuItem key={`ddopt-${v.id}`} value={v.id}>
{v.value}
Expand All @@ -80,14 +82,11 @@ const ItemTableField = ({ rowId, field, timeZone, label = "" }) => {
</MuiFormikSelect>
);
case "Text":
return (
<MuiFormikTextField name={name} label={label} fullWidth size="small" />
);
return <MuiFormikTextField {...commonProps} fullWidth size="small" />;
case "TextArea":
return (
<MuiFormikTextField
name={name}
label={label}
{...commonProps}
fullWidth
size="small"
multiline
Expand Down
28 changes: 28 additions & 0 deletions src/components/mui/FormItemTable/components/UnderlyingAlertNote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import { Typography } from "@mui/material";
import ErrorIcon from "@mui/icons-material/Error";
import T from "i18n-react/dist/i18n-react";

const UnderlyingAlertNote = ({ showAdditionalItems }) => {
if (!showAdditionalItems) return null;

return (
<Typography
variant="body2"
component="p"
sx={{ color: "error.warning", fontSize: "0.8rem" }}
>
Comment thread
santipalenque marked this conversation as resolved.
<ErrorIcon
color="error"
sx={{
fontSize: "1rem",
top: "0.2rem",
position: "relative"
}}
/>{" "}
{T.translate("edit_sponsor.cart_tab.edit_form.additional_info")}
</Typography>
);
};

export default UnderlyingAlertNote;
30 changes: 30 additions & 0 deletions src/components/mui/FormItemTable/helpers.js
Original file line number Diff line number Diff line change
@@ -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;
90 changes: 25 additions & 65 deletions src/components/mui/FormItemTable/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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");
Expand All @@ -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;
Comment on lines 78 to +81

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect custom-rate fallback in FormItemTable:"
sed -n '68,92p' src/components/mui/FormItemTable/index.js

echo
echo "Inspect price field implementation to verify whether 0 is stored as number/string:"
fd 'mui-formik-pricefield.*' src --exec sed -n '1,220p' {}

Repository: fntechgit/summit-admin

Length of output: 2931


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Search for other uses of customRate in the codebase ==="
rg "custom_rate|customRate" src --type js -B 2 -A 2 | head -100

echo
echo "=== Check for validation or constraint on custom_rate ==="
rg "custom_rate" src --type js | grep -i "validat\|required\|constraint\|min\|max" || echo "No validation constraints found"

echo
echo "=== Check FormItemTable context for business logic ==="
sed -n '1,120p' src/components/mui/FormItemTable/index.js | tail -50

Repository: fntechgit/summit-admin

Length of output: 9587


Use nullish coalescing to respect explicit 0 custom rates when computing row totals.

Line 79 uses customRate || row.rates[currentApplicableRate], which incorrectly treats numeric 0 as missing and falls back to the default rate. When a user explicitly sets a custom rate of 0 (e.g., for a complimentary sponsorship item), this causes the row total to be calculated with the wrong rate, inflating the amount.

Proposed fix
-    const rate = customRate || row.rates[currentApplicableRate];
+    const rate = customRate ?? row.rates?.[currentApplicableRate];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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;
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;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/mui/FormItemTable/index.js` around lines 78 - 81, The
calculation incorrectly treats a customRate of 0 as falsy because it uses ||;
update the expression that sets rate (currently using customRate ||
row.rates[currentApplicableRate]) to use nullish coalescing (customRate ??
row.rates[currentApplicableRate]) so explicit 0 custom rates are respected;
ensure you keep the subsequent null check (if (rate == null || qty == null)
return 0) unchanged and reference variables customRate, rate, values, row.rates,
currentApplicableRate, and qty when making the change.


return qty * rate;
};

Expand All @@ -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 =
Expand All @@ -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);
Expand Down Expand Up @@ -190,29 +166,11 @@ const FormItemTable = ({
<TableCell>{row.code}</TableCell>
<TableCell sx={{ position: "relative" }}>
<div>{row.name}</div>
{hasItemFields(row) && itemFieldsIncomplete(row) && (
<Typography
variant="body2"
component="p"
sx={{
color: "warning.main",
fontSize: "0.6em",
position: "absolute"
}}
>
<ErrorIcon
color="warning"
sx={{
fontSize: "1.4em",
top: "0.2em",
position: "relative"
}}
/>{" "}
{T.translate(
"edit_sponsor.cart_tab.edit_form.additional_info"
)}
</Typography>
)}
<UnderlyingAlertNote
showAdditionalItems={
hasItemFields(row) && itemFieldsIncomplete(row)
}
/>
</TableCell>
<TableCell>
<MuiFormikPriceField
Expand All @@ -229,19 +187,21 @@ const FormItemTable = ({
opacity: currentApplicableRate === "early_bird" ? 1 : "38%"
}}
>
{currencyAmountFromCents(row.rates.early_bird)}
{formatRate(row.rates.early_bird)}
</TableCell>
<TableCell
sx={{
opacity: currentApplicableRate === "standard" ? 1 : "38%"
}}
>
{currencyAmountFromCents(row.rates.standard)}
{formatRate(row.rates.standard)}
</TableCell>
<TableCell
sx={{ opacity: currentApplicableRate === "onsite" ? 1 : "38%" }}
sx={{
opacity: currentApplicableRate === "onsite" ? 1 : "38%"
}}
>
{currencyAmountFromCents(row.rates.onsite)}
{formatRate(row.rates.onsite)}
</TableCell>
{extraColumns.map((exc) => (
<TableCell key={`datacell-${row.form_item_id}-${exc.type_id}`}>
Expand Down
14 changes: 11 additions & 3 deletions src/components/mui/formik-inputs/mui-formik-datepicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand All @@ -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: {
Expand All @@ -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;
2 changes: 2 additions & 0 deletions src/components/mui/formik-inputs/mui-formik-discountfield.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const MuiFormikDiscountField = ({
label,
discountType,
inCents = false,
disabled = false,
...props
}) => {
// eslint-disable-next-line no-unused-vars
Expand Down Expand Up @@ -84,6 +85,7 @@ const MuiFormikDiscountField = ({
value={getDisplayValue()}
onChange={handleChange}
type="number"
disabled={disabled}
slotProps={{
input: {
...adornment
Expand Down
Loading
Loading