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