From 23184df2d304b424d74c2c7e0a1be75290b3880d Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Wed, 29 Apr 2026 17:41:19 -0300 Subject: [PATCH 01/14] chore: ui for order details and modals --- package.json | 2 +- src/actions/sponsor-purchases-actions.js | 25 +++ src/components/CustomTheme.js | 27 ++- .../components/EditAddressDialog.jsx | 144 ++++++++++++++ .../components/EditClientDialog.jsx | 125 +++++++++++++ src/components/mui/ClientCard/index.jsx | 111 +++++++++++ src/components/mui/InfoNote/index.jsx | 17 ++ .../mui/OrderDetailsGrid/helpers.js | 73 ++++++++ src/components/mui/OrderDetailsGrid/index.js | 128 +++++++++++++ src/components/mui/RefundForm/index.jsx | 85 +++++++++ src/i18n/en.json | 53 +++++- src/pages/sponsors/sponsor-page/tabDefs.js | 23 ++- .../tabs/sponsor-purchases-tab/index.js | 3 +- .../sponsor-order-details.js | 177 ++++++++++++++++++ .../sponsor-page-purchase-list-reducer.js | 13 +- src/utils/constants.js | 5 + src/utils/methods.js | 11 ++ yarn.lock | 8 +- 18 files changed, 1014 insertions(+), 16 deletions(-) create mode 100644 src/components/mui/ClientCard/components/EditAddressDialog.jsx create mode 100644 src/components/mui/ClientCard/components/EditClientDialog.jsx create mode 100644 src/components/mui/ClientCard/index.jsx create mode 100644 src/components/mui/InfoNote/index.jsx create mode 100644 src/components/mui/OrderDetailsGrid/helpers.js create mode 100644 src/components/mui/OrderDetailsGrid/index.js create mode 100644 src/components/mui/RefundForm/index.jsx create mode 100644 src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/sponsor-order-details.js diff --git a/package.json b/package.json index 351b69f74..8f9fab70e 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "moment": "^2.29.1", "moment-duration-format": "^2.3.2", "moment-timezone": "^0.5.33", - "openstack-uicore-foundation": "5.0.18-beta.1", + "openstack-uicore-foundation": "5.0.17", "p-limit": "^6.1.0", "path-browserify": "^1.0.1", "postcss-loader": "^6.2.1", diff --git a/src/actions/sponsor-purchases-actions.js b/src/actions/sponsor-purchases-actions.js index 65367720d..0c7b4422e 100644 --- a/src/actions/sponsor-purchases-actions.js +++ b/src/actions/sponsor-purchases-actions.js @@ -34,6 +34,7 @@ export const REQUEST_SPONSOR_PURCHASES = "REQUEST_SPONSOR_PURCHASES"; export const RECEIVE_SPONSOR_PURCHASES = "RECEIVE_SPONSOR_PURCHASES"; export const SPONSOR_PURCHASE_STATUS_UPDATED = "SPONSOR_PURCHASE_STATUS_UPDATED"; +export const RECEIVE_SPONSOR_ORDER = "RECEIVE_SPONSOR_ORDER"; export const getSponsorPurchases = ( @@ -168,3 +169,27 @@ export const rejectSponsorPurchase = dispatch(stopLoading()); }); }; + +export const getSponsorOrder = (orderId) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { entity: sponsor } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken, + expand: + "forms,forms.items,forms.items.meta_fields,forms.items.type,refunds,payments,notes,fees" + }; + + return getRequest( + null, + createAction(RECEIVE_SPONSOR_ORDER), + `${window.PURCHASES_API_URL}/api/v2/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases/${orderId}`, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); +}; diff --git a/src/components/CustomTheme.js b/src/components/CustomTheme.js index f2df9db1f..7e8581ce0 100644 --- a/src/components/CustomTheme.js +++ b/src/components/CustomTheme.js @@ -34,6 +34,11 @@ const theme = createTheme({ fontSize: "12px", fontWeight: 400 }, + subtitle2: ({ theme }) => ({ + fontSize: "14px", + fontWeight: 500, + color: theme.palette.text.primary + }), h4: { fontSize: "34px", fontWeight: 500, @@ -70,12 +75,24 @@ const theme = createTheme({ }, MuiButton: { styleOverrides: { - root: { - fontSize: "14px", + root: ({ ownerState }) => ({ fontWeight: 500, - lineHeight: "20px", - padding: "10px 20px" - } + ...(ownerState.size === "small" && { + fontSize: "13px", + lineHeight: "18px", + padding: "9px 16px" + }), + ...(ownerState.size === "medium" && { + fontSize: "14px", + lineHeight: "20px", + padding: "10px 20px" + }), + ...(ownerState.size === "large" && { + fontSize: "16px", + lineHeight: "22px", + padding: "12px 24px" + }) + }) } }, MuiTab: { diff --git a/src/components/mui/ClientCard/components/EditAddressDialog.jsx b/src/components/mui/ClientCard/components/EditAddressDialog.jsx new file mode 100644 index 000000000..805fff9c1 --- /dev/null +++ b/src/components/mui/ClientCard/components/EditAddressDialog.jsx @@ -0,0 +1,144 @@ +/** + * 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 { useFormik, FormikProvider } from "formik"; +import * as yup from "yup"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid2 +} from "@mui/material"; +import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield"; + +const EditAddressDialog = ({ open, onClose, onSubmit, address }) => { + const formik = useFormik({ + initialValues: { + line1: address?.line1 ?? "", + line2: address?.line2 ?? "", + postal_code: address?.postal_code ?? "", + city: address?.city ?? "", + state: address?.state ?? "", + country: address?.country ?? "" + }, + validationSchema: yup.object({ + line1: yup + .string(T.translate("validation.string")) + .required(T.translate("validation.required")), + line2: yup.string(T.translate("validation.string")), + postal_code: yup + .string(T.translate("validation.string")) + .required(T.translate("validation.required")), + city: yup + .string(T.translate("validation.string")) + .required(T.translate("validation.required")), + state: yup.string(T.translate("validation.string")), + country: yup + .string(T.translate("validation.string")) + .required(T.translate("validation.required")) + }), + onSubmit: (values) => { + onSubmit(values); + onClose(); + }, + validateOnChange: false, + enableReinitialize: true + }); + + const handleClose = () => { + formik.resetForm(); + onClose(); + }; + + return ( + + {T.translate("client_card.edit_address")} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EditAddressDialog; diff --git a/src/components/mui/ClientCard/components/EditClientDialog.jsx b/src/components/mui/ClientCard/components/EditClientDialog.jsx new file mode 100644 index 000000000..9f63190c6 --- /dev/null +++ b/src/components/mui/ClientCard/components/EditClientDialog.jsx @@ -0,0 +1,125 @@ +/** + * 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 { useFormik, FormikProvider } from "formik"; +import * as yup from "yup"; +import { + Box, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Grid2 +} from "@mui/material"; +import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield"; + +const EditClientDialog = ({ open, onClose, onSubmit, client }) => { + const formik = useFormik({ + initialValues: { + company_name: client?.company_name ?? "", + contact_name: client?.contact_name ?? "", + contact_email: client?.contact_email ?? "", + contact_phone: client?.contact_phone ?? "" + }, + validationSchema: yup.object({ + company_name: yup + .string(T.translate("validation.string")) + .required(T.translate("validation.required")), + contact_name: yup + .string(T.translate("validation.string")) + .required(T.translate("validation.required")), + contact_email: yup + .string(T.translate("validation.string")) + .email(T.translate("validation.email")) + .required(T.translate("validation.required")), + contact_phone: yup + .string(T.translate("validation.string")) + .required(T.translate("validation.required")) + }), + onSubmit: (values) => { + onSubmit(values); + onClose(); + }, + validateOnChange: false, + enableReinitialize: true + }); + + const handleClose = () => { + formik.resetForm(); + onClose(); + }; + + return ( + + {T.translate("client_card.edit_client")} + + + + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +export default EditClientDialog; diff --git a/src/components/mui/ClientCard/index.jsx b/src/components/mui/ClientCard/index.jsx new file mode 100644 index 000000000..88a4418f0 --- /dev/null +++ b/src/components/mui/ClientCard/index.jsx @@ -0,0 +1,111 @@ +/** + * 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, { useState } from "react"; +import T from "i18n-react/dist/i18n-react"; +import { + Box, + Card, + CardContent, + Divider, + Grid2, + IconButton, + Typography +} from "@mui/material"; +import EditIcon from "@mui/icons-material/Edit"; +import EditClientDialog from "./components/EditClientDialog"; +import EditAddressDialog from "./components/EditAddressDialog"; + +const ClientCard = ({ client, address, onClientSubmit, onAddressSubmit }) => { + const [clientDialogOpen, setClientDialogOpen] = useState(false); + const [addressDialogOpen, setAddressDialogOpen] = useState(false); + + const clientDetails = `${client?.company_name}\n${client?.contact_name}`; + const addressDetails = `${address?.line1} ${address?.line2},\n${address?.postal_code} ${address?.city}`; + const hasAddress = addressDetails.trim().length > 1; + + return ( + <> + + + + {T.translate("client_card.title")} + + + + + + {T.translate("client_card.client")} + + + {clientDetails} + + setClientDialogOpen(true)} + sx={{ position: "absolute", top: 0, right: 0 }} + > + + + + + + + + + {T.translate("client_card.address")} + + + {hasAddress ? addressDetails : "N/A"} + + setAddressDialogOpen(true)} + sx={{ position: "absolute", top: 0, right: 0 }} + > + + + + + + + + + setClientDialogOpen(false)} + onSubmit={onClientSubmit} + client={client} + /> + setAddressDialogOpen(false)} + onSubmit={onAddressSubmit} + address={address} + /> + + ); +}; + +export default ClientCard; diff --git a/src/components/mui/InfoNote/index.jsx b/src/components/mui/InfoNote/index.jsx new file mode 100644 index 000000000..938cf88c4 --- /dev/null +++ b/src/components/mui/InfoNote/index.jsx @@ -0,0 +1,17 @@ +import React from "react"; +import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; + +const InfoNote = ({ message, sx }) => ( + + + + {message} + + +); + +export default InfoNote; diff --git a/src/components/mui/OrderDetailsGrid/helpers.js b/src/components/mui/OrderDetailsGrid/helpers.js new file mode 100644 index 000000000..60050352a --- /dev/null +++ b/src/components/mui/OrderDetailsGrid/helpers.js @@ -0,0 +1,73 @@ +import React from "react"; +import T from "i18n-react/dist/i18n-react"; +import { currencyAmountFromCents } from "openstack-uicore-foundation/lib/utils/money"; +import { BPS, SPONSOR_FORMS_METAFIELD_CLASS } from "../../../utils/constants"; + +const formatDiscount = (amount, type) => { + if (type === "Amount") return currencyAmountFromCents(amount); + if (type === "Rate") return `${amount / BPS}%`; // transform from bps to percentage + return ""; +}; + +export const normalizeOrder = (data) => ({ + ...data, + total: currencyAmountFromCents(data.net_amount || 0), + amount_due: `-${currencyAmountFromCents(-1 * (data.amount_due || 0))}`, // currencyAmountFromCents doesn't allow negatives + forms: data.forms.map((form) => ({ + ...form, + add_on_name: form.add_on?.name || "", + discount: formatDiscount(form.discount_amount, form.discount_type), + amount: currencyAmountFromCents(form.net_amount) + })) +}); + +export const mapOrderData = (lines, showItemDescription = false) => { + if (!lines) return []; + + return lines + .map((line) => ({ + ...line, + discount: line.discount_amount === 0 ? "" : line.discount + })) + .reduce((res, f) => { + f.items.forEach((it) => { + const formMetaFields = it.meta_fields.filter( + (mf) => mf.class_field === SPONSOR_FORMS_METAFIELD_CLASS.FORM + ); + + const item_name = [it.type?.name]; + + if (showItemDescription) { + item_name.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} +
+ ); + }) + ); + + item_name.push(
); // spacer + item_name.push( +
+ {T.translate("order_details_grid.total")}: {it.quantity} +
+ ); + item_name.push( +
+ {T.translate("order_details_grid.rate")}:{" "} + {currencyAmountFromCents(it.current_rate)} +
+ ); + } + + res.push({ ...f, item_name }); + }); + return res; + }, []); +}; diff --git a/src/components/mui/OrderDetailsGrid/index.js b/src/components/mui/OrderDetailsGrid/index.js new file mode 100644 index 000000000..a05e85c18 --- /dev/null +++ b/src/components/mui/OrderDetailsGrid/index.js @@ -0,0 +1,128 @@ +/** + * 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 MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; +import { + PaymentRow, + RefundRow, + FeeRow, + NotesRow, + TotalRow +} from "openstack-uicore-foundation/lib/components/mui/table/extra-rows"; +import IconButton from "@mui/material/IconButton"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +import { mapOrderData } from "./helpers"; + +const OrderDetailsGrid = ({ + lines, + notes, + payments, + refunds, + fees, + total, + amountDue, + withDescription = false +}) => { + const data = mapOrderData(lines, withDescription); + + const handleDelete = (row) => { + console.log("delete", row); + }; + + const handleUndo = (row) => { + console.log("undo", row); + }; + + const columns = [ + { + columnKey: "code", + header: T.translate("order_details_grid.code") + }, + { columnKey: "name", header: T.translate("order_details_grid.content") }, + { columnKey: "item_name", header: "" }, + { + columnKey: "addon_name", + header: T.translate("order_details_grid.addon") + }, + { + columnKey: "discount", + header: T.translate("order_details_grid.discount") + }, + { columnKey: "amount", header: T.translate("order_details_grid.amount") }, + { + columnKey: "actions", + header: T.translate("order_details_grid.action"), + align: "center", + render: (row) => { + if (row.is_deleted) { + return ( + handleUndo(row)}> + {T.translate("general.undo")} + + ); + } + + return ( + handleDelete(row)}> + + + ); + } + } + ]; + + return ( + + {notes && + notes.map((note) => ( + + ))} + {payments && + payments.map((payment) => ( + + ))} + {refunds && + refunds.map((refund) => ( + + ))} + {fees && + fees.map((fee) => ( + + ))} + + + ); +}; + +export default OrderDetailsGrid; diff --git a/src/components/mui/RefundForm/index.jsx b/src/components/mui/RefundForm/index.jsx new file mode 100644 index 000000000..4781a17b5 --- /dev/null +++ b/src/components/mui/RefundForm/index.jsx @@ -0,0 +1,85 @@ +/** + * Copyright 2017 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 { useFormik, FormikProvider } from "formik"; +import * as yup from "yup"; +import { Box, Button, Grid2 } from "@mui/material"; +import MuiFormikTextField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/textfield"; +import MuiFormikPriceField from "openstack-uicore-foundation/lib/components/mui/formik-inputs/price-field"; +import InfoNote from "../InfoNote"; + +const RefundForm = ({ onSubmit }) => { + const formik = useFormik({ + initialValues: { + reason: "", + amount: 0 + }, + validationSchema: yup.object({ + reason: yup + .string(T.translate("validation.string")) + .required(T.translate("validation.required")), + amount: yup + .number(T.translate("validation.number")) + .required(T.translate("validation.required")) + }), + onSubmit, + validateOnChange: false, + enableReinitialize: true + }); + + return ( + + + + + + + + + + + + + + + + + ); +}; + +export default RefundForm; diff --git a/src/i18n/en.json b/src/i18n/en.json index 22c593037..2ad476a2a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -54,6 +54,7 @@ "save": "Save", "confirm": "Confirm", "cancel": "Cancel", + "undo": "Undo", "return": "Return", "ingest": "Ingest", "save_and_publish": "Save & Publish", @@ -2604,7 +2605,21 @@ "amount": "Amount", "details": "Details", "purchases": "purchases", - "status_updated": "Status updated successfully." + "status_updated": "Status updated successfully.", + "order_details": { + "order": "Order", + "purchase_date": "Purchase Date", + "purchased_by": "Purchased by", + "show": "Show", + "sponsor": "Sponsor", + "tier": "Tier", + "payment_type": "Payment Type", + "payment_status": "Payment Status", + "payment_date": "Payment Date", + "general_info": "General Info", + "payment_info": "Payment Info", + "issue_refund": "Issue a Refund" + } }, "mu_tab": { "alert_info": "Here you can see the status of this Sponsor's Media Uploads. If an additional file upload is required, it must be requested from the specific page where it is needed.", @@ -4134,5 +4149,41 @@ "resync_helper": "Use the sync icon to re-sync individual room folders to Dropbox.", "resync_tooltip": "Re-sync Dropbox folder", "resync_dispatched": "Room re-sync task has been dispatched." + }, + "order_details_grid": { + "code": "Code", + "content": "Content", + "addon": "Add-on", + "discount": "Discount", + "amount": "Amount", + "amount_due": "AMOUNT DUE", + "total": "Total", + "rate": "Rate", + "action": "Action" + }, + "refund_form": { + "reason": "Reason for Refund", + "amount": "Amount", + "queue_refund": "Queue refund", + "info": "If the original payment was made via Stripe, the refund will be automatically queued and processed." + }, + "client_card": { + "title": "Client & Address Details", + "client": "Client", + "address": "Address", + "edit_client": "Edit Client", + "edit_address": "Edit Address", + "company_name": "Company Name", + "contact_name": "Contact Name", + "contact_email": "Contact Email", + "contact_phone": "Contact Phone", + "line1": "Address Line 1", + "line2": "Address Line 2", + "postal_code": "Postal Code", + "city": "City", + "state": "State", + "country": "Country", + "save": "Save", + "cancel": "Cancel" } } diff --git a/src/pages/sponsors/sponsor-page/tabDefs.js b/src/pages/sponsors/sponsor-page/tabDefs.js index 1ccb7d88f..e06109643 100644 --- a/src/pages/sponsors/sponsor-page/tabDefs.js +++ b/src/pages/sponsors/sponsor-page/tabDefs.js @@ -12,6 +12,7 @@ import SponsorFormsManageItems from "./tabs/sponsor-forms-tab/components/manage- import SponsorCartTab from "./tabs/sponsor-cart-tab"; import SponsorPurchasesTab from "./tabs/sponsor-purchases-tab"; import SponsorBadgeScans from "./tabs/sponsor-badge-scans"; +import SponsorOrderDetails from "./tabs/sponsor-purchases-tab/sponsor-order-details"; const SponsorFormsRoute = ({ match }) => (
@@ -32,6 +33,25 @@ const SponsorFormsRoute = ({ match }) => (
); +const SponsorPurchasesRoute = ({ match }) => ( +
+ + + + + +
+); + export const SPONSOR_PAGE_TABS = [ { labelKey: "edit_sponsor.tab.general", @@ -77,8 +97,7 @@ export const SPONSOR_PAGE_TABS = [ { labelKey: "edit_sponsor.tab.purchases", path: "/purchases", - exact: true, - component: SponsorPurchasesTab, + component: SponsorPurchasesRoute, accessRoute: ACCESS_ROUTES.ADMIN_SPONSORS }, { diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/index.js index 30ac7fe6e..9e7de9dfa 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/index.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/index.js @@ -25,6 +25,7 @@ import { import MenuIcon from "@mui/icons-material/Menu"; import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input"; +import history from "../../../../../history"; import { approveSponsorPurchase, getSponsorPurchases, @@ -76,7 +77,7 @@ const SponsorPurchasesTab = ({ }; const handleDetails = (item) => { - console.log("DETAILS : ", item); + history.push(`purchases/${item.id}`); }; const handleMenu = (item) => { diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/sponsor-order-details.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/sponsor-order-details.js new file mode 100644 index 000000000..76f4dffa0 --- /dev/null +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/sponsor-order-details.js @@ -0,0 +1,177 @@ +/** + * 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, { useEffect } from "react"; +import { connect } from "react-redux"; +import T from "i18n-react/dist/i18n-react"; +import { Box, Card, CardContent, Grid2, Typography } from "@mui/material"; +import { + ListCard +} from "openstack-uicore-foundation/lib/components/mui/cards"; +import OrderDetailsGrid from "../../../../../components/mui/OrderDetailsGrid"; +import { getSponsorOrder } from "../../../../../actions/sponsor-purchases-actions"; +import { ACCESS_ROUTES, DATE_FORMAT } from "../../../../../utils/constants"; +import Restrict from "../../../../../routes/restrict"; +import { formatDate } from "../../../../../utils/methods"; +import RefundForm from "../../../../../components/mui/RefundForm"; +import ClientCard from "../../../../../components/mui/ClientCard"; + +const SponsorOrderDetails = ({ + match, + currentOrder, + currentSummit, + currentSponsor, + getSponsorOrder +}) => { + const orderId = match.params.order_id; + + useEffect(() => { + getSponsorOrder(orderId); + }, []); + + if (!currentOrder) return null; + + const { client, address } = currentOrder; + + const dashInfoRows = [ + { + label: T.translate( + "edit_sponsor.purchase_tab.order_details.purchase_date" + ), + value: formatDate(currentOrder.created, "LOC", DATE_FORMAT) + }, + { + label: T.translate( + "edit_sponsor.purchase_tab.order_details.purchased_by" + ), + value: `${currentOrder.purchased_by_full_name} - ${currentOrder.purchased_by_email}` + }, + { + label: T.translate("edit_sponsor.purchase_tab.order_details.show"), + value: currentSummit.name + }, + { + label: T.translate("edit_sponsor.purchase_tab.order_details.sponsor"), + value: currentSponsor.company_name + }, + { + label: T.translate("edit_sponsor.purchase_tab.order_details.tier"), + value: currentSponsor.sponsorships.map((s) => s.type_name).join(", ") + } + ]; + + const paymentInfoRows = [ + { + label: T.translate("edit_sponsor.purchase_tab.order_details.order"), + value: currentOrder.number + }, + { + label: T.translate( + "edit_sponsor.purchase_tab.order_details.payment_type" + ), + value: currentOrder.payment_method + }, + { + label: T.translate( + "edit_sponsor.purchase_tab.order_details.payment_status" + ), + value: currentOrder.status + }, + { + label: T.translate( + "edit_sponsor.purchase_tab.order_details.payment_date" + ), + value: formatDate(currentOrder.created, "LOC", DATE_FORMAT) + } + ]; + + return ( + + + {T.translate("edit_sponsor.purchase_tab.order_details.order")}{" "} + {currentOrder.number} + + + + + + + + + + + + + + + + + + + + + + + {T.translate( + "edit_sponsor.purchase_tab.order_details.issue_refund" + )} + + + + + + + + ); +}; + +const mapStateToProps = ({ + sponsorPagePurchaseListState, + currentSummitState, + currentSponsorState +}) => ({ + currentOrder: sponsorPagePurchaseListState.currentOrder, + currentSummit: currentSummitState.currentSummit, + currentSponsor: currentSponsorState.entity +}); + +export default Restrict( + connect(mapStateToProps, { + getSponsorOrder + })(SponsorOrderDetails), + ACCESS_ROUTES.ADMIN_SPONSORS +); diff --git a/src/reducers/sponsors/sponsor-page-purchase-list-reducer.js b/src/reducers/sponsors/sponsor-page-purchase-list-reducer.js index a90ea400c..30fdd0c7b 100644 --- a/src/reducers/sponsors/sponsor-page-purchase-list-reducer.js +++ b/src/reducers/sponsors/sponsor-page-purchase-list-reducer.js @@ -15,12 +15,14 @@ import moment from "moment-timezone"; import { amountFromCents } from "openstack-uicore-foundation/lib/utils/money"; import { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions"; import { - REQUEST_SPONSOR_PURCHASES, + RECEIVE_SPONSOR_ORDER, RECEIVE_SPONSOR_PURCHASES, + REQUEST_SPONSOR_PURCHASES, SPONSOR_PURCHASE_STATUS_UPDATED } from "../../actions/sponsor-purchases-actions"; import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions"; import { MILLISECONDS_TO_SECONDS } from "../../utils/constants"; +import { normalizeOrder } from "../../components/mui/OrderDetailsGrid/helpers"; const DEFAULT_STATE = { purchases: [], @@ -30,7 +32,8 @@ const DEFAULT_STATE = { lastPage: 1, perPage: 10, totalCount: 0, - term: "" + term: "", + currentOrder: null }; const sponsorPagePurchaseListReducer = (state = DEFAULT_STATE, action) => { @@ -87,6 +90,12 @@ const sponsorPagePurchaseListReducer = (state = DEFAULT_STATE, action) => { return { ...state, purchases }; } + case RECEIVE_SPONSOR_ORDER: { + const data = payload.response; + const currentOrder = normalizeOrder(data); + + return { ...state, currentOrder }; + } default: return state; } diff --git a/src/utils/constants.js b/src/utils/constants.js index 4aeb71930..50e23fc63 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -92,6 +92,9 @@ export const HUNDRED_PER_PAGE = 100; export const DEFAULT_EXTRA_QUESTIONS_PER_PAGE = 100; export const DEFAULT_ORDER_DIR = 1; +export const DATE_FORMAT = "MM/DD/YYYY"; +export const DATETIME_FORMAT = "MM/DD/YYYY hh:mm a"; + export const INT_BASE = 10; export const FIFTY_NINE = 59; export const ONE_MINUTE = 60; @@ -113,6 +116,8 @@ export const DATE_FILTER_ARRAY_SIZE = 2; export const MILLISECONDS_TO_SECONDS = 1000; +export const BPS = 100; + export const INDEX_NOT_FOUND = -1; export const CODE_200 = 200; diff --git a/src/utils/methods.js b/src/utils/methods.js index 110b37fee..b52505028 100644 --- a/src/utils/methods.js +++ b/src/utils/methods.js @@ -24,6 +24,7 @@ import T from "i18n-react/dist/i18n-react"; import { BADGE_QR_MINIMUM_EXPECTED_FIELDS, BYTES_IN_MEGABYTE, + DATETIME_FORMAT, ERROR_CODE_401, ERROR_CODE_403, ERROR_CODE_412, @@ -618,3 +619,13 @@ export const normalizeSelectAllField = ( [listName]: items.map((a) => a.id ?? a) }; }; + +export const formatDate = (date, timeZone, format = DATETIME_FORMAT) => { + if (timeZone === "LOC") { + return moment(date * MILLISECONDS_TO_SECONDS).format(format); + } + + return moment(date * MILLISECONDS_TO_SECONDS) + .tz(timeZone) + .format(format); +}; diff --git a/yarn.lock b/yarn.lock index 210f5d00f..994a480ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9056,10 +9056,10 @@ open@^10.0.3: is-inside-container "^1.0.0" wsl-utils "^0.1.0" -openstack-uicore-foundation@5.0.18-beta.1: - version "5.0.18-beta.1" - resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.18-beta.1.tgz#f79763d84cbe555c5401e254dbd756675bb52043" - integrity sha512-szrIkY99ljJwowsd3jsbmnOTd7hq3SeOTKCB2FFWu06hQ1FdRwX+vew6uqMTiXrIZt+DJlLaUXnd/bQaXCLqeg== +openstack-uicore-foundation@5.0.17-beta.4: + version "5.0.17-beta.4" + resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.17-beta.4.tgz#c5bf87e5282cdb40ae200477d4e355ee7cf81bc7" + integrity sha512-mehMZ50HZe4ZZ9Sc3SuCoGDhqEyeDzADqr9p1HLg3lts1mGgyteDZ9bccptba+cSsrJYqq8lD05kB9tAiW0lbA== optionator@^0.9.1: version "0.9.4" From d16443351a74ab20342d29ec64d66a7faad27ac2 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Thu, 30 Apr 2026 17:33:02 -0300 Subject: [PATCH 02/14] chore: API integration - WIP --- .env.example | 2 +- src/actions/sponsor-purchases-actions.js | 167 +++++++++++++++++- src/components/mui/ClientCard/index.jsx | 4 +- .../mui/OrderDetailsGrid/helpers.js | 11 +- src/components/mui/OrderDetailsGrid/index.js | 27 ++- src/i18n/en.json | 4 +- .../sponsor-order-details.js | 58 +++++- .../sponsor-page-purchase-list-reducer.js | 18 +- 8 files changed, 256 insertions(+), 35 deletions(-) diff --git a/.env.example b/.env.example index 066181d62..544d0fd7e 100644 --- a/.env.example +++ b/.env.example @@ -13,7 +13,7 @@ PUB_API_BASE_URL= OS_BASE_URL= SCOPES_BASE_REALM=${API_BASE_URL} PURCHASES_API_URL=https://purchases-api.dev.fnopen.com -PURCHASES_API_SCOPES="purchases-show-medata/read purchases-show-medata/write show-form/read show-form/write customized-form/write customized-form/read carts/read carts/write purchases/read cart-notes/write payment/write payment-profile/read" +PURCHASES_API_SCOPES="purchases-show-medata/read purchases-show-medata/write show-form/read show-form/write customized-form/write customized-form/read carts/read carts/write purchases/read purchases/write cart-notes/write payment/write payment-profile/read" SPONSOR_USERS_API_URL=https://sponsor-users-api.dev.fnopen.com SPONSOR_USERS_SCOPES="show-medata/read show-medata/write access-requests/read access-requests/write sponsor-users/read sponsor-users/write groups/read groups/write media-upload/write" EMAIL_SCOPES="clients/read templates/read templates/write emails/read" diff --git a/src/actions/sponsor-purchases-actions.js b/src/actions/sponsor-purchases-actions.js index 0c7b4422e..28a334dd7 100644 --- a/src/actions/sponsor-purchases-actions.js +++ b/src/actions/sponsor-purchases-actions.js @@ -24,8 +24,8 @@ import T from "i18n-react/dist/i18n-react"; import { escapeFilterValue, getAccessTokenSafely } from "../utils/methods"; import { DEFAULT_CURRENT_PAGE, - DEFAULT_ORDER_DIR, DEFAULT_PER_PAGE, + DUMMY_ACTION, PURCHASE_STATUS } from "../utils/constants"; import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; @@ -35,14 +35,16 @@ export const RECEIVE_SPONSOR_PURCHASES = "RECEIVE_SPONSOR_PURCHASES"; export const SPONSOR_PURCHASE_STATUS_UPDATED = "SPONSOR_PURCHASE_STATUS_UPDATED"; export const RECEIVE_SPONSOR_ORDER = "RECEIVE_SPONSOR_ORDER"; +export const SPONSOR_CLIENT_ADDRESS_UPDATED = "SPONSOR_CLIENT_ADDRESS_UPDATED"; +export const SPONSOR_CLIENT_UPDATED = "SPONSOR_CLIENT_UPDATED"; export const getSponsorPurchases = ( term = "", page = DEFAULT_CURRENT_PAGE, perPage = DEFAULT_PER_PAGE, - order = "id", - orderDir = DEFAULT_ORDER_DIR + order = "created", + orderDir = -1 ) => async (dispatch, getState) => { const { currentSummitState, currentSponsorState } = getState(); @@ -193,3 +195,162 @@ export const getSponsorOrder = (orderId) => async (dispatch, getState) => { dispatch(stopLoading()); }); }; + +export const updateClientAddress = + (orderId, address) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { entity: sponsor } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return putRequest( + null, + createAction(SPONSOR_CLIENT_ADDRESS_UPDATED), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases/${orderId}/address`, + address, + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate( + "edit_sponsor.purchase_tab.order_details.address_updated" + ) + }) + ); + }) + .finally(() => { + dispatch(stopLoading()); + }); + }; + +export const updateClientInfo = + (orderId, client) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { entity: sponsor } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return putRequest( + null, + createAction(SPONSOR_CLIENT_UPDATED), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases/${orderId}/client`, + client, + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate( + "edit_sponsor.purchase_tab.order_details.client_updated" + ) + }) + ); + }) + .finally(() => { + dispatch(stopLoading()); + }); + }; + +export const cancelSponsorForm = + (orderId, lineId) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { entity: sponsor } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + + const params = { + access_token: accessToken + }; + + dispatch(startLoading()); + + return deleteRequest( + null, + createAction(DUMMY_ACTION), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases/${orderId}/lines/${lineId}/cancel`, + null, + snackbarErrorHandler + )(params)(dispatch) + .then(() => dispatch(getSponsorOrder(orderId))) + .catch(console.log) // need to catch promise reject + .finally(() => { + dispatch(stopLoading()); + }); + }; + +export const undoCancelSponsorForm = + (orderId, lineId) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { entity: sponsor } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return putRequest( + null, + createAction(DUMMY_ACTION), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases/${orderId}/lines/${lineId}/cancel`, + {}, + snackbarErrorHandler + )(params)(dispatch) + .then(() => dispatch(getSponsorOrder(orderId))) + .finally(() => { + dispatch(stopLoading()); + }); + }; + +export const refundSponsorOrder = + (orderId, amount, reason) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { entity: sponsor } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return putRequest( + null, + createAction(DUMMY_ACTION), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases/${orderId}/refund`, + { amount, reason }, + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate( + "edit_sponsor.purchase_tab.order_details.order_refunded" + ) + }) + ); + dispatch(getSponsorOrder(orderId)); + }) + .finally(() => { + dispatch(stopLoading()); + }); + }; diff --git a/src/components/mui/ClientCard/index.jsx b/src/components/mui/ClientCard/index.jsx index 88a4418f0..bc6ce69be 100644 --- a/src/components/mui/ClientCard/index.jsx +++ b/src/components/mui/ClientCard/index.jsx @@ -30,8 +30,8 @@ const ClientCard = ({ client, address, onClientSubmit, onAddressSubmit }) => { const [clientDialogOpen, setClientDialogOpen] = useState(false); const [addressDialogOpen, setAddressDialogOpen] = useState(false); - const clientDetails = `${client?.company_name}\n${client?.contact_name}`; - const addressDetails = `${address?.line1} ${address?.line2},\n${address?.postal_code} ${address?.city}`; + const clientDetails = `${client?.company_name}\n${client?.contact_name} - ${client?.contact_email} - ${client?.contact_phone}`; + const addressDetails = `${address?.line1} ${address?.line2},\n${address?.postal_code} ${address?.city} ${address?.state} ${address?.country}`; const hasAddress = addressDetails.trim().length > 1; return ( diff --git a/src/components/mui/OrderDetailsGrid/helpers.js b/src/components/mui/OrderDetailsGrid/helpers.js index 60050352a..c9b0528bc 100644 --- a/src/components/mui/OrderDetailsGrid/helpers.js +++ b/src/components/mui/OrderDetailsGrid/helpers.js @@ -12,7 +12,10 @@ const formatDiscount = (amount, type) => { export const normalizeOrder = (data) => ({ ...data, total: currencyAmountFromCents(data.net_amount || 0), - amount_due: `-${currencyAmountFromCents(-1 * (data.amount_due || 0))}`, // currencyAmountFromCents doesn't allow negatives + amount_due: + data.amount_due === 0 + ? "$0.00" + : `-${currencyAmountFromCents(-1 * (data.amount_due || 0))}`, // currencyAmountFromCents doesn't allow negatives forms: data.forms.map((form) => ({ ...form, add_on_name: form.add_on?.name || "", @@ -66,7 +69,11 @@ export const mapOrderData = (lines, showItemDescription = false) => { ); } - res.push({ ...f, item_name }); + const amount = currencyAmountFromCents(it.amount); + const lineId = it.line_id; + const cancelled = it.canceled_by_id !== null; + + res.push({ ...f, item_name, amount, id: lineId, cancelled }); }); return res; }, []); diff --git a/src/components/mui/OrderDetailsGrid/index.js b/src/components/mui/OrderDetailsGrid/index.js index a05e85c18..2c5db3e63 100644 --- a/src/components/mui/OrderDetailsGrid/index.js +++ b/src/components/mui/OrderDetailsGrid/index.js @@ -34,18 +34,12 @@ const OrderDetailsGrid = ({ fees, total, amountDue, - withDescription = false + withDescription = false, + onCancelForm, + onUndoCancelForm }) => { const data = mapOrderData(lines, withDescription); - const handleDelete = (row) => { - console.log("delete", row); - }; - - const handleUndo = (row) => { - console.log("undo", row); - }; - const columns = [ { columnKey: "code", @@ -67,16 +61,17 @@ const OrderDetailsGrid = ({ header: T.translate("order_details_grid.action"), align: "center", render: (row) => { - if (row.is_deleted) { + if (row.cancelled) { return ( - handleUndo(row)}> - {T.translate("general.undo")} + onUndoCancelForm(row)}> + {" "} + {T.translate("general.undo").toUpperCase()} ); } return ( - handleDelete(row)}> + onCancelForm(row)}> ); @@ -85,7 +80,11 @@ const OrderDetailsGrid = ({ ]; return ( - + {notes && notes.map((note) => ( { const orderId = match.params.order_id; @@ -95,6 +105,26 @@ const SponsorOrderDetails = ({ } ]; + const handleClientSave = (values) => { + updateClientInfo(currentOrder.id, values); + }; + + const handleAddressSave = (values) => { + updateClientAddress(currentOrder.id, values); + }; + + const handleCancelForm = (item) => { + cancelSponsorForm(currentOrder.id, item.id); + }; + + const handleUndoCancelForm = (item) => { + undoCancelSponsorForm(currentOrder.id, item.id); + }; + + const handleOrderRefund = (values) => { + refundSponsorOrder(currentOrder.id, values.amount, values.reason); + }; + return ( @@ -119,7 +149,12 @@ const SponsorOrderDetails = ({ /> - + @@ -150,7 +187,7 @@ const SponsorOrderDetails = ({ "edit_sponsor.purchase_tab.order_details.issue_refund" )} - + @@ -171,7 +208,12 @@ const mapStateToProps = ({ export default Restrict( connect(mapStateToProps, { - getSponsorOrder + getSponsorOrder, + updateClientInfo, + updateClientAddress, + cancelSponsorForm, + undoCancelSponsorForm, + refundSponsorOrder })(SponsorOrderDetails), ACCESS_ROUTES.ADMIN_SPONSORS ); diff --git a/src/reducers/sponsors/sponsor-page-purchase-list-reducer.js b/src/reducers/sponsors/sponsor-page-purchase-list-reducer.js index 30fdd0c7b..ba21105ec 100644 --- a/src/reducers/sponsors/sponsor-page-purchase-list-reducer.js +++ b/src/reducers/sponsors/sponsor-page-purchase-list-reducer.js @@ -18,6 +18,8 @@ import { RECEIVE_SPONSOR_ORDER, RECEIVE_SPONSOR_PURCHASES, REQUEST_SPONSOR_PURCHASES, + SPONSOR_CLIENT_ADDRESS_UPDATED, + SPONSOR_CLIENT_UPDATED, SPONSOR_PURCHASE_STATUS_UPDATED } from "../../actions/sponsor-purchases-actions"; import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions"; @@ -26,8 +28,8 @@ import { normalizeOrder } from "../../components/mui/OrderDetailsGrid/helpers"; const DEFAULT_STATE = { purchases: [], - order: "order", - orderDir: 1, + order: "created", + orderDir: -1, currentPage: 1, lastPage: 1, perPage: 10, @@ -67,8 +69,8 @@ const sponsorPagePurchaseListReducer = (state = DEFAULT_STATE, action) => { const purchases = payload.response.data.map((a) => ({ ...a, order: a.order_number, - amount: `$${amountFromCents(a.raw_amount - a.discount_amount)}`, - purchased: moment(a.created * MILLISECONDS_TO_SECONDS).format( + amount: `$${amountFromCents(a.net_amount)}`, + purchased: moment(a.purchased_date * MILLISECONDS_TO_SECONDS).format( "YYYY/MM/DD HH:mm a" ) })); @@ -96,6 +98,14 @@ const sponsorPagePurchaseListReducer = (state = DEFAULT_STATE, action) => { return { ...state, currentOrder }; } + case SPONSOR_CLIENT_UPDATED: { + const client = payload.response; + return { ...state, currentOrder: { ...state.currentOrder, client } }; + } + case SPONSOR_CLIENT_ADDRESS_UPDATED: { + const address = payload.response; + return { ...state, currentOrder: { ...state.currentOrder, address } }; + } default: return state; } From 52777fdc2c359da2fbee4e43add1e739c9a2e4e8 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Mon, 4 May 2026 10:47:39 -0300 Subject: [PATCH 03/14] chore: integrate refund API calls --- src/actions/sponsor-actions.js | 4 ++-- src/actions/sponsor-purchases-actions.js | 7 ++++--- src/components/mui/InfoNote/index.jsx | 2 +- src/components/mui/RefundForm/index.jsx | 1 + src/i18n/en.json | 4 +++- .../sponsor-purchases-tab/sponsor-order-details.js | 11 +++++++++-- 6 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/actions/sponsor-actions.js b/src/actions/sponsor-actions.js index 196cd4c0f..94c116f0c 100644 --- a/src/actions/sponsor-actions.js +++ b/src/actions/sponsor-actions.js @@ -309,9 +309,9 @@ export const getSponsor = (sponsorId) => async (dispatch, getState) => { const params = { access_token: accessToken, expand: - "company,members,sponsorships,sponsorships.type,featured_event,extra_questions,extra_questions.values,lead_report_setting", + "company,members,sponsorships,sponsorships.type,sponsorships.type.type,featured_event,extra_questions,extra_questions.values,lead_report_setting", fields: - "featured_event.id,featured_event.title,sponsorships.id,sponsorships.type.id,sponsorships.type.type_id" + "featured_event.id,featured_event.title,sponsorships.id,sponsorships.type.id,sponsorships.type.type.id,sponsorships.type.type.name" }; return getRequest( diff --git a/src/actions/sponsor-purchases-actions.js b/src/actions/sponsor-purchases-actions.js index 28a334dd7..a9c60f04b 100644 --- a/src/actions/sponsor-purchases-actions.js +++ b/src/actions/sponsor-purchases-actions.js @@ -16,6 +16,7 @@ import { createAction, getRequest, putRequest, + postRequest, deleteRequest, startLoading, stopLoading @@ -332,11 +333,11 @@ export const refundSponsorOrder = access_token: accessToken }; - return putRequest( + return postRequest( null, createAction(DUMMY_ACTION), - `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases/${orderId}/refund`, - { amount, reason }, + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases/${orderId}/refunds`, + { amount, notes: reason }, snackbarErrorHandler )(params)(dispatch) .then(() => { diff --git a/src/components/mui/InfoNote/index.jsx b/src/components/mui/InfoNote/index.jsx index 938cf88c4..413735faa 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/RefundForm/index.jsx b/src/components/mui/RefundForm/index.jsx index 4781a17b5..dc14734f0 100644 --- a/src/components/mui/RefundForm/index.jsx +++ b/src/components/mui/RefundForm/index.jsx @@ -61,6 +61,7 @@ const RefundForm = ({ onSubmit }) => { name="amount" fullWidth size="small" + inCents label={T.translate("refund_form.amount")} /> diff --git a/src/i18n/en.json b/src/i18n/en.json index 7e0721645..28675833d 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2619,8 +2619,10 @@ "general_info": "General Info", "payment_info": "Payment Info", "issue_refund": "Issue a Refund", + "info_note": "Active order items can be canceled. Canceled items show an undo action to restore them. Refund and payment rows are display-only.", "address_updated": "Address updated successfully", - "client_updated": "Client info updated successfully" + "client_updated": "Client info updated successfully", + "order_refunded": "Refund issued successfully." } }, "mu_tab": { diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/sponsor-order-details.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/sponsor-order-details.js index 0c1496b31..cb4e44070 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/sponsor-order-details.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/sponsor-order-details.js @@ -30,6 +30,7 @@ import Restrict from "../../../../../routes/restrict"; import { formatDate } from "../../../../../utils/methods"; import RefundForm from "../../../../../components/mui/RefundForm"; import ClientCard from "../../../../../components/mui/ClientCard"; +import InfoNote from "../../../../../components/mui/InfoNote"; const SponsorOrderDetails = ({ match, @@ -72,11 +73,11 @@ const SponsorOrderDetails = ({ }, { label: T.translate("edit_sponsor.purchase_tab.order_details.sponsor"), - value: currentSponsor.company_name + value: currentSponsor.company?.name || "N/A" }, { label: T.translate("edit_sponsor.purchase_tab.order_details.tier"), - value: currentSponsor.sponsorships.map((s) => s.type_name).join(", ") + value: currentSponsor.sponsorships.map((s) => s.type.type.name).join(", ") } ]; @@ -162,6 +163,12 @@ const SponsorOrderDetails = ({ variant="outlined" > + Date: Mon, 4 May 2026 15:09:27 -0300 Subject: [PATCH 04/14] chore: upgrade uicore version --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index 994a480ad..7c2a07da5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9056,10 +9056,10 @@ open@^10.0.3: is-inside-container "^1.0.0" wsl-utils "^0.1.0" -openstack-uicore-foundation@5.0.17-beta.4: - version "5.0.17-beta.4" - resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.17-beta.4.tgz#c5bf87e5282cdb40ae200477d4e355ee7cf81bc7" - integrity sha512-mehMZ50HZe4ZZ9Sc3SuCoGDhqEyeDzADqr9p1HLg3lts1mGgyteDZ9bccptba+cSsrJYqq8lD05kB9tAiW0lbA== +openstack-uicore-foundation@5.0.16: + version "5.0.16" + resolved "https://registry.yarnpkg.com/openstack-uicore-foundation/-/openstack-uicore-foundation-5.0.16.tgz#5b7a8b79e926769b98bd2449633da8cf94bc78e8" + integrity sha512-33juij1pykdjspMQzEwGpWcgre7pP4RQc+ZYlSuMafG3Vv7ydaSJyC5Y8EVkHQx2RQeJqmL4P9v3bSe1mHg2CA== optionator@^0.9.1: version "0.9.4" From 895cc85c3805962ee988e9e2d90b18e96232a826 Mon Sep 17 00:00:00 2001 From: Santiago Palenque Date: Tue, 5 May 2026 19:14:53 -0300 Subject: [PATCH 05/14] fix: cart payment view and order details view to use same component - fix hide unselectd items --- src/actions/sponsor-cart-actions.js | 30 ++- .../mui/OrderDetailsGrid/helpers.js | 118 ++++----- src/components/mui/OrderDetailsGrid/index.js | 223 +++++++++++++----- src/i18n/en.json | 3 +- .../sponsor-cart-tab/components/cart-view.js | 22 +- .../components/payment-view.js | 69 ++---- .../sponsor-page-cart-list-reducer.js | 40 ++-- yarn.lock | 8 +- 8 files changed, 307 insertions(+), 206 deletions(-) diff --git a/src/actions/sponsor-cart-actions.js b/src/actions/sponsor-cart-actions.js index 6f03ad1e7..40da5411b 100644 --- a/src/actions/sponsor-cart-actions.js +++ b/src/actions/sponsor-cart-actions.js @@ -50,6 +50,7 @@ export const SPONSOR_CART_NOTE_ADDED = "SPONSOR_CART_NOTE_ADDED"; export const SPONSOR_CART_NOTE_UPDATED = "SPONSOR_CART_NOTE_UPDATED"; export const SPONSOR_CART_NOTE_DELETED = "SPONSOR_CART_NOTE_DELETED"; export const CART_STATUS_UPDATED = "CART_STATUS_UPDATED"; +export const SPONSOR_CART_REOPENED = "SPONSOR_CART_REOPENED"; export const RECEIVE_PAYMENT_PROFILE = "RECEIVE_PAYMENT_PROFILE"; export const OFFLINE_PAYMENT_CREATED = "OFFLINE_PAYMENT_CREATED"; export const PAYMENT_INTENT_CREATED = "PAYMENT_INTENT_CREATED"; @@ -91,7 +92,7 @@ export const getSponsorCart = const params = { access_token: accessToken, - expand: "forms,forms.items,forms.items.type,forms.items.meta_fields,notes" + expand: "forms,forms.items,forms.items.type,forms.items.meta_fields,notes,fees" }; if (filter.length > 0) { @@ -473,7 +474,7 @@ export const checkoutCart = () => async (dispatch, getState) => { const params = { access_token: accessToken, - expand: "forms,forms.items,forms.items.type,forms.items.meta_fields,notes" + expand: "forms,forms.items,forms.items.type,forms.items.meta_fields,notes,fees" }; return putRequest( @@ -487,6 +488,31 @@ export const checkoutCart = () => async (dispatch, getState) => { }); }; +export const reopenCart = () => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { entity: sponsor } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + + dispatch(startLoading()); + + const params = { + access_token: accessToken, + expand: "forms,forms.items,forms.items.type,forms.items.meta_fields,notes,fees" + }; + + return deleteRequest( + null, + createAction(SPONSOR_CART_REOPENED), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/carts/current/checkout`, + null, + snackbarErrorHandler + )(params)(dispatch) + .finally(() => { + dispatch(stopLoading()); + }); +}; + export const payWithInvoice = () => async (dispatch, getState) => { const { currentSummitState, currentSponsorState, sponsorPageCartListState } = getState(); diff --git a/src/components/mui/OrderDetailsGrid/helpers.js b/src/components/mui/OrderDetailsGrid/helpers.js index c9b0528bc..3471ddf6f 100644 --- a/src/components/mui/OrderDetailsGrid/helpers.js +++ b/src/components/mui/OrderDetailsGrid/helpers.js @@ -9,72 +9,76 @@ const formatDiscount = (amount, type) => { return ""; }; -export const normalizeOrder = (data) => ({ - ...data, - total: currencyAmountFromCents(data.net_amount || 0), - amount_due: - data.amount_due === 0 - ? "$0.00" - : `-${currencyAmountFromCents(-1 * (data.amount_due || 0))}`, // currencyAmountFromCents doesn't allow negatives - forms: data.forms.map((form) => ({ - ...form, - add_on_name: form.add_on?.name || "", - discount: formatDiscount(form.discount_amount, form.discount_type), - amount: currencyAmountFromCents(form.net_amount) - })) -}); - -export const mapOrderData = (lines, showItemDescription = false) => { - if (!lines) return []; +export const normalizeOrder = (data) => { + const amountDueSign = data?.amount_due < 0 ? "-" : ""; + const amountDueStr = currencyAmountFromCents(Math.abs(data.amount_due || 0)); // currencyAmountFromCents doesn't allow negatives + const amountDue = `${amountDueSign}${amountDueStr}`; - return lines - .map((line) => ({ - ...line, - discount: line.discount_amount === 0 ? "" : line.discount + return { + ...data, + total: currencyAmountFromCents(data.net_amount || 0), + amount_due: data.amount_due === 0 ? "$0.00" : amountDue, + forms: data.forms.map((form) => ({ + ...form, + add_on_name: form.add_on?.name || "", + discount: formatDiscount(form.discount_amount, form.discount_type), + discount_total: form.discount_in_cents || 100, + amount: currencyAmountFromCents(form.net_amount || 0) })) - .reduce((res, f) => { - f.items.forEach((it) => { + }; +}; + +export const mapOrderData = (forms) => { + if (!forms) return []; + + return forms.map((form) => ({ + ...form, + 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 item_name = [it.type?.name]; + const itemDetails = [it.type?.name]; - if (showItemDescription) { - item_name.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} -
- ); - }) - ); + // item details + 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} +
+ ); + }) + ); - item_name.push(
); // spacer - item_name.push( -
- {T.translate("order_details_grid.total")}: {it.quantity} -
- ); - item_name.push( -
- {T.translate("order_details_grid.rate")}:{" "} - {currencyAmountFromCents(it.current_rate)} -
- ); - } + itemDetails.push(
); // spacer + itemDetails.push( +
+ {T.translate("order_details_grid.total")}: {it.quantity} +
+ ); - const amount = currencyAmountFromCents(it.amount); + const amount = currencyAmountFromCents(it.amount || 0); const lineId = it.line_id; - const cancelled = it.canceled_by_id !== null; + const cancelled = !!it.canceled_by_id; + const rate = currencyAmountFromCents(it.current_rate || 0); - res.push({ ...f, item_name, amount, id: lineId, cancelled }); - }); - return res; - }, []); + return { + id: lineId, + code: form.code, + name: form.name, + rate, + addon_name: form.addon_name, + item_name: itemDetails, + amount, + cancelled + }; + }) + })); }; diff --git a/src/components/mui/OrderDetailsGrid/index.js b/src/components/mui/OrderDetailsGrid/index.js index 2c5db3e63..ecfd2f2ee 100644 --- a/src/components/mui/OrderDetailsGrid/index.js +++ b/src/components/mui/OrderDetailsGrid/index.js @@ -13,17 +13,25 @@ import React from "react"; import T from "i18n-react/dist/i18n-react"; -import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; import { - PaymentRow, - RefundRow, FeeRow, NotesRow, + PaymentRow, + RefundRow, + DiscountRow, TotalRow } from "openstack-uicore-foundation/lib/components/mui/table/extra-rows"; import IconButton from "@mui/material/IconButton"; import DeleteIcon from "@mui/icons-material/Delete"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; +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 { mapOrderData } from "./helpers"; const OrderDetailsGrid = ({ @@ -39,88 +47,173 @@ const OrderDetailsGrid = ({ onUndoCancelForm }) => { const data = mapOrderData(lines, withDescription); + const showActionCol = onCancelForm && onUndoCancelForm; + const trailingCols = showActionCol ? 1 : 0; const columns = [ { columnKey: "code", header: T.translate("order_details_grid.code") }, - { columnKey: "name", header: T.translate("order_details_grid.content") }, - { columnKey: "item_name", header: "" }, + { + columnKey: "name", + header: T.translate("order_details_grid.contents") + }, { columnKey: "addon_name", header: T.translate("order_details_grid.addon") }, { - columnKey: "discount", - header: T.translate("order_details_grid.discount") + columnKey: "item_name", + header: T.translate("order_details_grid.details") + }, + { + columnKey: "rate", + header: T.translate("order_details_grid.rate") }, - { columnKey: "amount", header: T.translate("order_details_grid.amount") }, { - columnKey: "actions", - header: T.translate("order_details_grid.action"), - align: "center", - render: (row) => { - if (row.cancelled) { + columnKey: "amount", + header: T.translate("order_details_grid.amount") + } + ]; + + if (showActionCol) { + columns.push( + { + columnKey: "actions", + header: T.translate("order_details_grid.action"), + align: "center", + render: (row) => { + if (row.cancelled) { + return ( + onUndoCancelForm(row)}> + {" "} + {T.translate("general.undo").toUpperCase()} + + ); + } + return ( - onUndoCancelForm(row)}> - {" "} - {T.translate("general.undo").toUpperCase()} + onCancelForm(row)}> + ); } - - return ( - onCancelForm(row)}> - - - ); } - } - ]; + ) + } return ( - - {notes && - notes.map((note) => ( - - ))} - {payments && - payments.map((payment) => ( - - ))} - {refunds && - refunds.map((refund) => ( - - ))} - {fees && - fees.map((fee) => ( - - ))} - - + + + + + {/* TABLE HEADER */} + + + {columns.map((col) => ( + + {col.header} + + ))} + + + + {data.map((form) => { + const rows = form.items.map((row) => ( + + {columns.map((col) => ( + + {col.render ? ( + col.render(row) + ) : ( + + {row[col.columnKey]} + + )} + + ))} + + )); + + rows.push( + + ); + + return rows; + })} + {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/i18n/en.json b/src/i18n/en.json index 28675833d..39bd44b13 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -4156,8 +4156,9 @@ }, "order_details_grid": { "code": "Code", - "content": "Content", + "contents": "Contents", "addon": "Add-on", + "details": "Details", "discount": "Discount", "amount": "Amount", "amount_due": "AMOUNT DUE", diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/cart-view.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/cart-view.js index 875833845..66ffb0028 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/cart-view.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-cart-tab/components/cart-view.js @@ -36,6 +36,7 @@ import { getSponsorCart, lockSponsorCartForm, payWithInvoice, + reopenCart, saveSponsorCartNote, unlockSponsorCartForm } from "../../../../../../actions/sponsor-cart-actions"; @@ -56,10 +57,12 @@ const CartView = ({ saveSponsorCartNote, deleteSponsorCartNote, checkoutCart, + reopenCart, payWithInvoice }) => { const cartIsPendingPayment = cart?.status === SPONSOR_CART_STATUS.PENDING_PAYMENT; + const cartIsCheckedOut = cart?.status === SPONSOR_CART_STATUS.CHECKED_OUT; useEffect(() => { getSponsorCart(); @@ -69,6 +72,10 @@ const CartView = ({ getSponsorCart(searchTerm); }; + const handleReopenCart = () => { + reopenCart(); + }; + const handleDelete = (itemId) => { deleteSponsorCartForm(itemId); }; @@ -180,12 +187,12 @@ const CartView = ({ mb: 2 }} > - + {cart && ( {cart?.forms.length} forms in Cart )} - + + {(cartIsPendingPayment || cartIsCheckedOut) && ( + + )}