diff --git a/.env.example b/.env.example
index b71ae142b..9c466c3b5 100644
--- a/.env.example
+++ b/.env.example
@@ -12,7 +12,7 @@ PRINT_APP_URL=https://badge-print-app.dev.fnopen.com
PUB_API_BASE_URL=
OS_BASE_URL=
SCOPES_BASE_REALM=${API_BASE_URL}
-PURCHASES_API_SCOPES=purchases-show-medata/read purchases-show-medata/write show-form/read show-form/write
+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
SPONSOR_USERS_API_SCOPES="show-medata/read show-medata/write access-requests/read access-requests/write sponsor-users/read sponsor-users/write groups/read groups/write"
EMAIL_SCOPES="clients/read templates/read templates/write emails/read"
FILE_UPLOAD_SCOPES="files/upload"
diff --git a/src/actions/sponsor-cart-actions.js b/src/actions/sponsor-cart-actions.js
new file mode 100644
index 000000000..3d5cf0cd4
--- /dev/null
+++ b/src/actions/sponsor-cart-actions.js
@@ -0,0 +1,165 @@
+/**
+ * Copyright 2018 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 {
+ authErrorHandler,
+ createAction,
+ getRequest,
+ deleteRequest,
+ putRequest,
+ startLoading,
+ stopLoading
+} from "openstack-uicore-foundation/lib/utils/actions";
+
+import T from "i18n-react";
+import { escapeFilterValue, getAccessTokenSafely } from "../utils/methods";
+import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions";
+import { ERROR_CODE_404 } from "../utils/constants";
+
+export const REQUEST_SPONSOR_CART = "REQUEST_SPONSOR_CART";
+export const RECEIVE_SPONSOR_CART = "RECEIVE_SPONSOR_CART";
+export const SPONSOR_CART_FORM_DELETED = "SPONSOR_CART_FORM_DELETED";
+export const SPONSOR_CART_FORM_LOCKED = "SPONSOR_CART_FORM_LOCKED";
+
+const customErrorHandler = (err, res) => (dispatch, state) => {
+ const code = err.status;
+ dispatch(stopLoading());
+ switch (code) {
+ case ERROR_CODE_404:
+ break;
+ default:
+ authErrorHandler(err, res)(dispatch, state);
+ }
+};
+
+export const getSponsorCart =
+ (term = "") =>
+ async (dispatch, getState) => {
+ const { currentSummitState, currentSponsorState } = getState();
+ const { currentSummit } = currentSummitState;
+ const {
+ entity: { id: sponsorId }
+ } = currentSponsorState;
+ const accessToken = await getAccessTokenSafely();
+ const summitTZ = currentSummit.time_zone.name;
+ const filter = [];
+
+ dispatch(startLoading());
+
+ if (term) {
+ const escapedTerm = escapeFilterValue(term);
+ filter.push(`name=@${escapedTerm},code=@${escapedTerm}`);
+ }
+
+ const params = {
+ access_token: accessToken
+ };
+
+ if (filter.length > 0) {
+ params["filter[]"] = filter;
+ }
+
+ return getRequest(
+ createAction(REQUEST_SPONSOR_CART),
+ createAction(RECEIVE_SPONSOR_CART),
+ `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/carts/current`,
+ customErrorHandler,
+ { term, summitTZ }
+ )(params)(dispatch)
+ .catch((err) => {
+ console.error(err);
+ })
+ .finally(() => {
+ dispatch(stopLoading());
+ });
+ };
+
+export const deleteSponsorCartForm = (formId) => async (dispatch, getState) => {
+ const { currentSummitState, currentSponsorState } = getState();
+ const { currentSummit } = currentSummitState;
+ const {
+ entity: { id: sponsorId }
+ } = currentSponsorState;
+ const accessToken = await getAccessTokenSafely();
+ const params = { access_token: accessToken };
+
+ dispatch(startLoading());
+
+ return deleteRequest(
+ null,
+ createAction(SPONSOR_CART_FORM_DELETED)({ formId }),
+ `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/carts/current/forms/${formId}`,
+ null,
+ snackbarErrorHandler
+ )(params)(dispatch)
+ .then(() => {
+ getSponsorCart()(dispatch, getState);
+ dispatch(
+ snackbarSuccessHandler({
+ title: T.translate("general.success"),
+ html: T.translate("sponsor_forms.form_delete_success")
+ })
+ );
+ })
+ .finally(() => {
+ dispatch(stopLoading());
+ });
+};
+
+export const lockSponsorCartForm = (formId) => 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());
+
+ putRequest(
+ null,
+ createAction(SPONSOR_CART_FORM_LOCKED)({ formId, locked: true }),
+ `${window.SPONSOR_USERS_API_URL}/api/v1/shows/${currentSummit.id}/sponsors/${sponsor.id}/carts/current/forms/${formId}/lock`,
+ {},
+ snackbarErrorHandler
+ )(params)(dispatch)
+ .catch(console.log) // need to catch promise reject
+ .finally(() => {
+ dispatch(stopLoading());
+ });
+};
+
+export const unlockSponsorCartForm = (formId) => 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(SPONSOR_CART_FORM_LOCKED)({ formId, locked: false }),
+ `${window.SPONSOR_USERS_API_URL}/api/v1/shows/${currentSummit.id}/sponsors/${sponsor.id}/carts/current/forms/${formId}/lock`,
+ null,
+ snackbarErrorHandler
+ )(params)(dispatch)
+ .catch(console.log) // need to catch promise reject
+ .finally(() => {
+ dispatch(stopLoading());
+ });
+};
diff --git a/src/components/mui/table/extra-rows/NotesRow.jsx b/src/components/mui/table/extra-rows/NotesRow.jsx
new file mode 100644
index 000000000..6b4b61394
--- /dev/null
+++ b/src/components/mui/table/extra-rows/NotesRow.jsx
@@ -0,0 +1,16 @@
+import TableCell from "@mui/material/TableCell";
+import TableRow from "@mui/material/TableRow";
+import * as React from "react";
+import { Typography } from "@mui/material";
+
+const NotesRow = ({ colCount, note }) => (
+
+
+
+ {note}
+
+
+
+ );
+
+export default NotesRow;
diff --git a/src/components/mui/table/extra-rows/TotalRow.jsx b/src/components/mui/table/extra-rows/TotalRow.jsx
new file mode 100644
index 000000000..2c1fa7d64
--- /dev/null
+++ b/src/components/mui/table/extra-rows/TotalRow.jsx
@@ -0,0 +1,32 @@
+import TableCell from "@mui/material/TableCell";
+import TableRow from "@mui/material/TableRow";
+import * as React from "react";
+import T from "i18n-react/dist/i18n-react";
+
+const TotalRow = ({ columns, targetCol, total, trailing = 0 }) => {
+ return (
+
+ {columns.map((col, i) => {
+ if (i === 0)
+ return (
+
+ {T.translate("mui_table.total")}
+
+ );
+ if (col.columnKey === targetCol)
+ return (
+
+ {total}
+
+ );
+ return ;
+ })}
+ {[...Array(trailing)].map((_, i) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+ ))}
+
+ );
+};
+
+export default TotalRow;
diff --git a/src/components/mui/table/extra-rows/index.js b/src/components/mui/table/extra-rows/index.js
new file mode 100644
index 000000000..6e7961237
--- /dev/null
+++ b/src/components/mui/table/extra-rows/index.js
@@ -0,0 +1,2 @@
+export { default as TotalRow } from "./TotalRow";
+export { default as NotesRow } from "./NotesRow";
diff --git a/src/components/mui/table/mui-table.js b/src/components/mui/table/mui-table.js
index dc33d4e89..5f8b9d55b 100644
--- a/src/components/mui/table/mui-table.js
+++ b/src/components/mui/table/mui-table.js
@@ -3,9 +3,9 @@ import T from "i18n-react/dist/i18n-react";
import { isBoolean } from "lodash";
import {
Box,
+ Button,
IconButton,
Paper,
- Button,
Table,
TableBody,
TableCell,
@@ -31,6 +31,7 @@ import styles from "./mui-table.module.less";
const MuiTable = ({
columns = [],
data = [],
+ children,
totalRows,
perPage,
currentPage,
@@ -253,6 +254,8 @@ const MuiTable = ({
)}
))}
+ {/* Here we inject extra rows passed as children */}
+ {children}
{data.length === 0 && (
@@ -265,28 +268,30 @@ const MuiTable = ({
{/* PAGINATION */}
-
+ {perPage && currentPage && (
+
+ )}
);
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 7384e4d74..3ae951780 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -2434,6 +2434,20 @@
"unarchived": "Form successfully unarchived."
}
},
+ "cart_tab": {
+ "new_form": "New Form",
+ "forms": "forms",
+ "code": "Code",
+ "name": "Name",
+ "add_ons": "Add-ons",
+ "discount": "Discount",
+ "amount": "Amount",
+ "manage_items": "Manage Items",
+ "add_form": "Add Form",
+ "no_cart": "No cart found.",
+ "pay_cc": "pay with credit card or ach",
+ "pay_invoice": "pay with invoice"
+ },
"placeholders": {
"select_sponsorship": "Select a Sponsorship",
"sponsorship_type": "Start typing to choose a Tier...",
@@ -3762,7 +3776,8 @@
"no_items": "No items found.",
"rows_per_page": "Rows per page",
"sorted_desc": "sorted descending",
- "sorted_asc": "sorted ascending"
+ "sorted_asc": "sorted ascending",
+ "total": "Total"
},
"event_rsvp_list": {
"name": "Name",
diff --git a/src/pages/sponsors/edit-sponsor-page.js b/src/pages/sponsors/edit-sponsor-page.js
index 5f3b6e214..a69defbb8 100644
--- a/src/pages/sponsors/edit-sponsor-page.js
+++ b/src/pages/sponsors/edit-sponsor-page.js
@@ -42,6 +42,7 @@ import SponsorGeneralForm from "../../components/forms/sponsor-general-form/inde
import SponsorUsersListPerSponsorPage from "./sponsor-users-list-per-sponsor";
import SponsorFormsTab from "./sponsor-forms-tab";
import SponsorBadgeScans from "./sponsor-badge-scans";
+import SponsorCartTab from "./sponsor-cart-tab";
const CustomTabPanel = (props) => {
const { children, value, index, ...other } = props;
@@ -127,7 +128,7 @@ const EditSponsorPage = (props) => {
return (
-
+
{entity.company?.name}
@@ -185,6 +186,9 @@ const EditSponsorPage = (props) => {
+
+
+
diff --git a/src/pages/sponsors/sponsor-cart-tab/index.js b/src/pages/sponsors/sponsor-cart-tab/index.js
new file mode 100644
index 000000000..c41d0f587
--- /dev/null
+++ b/src/pages/sponsors/sponsor-cart-tab/index.js
@@ -0,0 +1,241 @@
+/**
+ * Copyright 2024 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, useState } from "react";
+import { connect } from "react-redux";
+import T from "i18n-react/dist/i18n-react";
+import {
+ Box,
+ Button,
+ Grid2,
+ IconButton,
+ Paper,
+ Typography
+} from "@mui/material";
+import LockOpenIcon from "@mui/icons-material/LockOpen";
+import LockClosedIcon from "@mui/icons-material/Lock";
+import AddIcon from "@mui/icons-material/Add";
+import {
+ deleteSponsorCartForm,
+ getSponsorCart,
+ lockSponsorCartForm,
+ unlockSponsorCartForm
+} from "../../../actions/sponsor-cart-actions";
+import SearchInput from "../../../components/mui/search-input";
+import { TotalRow } from "../../../components/mui/table/extra-rows";
+import MuiTable from "../../../components/mui/table/mui-table";
+
+const SponsorCartTab = ({
+ cart,
+ term,
+ sponsor,
+ summitId,
+ getSponsorCart,
+ deleteSponsorCartForm,
+ lockSponsorCartForm,
+ unlockSponsorCartForm
+}) => {
+ const [openPopup, setOpenPopup] = useState(null);
+ const [formEdit, setFormEdit] = useState(null);
+
+ useEffect(() => {
+ getSponsorCart();
+ }, []);
+
+ const handleSearch = (searchTerm) => {
+ getSponsorCart(searchTerm);
+ };
+
+ const handleManageItems = (item) => {
+ console.log("MANAGE ITEMS : ", item);
+ };
+
+ const handleEdit = (item) => {
+ setFormEdit(item);
+ };
+
+ const handleDelete = (itemId) => {
+ deleteSponsorCartForm(itemId);
+ };
+
+ const handleLock = (form) => {
+ if (form.is_locked) {
+ unlockSponsorCartForm(form.form_id);
+ } else {
+ lockSponsorCartForm(form.form_id);
+ }
+ };
+
+ const handlePayCreditCard = () => {
+ console.log("PAY CREDIT CARD");
+ };
+
+ const handlePayInvoice = () => {
+ console.log("PAY INVOICE");
+ };
+
+ const tableColumns = [
+ {
+ columnKey: "code",
+ header: T.translate("edit_sponsor.cart_tab.code")
+ },
+ {
+ columnKey: "name",
+ header: T.translate("edit_sponsor.cart_tab.name")
+ },
+ {
+ columnKey: "allowed_add_ons",
+ header: T.translate("edit_sponsor.cart_tab.add_ons"),
+ render: (row) =>
+ row.allowed_add_ons?.length > 0
+ ? row.allowed_add_ons.map((a) => `${a.type} ${a.name}`).join(", ")
+ : "None"
+ },
+ {
+ columnKey: "manage_items",
+ header: "",
+ width: 100,
+ align: "center",
+ render: (row) => (
+
+ )
+ },
+ {
+ columnKey: "discount",
+ header: T.translate("edit_sponsor.cart_tab.discount")
+ },
+ {
+ columnKey: "amount",
+ header: T.translate("edit_sponsor.cart_tab.amount")
+ },
+ {
+ columnKey: "lock",
+ header: "",
+ render: (row) => (
+ handleLock(row)}>
+ {row.is_locked ? (
+
+ ) : (
+
+ )}
+
+ )
+ }
+ ];
+
+ return (
+
+
+
+ {cart && (
+ {cart?.forms.length} forms in Cart
+ )}
+
+
+
+
+
+
+
+
+ {!cart && (
+
+ {T.translate("edit_sponsor.cart_tab.no_cart")}
+
+ )}
+ {!!cart && (
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+};
+
+const mapStateToProps = ({ sponsorPageCartListState }) => ({
+ ...sponsorPageCartListState
+});
+
+export default connect(mapStateToProps, {
+ getSponsorCart,
+ deleteSponsorCartForm,
+ lockSponsorCartForm,
+ unlockSponsorCartForm
+})(SponsorCartTab);
diff --git a/src/reducers/sponsors/sponsor-page-cart-list-reducer.js b/src/reducers/sponsors/sponsor-page-cart-list-reducer.js
new file mode 100644
index 000000000..7d12f3429
--- /dev/null
+++ b/src/reducers/sponsors/sponsor-page-cart-list-reducer.js
@@ -0,0 +1,97 @@
+/**
+ * Copyright 2019 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 { LOGOUT_USER } from "openstack-uicore-foundation/lib/security/actions";
+import { amountFromCents } from "openstack-uicore-foundation/lib/utils/money";
+import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions";
+import {
+ REQUEST_SPONSOR_CART,
+ RECEIVE_SPONSOR_CART,
+ SPONSOR_CART_FORM_DELETED,
+ SPONSOR_CART_FORM_LOCKED
+} from "../../actions/sponsor-cart-actions";
+
+const DEFAULT_STATE = {
+ cart: null,
+ term: "",
+ summitTZ: ""
+};
+
+const sponsorPageCartListReducer = (state = DEFAULT_STATE, action) => {
+ const { type, payload } = action;
+
+ switch (type) {
+ case SET_CURRENT_SUMMIT:
+ case LOGOUT_USER: {
+ return DEFAULT_STATE;
+ }
+ case REQUEST_SPONSOR_CART: {
+ const { term, summitTZ } = payload;
+
+ return {
+ ...state,
+ cart: null,
+ term,
+ summitTZ
+ };
+ }
+ case RECEIVE_SPONSOR_CART: {
+ const cart = payload.response;
+ cart.forms = cart.forms.map((form) => ({
+ ...form,
+ amount: amountFromCents(form.net_amount),
+ discount: amountFromCents(form.discount_amount)
+ }));
+ cart.total = amountFromCents(cart.net_amount);
+
+ return {
+ ...state,
+ cart
+ };
+ }
+ case SPONSOR_CART_FORM_DELETED: {
+ const { formId } = payload;
+ const forms = state.cart.forms.filter((form) => form.id !== formId);
+
+ return {
+ ...state,
+ cart: {
+ ...state.cart,
+ forms
+ }
+ };
+ }
+ case SPONSOR_CART_FORM_LOCKED: {
+ const { formId, locked } = payload;
+
+ const forms = state.cart.forms.map((form) => {
+ if (form.form_id === formId) {
+ return {...form, locked};
+ }
+ return form;
+ });
+
+ return {
+ ...state,
+ cart: {
+ ...state.cart,
+ forms
+ }
+ };
+ }
+ default:
+ return state;
+ }
+};
+
+export default sponsorPageCartListReducer;
diff --git a/src/store.js b/src/store.js
index 64762f8ac..0d224d75f 100644
--- a/src/store.js
+++ b/src/store.js
@@ -164,6 +164,7 @@ import eventRSVPInvitationListReducer from "./reducers/rsvps/event-rsvp-invitati
import eventRSVPReducer from "./reducers/events/event-rsvp-reducer.js";
import sponsorPageFormsListReducer from "./reducers/sponsors/sponsor-page-forms-list-reducer.js";
import sponsorCustomizedFormReducer from "./reducers/sponsors/sponsor-customized-form-reducer.js";
+import sponsorPageCartListReducer from "./reducers/sponsors/sponsor-page-cart-list-reducer";
// default: localStorage if web, AsyncStorage if react-native
@@ -249,6 +250,7 @@ const reducers = persistCombineReducers(config, {
sponsorFormItemsListState: sponsorFormItemsListReducer,
sponsorUsersListState: sponsorUsersListReducer,
sponsorPageFormsListState: sponsorPageFormsListReducer,
+ sponsorPageCartListState: sponsorPageCartListReducer,
sponsorCustomizedFormState: sponsorCustomizedFormReducer,
currentSponsorPromocodeListState: sponsorPromocodeListReducer,
currentSponsorExtraQuestionState: sponsorExtraQuestionReducer,
diff --git a/src/utils/methods.js b/src/utils/methods.js
index 4eca84e11..a558014ca 100644
--- a/src/utils/methods.js
+++ b/src/utils/methods.js
@@ -21,17 +21,17 @@ import Swal from "sweetalert2";
import * as Sentry from "@sentry/react";
import T from "i18n-react/dist/i18n-react";
import {
+ BADGE_QR_MINIMUM_EXPECTED_FIELDS,
ERROR_CODE_401,
ERROR_CODE_403,
ERROR_CODE_412,
ERROR_CODE_500,
HEX_RADIX,
- MILLISECONDS_TO_SECONDS,
- ONE_MINUTE,
INT_BASE,
- OR_FILTER,
MARKETING_SETTING_TYPE_HEX_COLOR,
- BADGE_QR_MINIMUM_EXPECTED_FIELDS
+ MILLISECONDS_TO_SECONDS,
+ ONE_MINUTE,
+ OR_FILTER
} from "./constants";
const DAY_IN_SECONDS = 86400; // 86400 seconds per day