diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js index e54fc2134..934e81458 100644 --- a/src/actions/sponsor-forms-actions.js +++ b/src/actions/sponsor-forms-actions.js @@ -65,6 +65,31 @@ export const SPONSOR_CUSTOMIZED_FORM_DELETED = export const SPONSOR_CUSTOMIZED_FORM_ARCHIVED_CHANGED = "SPONSOR_CUSTOMIZED_FORM_ARCHIVED_CHANGED"; +export const RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEMS = + "RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEMS"; +export const REQUEST_SPONSOR_CUSTOMIZED_FORM_ITEMS = + "REQUEST_SPONSOR_CUSTOMIZED_FORM_ITEMS"; +export const RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM = + "RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM"; +export const SPONSOR_CUSTOMIZED_FORM_ITEM_DELETED = + "SPONSOR_CUSTOMIZED_FORM_ITEM_DELETED"; +export const SPONSOR_CUSTOMIZED_FORM_ITEM_ARCHIVED = + "SPONSOR_CUSTOMIZED_FORM_ITEM_ARCHIVED"; +export const SPONSOR_CUSTOMIZED_FORM_ITEM_UNARCHIVED = + "SPONSOR_CUSTOMIZED_FORM_ITEM_UNARCHIVED"; +export const UPDATE_SPONSOR_FORM_MANAGED_ITEM = + "UPDATE_SPONSOR_FORM_MANAGED_ITEM"; +export const SPONSOR_FORM_MANAGED_ITEM_UPDATED = + "SPONSOR_FORM_MANAGED_ITEM_UPDATED"; +export const SPONSOR_FORM_MANAGED_ITEM_ADDED = + "SPONSOR_FORM_MANAGED_ITEM_ADDED"; +export const SPONSOR_FORM_MANAGED_ITEM_DELETED = + "SPONSOR_FORM_MANAGED_ITEM_DELETED"; +export const SPONSOR_CUSTOMIZED_FORM_ITEMS_ADDED = + "SPONSOR_CUSTOMIZED_FORM_ITEMS_ADDED"; +export const RESET_SPONSOR_FORM_MANAGED_ITEM = + "RESET_SPONSOR_FORM_MANAGED_ITEM"; + // ITEMS export const REQUEST_SPONSOR_FORM_ITEMS = "REQUEST_SPONSOR_FORM_ITEMS"; export const RECEIVE_SPONSOR_FORM_ITEMS = "RECEIVE_SPONSOR_FORM_ITEMS"; @@ -633,6 +658,61 @@ export const getSponsorCustomizedForm = }); }; +export const getSponsorCustomizedFormItems = + ( + formId, + term = "", + page = DEFAULT_CURRENT_PAGE, + perPage = DEFAULT_PER_PAGE, + order = "id", + orderDir = DEFAULT_ORDER_DIR, + hideArchived = false + ) => + async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const { currentSummit } = currentSummitState; + const { + entity: { id: sponsorId } + } = currentSponsorState; + const accessToken = await getAccessTokenSafely(); + const filter = []; + + dispatch(startLoading()); + + if (term) { + const escapedTerm = escapeFilterValue(term); + filter.push(`name=@${escapedTerm},code=@${escapedTerm}`); + } + + const params = { + page, + per_page: perPage, + access_token: accessToken + }; + + if (hideArchived) filter.push("is_archived==0"); + + if (filter.length > 0) { + params["filter[]"] = filter; + } + + // order + if (order != null && orderDir != null) { + const orderDirSign = orderDir === 1 ? "" : "-"; + params.order = `${orderDirSign}${order}`; + } + + return getRequest( + createAction(REQUEST_SPONSOR_CUSTOMIZED_FORM_ITEMS), + createAction(RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEMS), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items`, + authErrorHandler, + { term, order, orderDir, page, perPage, hideArchived } + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); + }; + export const saveSponsorCustomizedForm = (entity) => async (dispatch, getState) => { const { currentSummitState, currentSponsorState } = getState(); @@ -1134,9 +1214,6 @@ const normalizeItem = (entity) => { const normalizedEntity = { ...entity }; const { meta_fields, - early_bird_rate, - standard_rate, - onsite_rate, quantity_limit_per_show, quantity_limit_per_sponsor, default_quantity, @@ -1151,15 +1228,7 @@ const normalizeItem = (entity) => { normalizedEntity.images = images?.filter((img) => img.file_path); } - if (early_bird_rate === "" || typeof early_bird_rate === "undefined") - delete normalizedEntity.early_bird_rate; - else normalizedEntity.early_bird_rate = amountToCents(early_bird_rate); - if (standard_rate === "" || typeof standard_rate === "undefined") - delete normalizedEntity.standard_rate; - else normalizedEntity.standard_rate = amountToCents(standard_rate); - if (onsite_rate === "" || typeof onsite_rate === "undefined") - delete normalizedEntity.onsite_rate; - else normalizedEntity.onsite_rate = amountToCents(onsite_rate); + normalizeRates(entity, normalizedEntity); if (quantity_limit_per_show === "") delete normalizedEntity.quantity_limit_per_show; @@ -1205,3 +1274,246 @@ export const addInventoryItems = dispatch(stopLoading()); }); }; + +export const saveSponsorFormManagedItem = + (formId, entity) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + const { + entity: { id: sponsorId } + } = currentSponsorState; + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + const normalizedEntity = normalizeManagedItem(entity); + + if (entity.id) { + return putRequest( + createAction(UPDATE_SPONSOR_FORM_MANAGED_ITEM), + createAction(SPONSOR_FORM_MANAGED_ITEM_UPDATED), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items/${entity.id}`, + normalizedEntity, + snackbarErrorHandler, + entity + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate( + "edit_sponsor.forms_tab.form_manage_items.item_updated" + ) + }) + ); + }) + .finally(() => { + dispatch(stopLoading()); + }); + } + + const successMessage = { + title: T.translate("general.done"), + html: T.translate( + "edit_sponsor.forms_tab.form_manage_items.item_created" + ), + type: "success" + }; + + return postRequest( + createAction(UPDATE_SPONSOR_FORM_MANAGED_ITEM), + createAction(SPONSOR_FORM_MANAGED_ITEM_ADDED), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items`, + normalizedEntity, + snackbarErrorHandler, + entity + )(params)(dispatch) + .then(() => { + dispatch(snackbarSuccessHandler(successMessage)); + }) + .finally(() => { + dispatch(stopLoading()); + }); + }; + +export const resetSponsorFormManagedItem = () => (dispatch) => { + dispatch(createAction(RESET_SPONSOR_FORM_MANAGED_ITEM)({})); +}; + +const normalizeManagedItem = (entity) => { + const normalizedEntity = { ...entity }; + normalizedEntity.meta_fields = normalizedEntity.meta_fields?.filter( + (mf) => mf.name + ); + normalizedEntity.images = normalizedEntity.images?.filter( + (img) => img.file_path + ); + + normalizeRates(entity, normalizedEntity); + + return normalizedEntity; +}; + +const normalizeRates = (entity, normalizedEntity) => { + const { early_bird_rate, standard_rate, onsite_rate } = entity; + + if (early_bird_rate === "" || early_bird_rate === undefined) + delete normalizedEntity.early_bird_rate; + else normalizedEntity.early_bird_rate = amountToCents(early_bird_rate); + + if (standard_rate === "" || standard_rate === undefined) + delete normalizedEntity.standard_rate; + else normalizedEntity.standard_rate = amountToCents(standard_rate); + + if (onsite_rate === "" || onsite_rate === undefined) + delete normalizedEntity.onsite_rate; + else normalizedEntity.onsite_rate = amountToCents(onsite_rate); +}; + +export const deleteSponsorFormManagedItem = + (formId, itemId) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + const { + entity: { id: sponsorId } + } = currentSponsorState; + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return deleteRequest( + null, + createAction(SPONSOR_FORM_MANAGED_ITEM_DELETED)({ itemId }), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items/${itemId}`, + null, + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate("sponsor_forms.form_delete_success") + }) + ); + }) + .finally(() => { + dispatch(stopLoading()); + }); + }; + +export const getSponsorFormManagedItem = + (formId, itemId) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + const { + entity: { id: sponsorId } + } = currentSponsorState; + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return getRequest( + null, + createAction(RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items/${itemId}`, + authErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); + }; + +export const addSponsorManagedFormItems = + (formId, itemIds) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + const { + entity: { id: sponsorId } + } = currentSponsorState; + + dispatch(startLoading()); + + const params = { + access_token: accessToken + }; + + return postRequest( + null, + createAction(SPONSOR_CUSTOMIZED_FORM_ITEMS_ADDED), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items/clone`, + { inventory_item_ids: itemIds }, + snackbarErrorHandler + )(params)(dispatch) + .then(() => { + dispatch(getSponsorCustomizedFormItems(formId)); + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate( + "sponsor_form_item_list.add_from_inventory.items_added" + ) + }) + ); + }) + .finally(() => { + dispatch(stopLoading()); + }); + }; + +export const archiveSponsorCustomizedFormItem = + (formId, itemId) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + const { + entity: { id: sponsorId } + } = currentSponsorState; + const params = { access_token: accessToken }; + + dispatch(startLoading()); + + return putRequest( + null, + createAction(SPONSOR_CUSTOMIZED_FORM_ITEM_ARCHIVED), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items/${itemId}/archive`, + null, + snackbarErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); + }; + +export const unarchiveSponsorCustomizedFormItem = + (formId, itemId) => async (dispatch, getState) => { + const { currentSummitState, currentSponsorState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + const { + entity: { id: sponsorId } + } = currentSponsorState; + const params = { access_token: accessToken }; + + dispatch(startLoading()); + + return deleteRequest( + null, + createAction(SPONSOR_CUSTOMIZED_FORM_ITEM_UNARCHIVED)({ itemId }), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/sponsor-forms/${formId}/items/${itemId}/archive`, + null, + snackbarErrorHandler + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); + }; diff --git a/src/i18n/en.json b/src/i18n/en.json index 7384e4d74..8c16c2989 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2432,6 +2432,27 @@ "error": "There was a problem creating the forms, please try again.", "archived": "Form successfully archived.", "unarchived": "Form successfully unarchived." + }, + "form_manage_items": { + "add_item": "Add Item", + "add_item_inventory": "Add Item from Inventory", + "alert_info": "You can add or archive items from the list. To edit an item click on the item's Edit button. You can also change only a rate by clicking on it.", + "hide_archived": "Hide archived items", + "select_items": "Select items", + "code": "Code", + "name": "Name", + "early_bird_rate": "Early Bird Rate", + "standard_rate": "Standard Rate", + "onsite_rate": "On site rate", + "default_quantity": "Default Quantity", + "add_selected": "Add Selected Items", + "item_updated": "Form item updated successfully", + "item_created": "Form item created successfully", + "sort_asc_label": "A-Z", + "sort_desc_label": "Z-A", + "placeholder": { + "search": "Search..." + } } }, "placeholders": { @@ -2528,7 +2549,7 @@ }, "sponsor_form_item_list": { "form_items": "Form Items", - "alert_info": "You can add or archive items from the list. To edit an item click on the item's Edit botton. You can also change only a rate by clicking on it.", + "alert_info": "You can add or archive items from the list. To edit an item click on the item's Edit button. You can also change only a rate by clicking on it.", "code": "Code", "name": "Name", "early_bird_rate": "Early bird rate", @@ -2570,7 +2591,7 @@ "code": "Code", "name": "Name", "early_bird_rate": "Early bird rate", - "standard_rate": "Standad rate", + "standard_rate": "Standard rate", "onsite_rate": "On site rate", "save": "Add selected items", "items_added": "Items added successfully." @@ -3617,6 +3638,8 @@ "inventory_item": "Item", "inventory_item_created": "Inventory Item created successfully.", "inventory_item_saved": "Inventory Item saved successfully.", + "new_item": "New Item", + "edit_item": "Edit Item", "code": "Code", "name": "Name", "default_quantity": "Default Quantity", diff --git a/src/layouts/sponsor-id-layout.js b/src/layouts/sponsor-id-layout.js index ae3eee227..4b117c411 100644 --- a/src/layouts/sponsor-id-layout.js +++ b/src/layouts/sponsor-id-layout.js @@ -141,7 +141,13 @@ class SponsorIdLayout extends React.Component { )} /> - + + + + diff --git a/src/pages/sponsors/edit-sponsor-page.js b/src/pages/sponsors/edit-sponsor-page.js index 5f3b6e214..470592250 100644 --- a/src/pages/sponsors/edit-sponsor-page.js +++ b/src/pages/sponsors/edit-sponsor-page.js @@ -42,6 +42,8 @@ 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 SponsorFormsManageItems from "./sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items"; +import { SPONSOR_TABS } from "../../utils/constants"; const CustomTabPanel = (props) => { const { children, value, index, ...other } = props; @@ -68,6 +70,9 @@ const EditSponsorPage = (props) => { const { entity, member, + history, + location, + match, currentSummit, resetSponsorForm, getSponsorAdvertisements, @@ -91,10 +96,16 @@ const EditSponsorPage = (props) => { getExtraQuestionMeta } = props; - const [selectedTab, setSelectedTab] = useState(0); + const [selectedTab, setSelectedTab] = useState( + location.pathname.includes("/sponsor-forms/") && + location.pathname.includes("/items") + ? SPONSOR_TABS.FORMS + : 0 + ); const handleTabChange = (event, newValue) => { setSelectedTab(newValue); + history.push(`/app/summits/${currentSummit.id}/sponsors/${entity.id}`); }; useEffect(() => { @@ -115,19 +126,35 @@ const EditSponsorPage = (props) => { }; const tabs = [ - { label: T.translate("edit_sponsor.tab.general"), value: 0 }, - { label: T.translate("edit_sponsor.tab.users"), value: 1 }, - { label: T.translate("edit_sponsor.tab.pages"), value: 2 }, - { label: T.translate("edit_sponsor.tab.media_uploads"), value: 3 }, - { label: T.translate("edit_sponsor.tab.forms"), value: 4 }, - { label: T.translate("edit_sponsor.tab.cart"), value: 5 }, - { label: T.translate("edit_sponsor.tab.purchases"), value: 6 }, - { label: T.translate("edit_sponsor.tab.badge_scans"), value: 7 } + { + label: T.translate("edit_sponsor.tab.general"), + value: SPONSOR_TABS.GENERAL + }, + { label: T.translate("edit_sponsor.tab.users"), value: SPONSOR_TABS.USERS }, + { label: T.translate("edit_sponsor.tab.pages"), value: SPONSOR_TABS.PAGES }, + { + label: T.translate("edit_sponsor.tab.media_uploads"), + value: SPONSOR_TABS.MEDIA_UPLOADS + }, + { label: T.translate("edit_sponsor.tab.forms"), value: SPONSOR_TABS.FORMS }, + { label: T.translate("edit_sponsor.tab.cart"), value: SPONSOR_TABS.CART }, + { + label: T.translate("edit_sponsor.tab.purchases"), + value: SPONSOR_TABS.PURCHASES + }, + { + label: T.translate("edit_sponsor.tab.badge_scans"), + value: SPONSOR_TABS.BADGE_SCANS + } ]; + const sponsorFormItemRoute = + location.pathname.includes("/sponsor-forms/") && + location.pathname.includes("/items"); + return ( - + {entity.company?.name} @@ -144,6 +171,7 @@ const EditSponsorPage = (props) => { key={t.value} label={t.label} value={t.value} + onClick={() => handleTabChange(null, t.value)} sx={{ fontSize: "1.4rem", lineHeight: "1.8rem", @@ -183,7 +211,15 @@ const EditSponsorPage = (props) => { - + {sponsorFormItemRoute ? ( + + ) : ( + + )} diff --git a/src/pages/sponsors/sponsor-form-item-list-page/index.js b/src/pages/sponsors/sponsor-form-item-list-page/index.js index 94a3bd0a7..e6cddeb62 100644 --- a/src/pages/sponsors/sponsor-form-item-list-page/index.js +++ b/src/pages/sponsors/sponsor-form-item-list-page/index.js @@ -15,7 +15,6 @@ import React, { useEffect, useState } from "react"; import { Breadcrumb } from "react-breadcrumbs"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; -import * as yup from "yup"; import { Alert, Box, @@ -29,7 +28,6 @@ import AddIcon from "@mui/icons-material/Add"; import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import ImageIcon from "@mui/icons-material/Image"; -import { parsePrice } from "openstack-uicore-foundation/lib/utils/money"; import { deleteSponsorFormItem, getSponsorFormItem, @@ -42,6 +40,7 @@ import ItemPopup from "./components/item-popup"; import InventoryPopup from "./components/inventory-popup"; import MuiTableEditable from "../../../components/mui/editable-table/mui-table-editable"; import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; +import { rateCellValidation } from "../../../utils/yup"; const SponsorFormItemListPage = ({ match, @@ -92,7 +91,8 @@ const SponsorFormItemListPage = ({ }; const handleCellEdit = (rowId, column, value) => { - const tmpEntity = { id: rowId, [column]: parsePrice(value) }; + const valueWithNoSign = String(value).replace(/^[^\d.-]+/, ""); + const tmpEntity = { id: rowId, [column]: valueWithNoSign }; updateSponsorFormItem(formId, tmpEntity); }; @@ -113,37 +113,6 @@ const SponsorFormItemListPage = ({ setOpenPopup("inventory"); }; - const rateCellValidation = () => - yup - .number() - // allow $ at the start - .transform((value, originalValue) => { - if (typeof originalValue === "string") { - const cleaned = originalValue.replace(/^\$/, ""); - return cleaned === "" ? undefined : parseFloat(cleaned); - } - return value; - }) - // check if there's letters or characters - .test({ - name: "valid-format", - message: T.translate("validation.number"), - test: (value, { originalValue }) => { - if ( - originalValue === undefined || - originalValue === null || - originalValue === "" - ) - return true; - return /^\$?-?\d+(\.\d+)?$/.test(originalValue); - } - }) - .min(0, T.translate("validation.number_positive")) - .test("max-decimals", T.translate("validation.two_decimals"), (value) => { - if (value === undefined || value === null) return true; - return /^\d+(\.\d{1,2})?$/.test(value.toString()); - }); - const columns = [ { columnKey: "code", diff --git a/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js new file mode 100644 index 000000000..b7299ca8a --- /dev/null +++ b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js @@ -0,0 +1,288 @@ +import React, { useEffect, useState } from "react"; +import T from "i18n-react/dist/i18n-react"; +import PropTypes from "prop-types"; +import { connect } from "react-redux"; +import { + Box, + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + Divider, + FormControlLabel, + Grid2, + IconButton, + Tooltip, + Typography +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import ImageIcon from "@mui/icons-material/Image"; +import SwapVertIcon from "@mui/icons-material/SwapVert"; +import { currencyAmountFromCents } from "openstack-uicore-foundation/lib/utils/money"; +import SearchInput from "../../../../../components/mui/search-input"; +import { + DEFAULT_CURRENT_PAGE, + DEFAULT_PER_PAGE +} from "../../../../../utils/constants"; + +import { getInventoryItems } from "../../../../../actions/inventory-item-actions"; +import MuiTable from "../../../../../components/mui/table/mui-table"; +import MenuButton from "../../../../../components/mui/menu-button"; + +const SponsorFormItemFromInventoryPopup = ({ + open, + inventoryItems, + term, + order, + perPage, + orderDir, + currentPage, + totalInventoryItems, + onSave, + onClose, + getInventoryItems +}) => { + const [selectedRows, setSelectedRows] = useState([]); + + useEffect(() => { + getInventoryItems("", 1, DEFAULT_PER_PAGE, "id", 1); + }, []); + + const handleSort = (key, dir) => { + getInventoryItems(term, 1, DEFAULT_PER_PAGE, key, dir); + }; + + const handlePageChange = (page) => { + getInventoryItems(term, page, perPage, order, orderDir); + }; + + const handlePerPageChange = (newPerPage) => { + getInventoryItems(term, DEFAULT_CURRENT_PAGE, newPerPage, order, orderDir); + }; + + const handleClose = () => { + setSelectedRows([]); + onClose(); + }; + + const handleOnCheck = (rowId, checked) => { + if (checked) { + setSelectedRows([...selectedRows, rowId]); + } else { + setSelectedRows(selectedRows.filter((r) => r !== rowId)); + } + }; + + const handleOnSearch = (searchTerm) => { + getInventoryItems(searchTerm, 1, DEFAULT_PER_PAGE, "id", 1); + }; + + const handleOnSave = () => { + onSave(selectedRows); + }; + + const columns = [ + { + columnKey: "select", + header: "", + width: 30, + align: "center", + render: (row) => ( + handleOnCheck(row.id, ev.target.checked)} + /> + } + /> + ) + }, + { + columnKey: "code", + header: T.translate("edit_sponsor.forms_tab.form_manage_items.code"), + sortable: false + }, + { + columnKey: "name", + header: T.translate("edit_sponsor.forms_tab.form_manage_items.name"), + sortable: false + }, + { + columnKey: "early_bird_rate", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.early_bird_rate" + ), + sortable: false, + render: (row) => currencyAmountFromCents(row.early_bird_rate) + }, + { + columnKey: "standard_rate", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.standard_rate" + ), + sortable: false, + render: (row) => currencyAmountFromCents(row.standard_rate) + }, + { + columnKey: "onsite_rate", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.onsite_rate" + ), + sortable: false, + render: (row) => currencyAmountFromCents(row.onsite_rate) + }, + { + columnKey: "default_quantity", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.default_quantity" + ), + sortable: false + }, + { + columnKey: "images", + header: "", + width: 40, + align: "center", + render: (row) => + row.images?.length > 0 ? ( + + + + window.open( + row.images[0].file_url, + "_blank", + "noopener,noreferrer" + ) + } + /> + + + ) : null + } + ]; + + const tableOptions = { + sortCol: order, + sortDir: orderDir + }; + + return ( + + + + {T.translate( + "edit_sponsor.forms_tab.form_manage_items.add_item_inventory" + )} + + handleClose()}> + + + + + + + + {selectedRows.length} items selected + + + + handleSort("name", 1) + }, + { + label: T.translate( + "edit_sponsor.forms_tab.form_manage_items.sort_desc_label" + ), + onClick: () => handleSort("name", 0) + } + ]} + > + sort by + + + + + + + + + {inventoryItems.length > 0 && ( + + + + )} + + + + + + + ); +}; + +SponsorFormItemFromInventoryPopup.propTypes = { + onClose: PropTypes.func.isRequired, + onSave: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + inventoryItems: PropTypes.array.isRequired, + term: PropTypes.string, + order: PropTypes.string, + perPage: PropTypes.number, + orderDir: PropTypes.string, + currentPage: PropTypes.number, + totalInventoryItems: PropTypes.number, + getInventoryItems: PropTypes.func.isRequired +}; + +const mapStateToProps = ({ currentInventoryItemListState }) => ({ + ...currentInventoryItemListState +}); + +export default connect(mapStateToProps, { + getInventoryItems +})(SponsorFormItemFromInventoryPopup); diff --git a/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js new file mode 100644 index 000000000..d4a292af0 --- /dev/null +++ b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js @@ -0,0 +1,403 @@ +/** + * 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, + Checkbox, + FormControlLabel, + FormGroup, + Grid2, + IconButton, + Tooltip +} from "@mui/material"; +import AddIcon from "@mui/icons-material/Add"; +import ImageIcon from "@mui/icons-material/Image"; +import { + addSponsorManagedFormItems, + archiveSponsorCustomizedFormItem, + getSponsorCustomizedFormItems, + saveSponsorFormManagedItem, + deleteSponsorFormManagedItem, + resetSponsorFormManagedItem, + unarchiveSponsorCustomizedFormItem, + getSponsorFormManagedItem +} from "../../../../../actions/sponsor-forms-actions"; +import CustomAlert from "../../../../../components/mui/custom-alert"; +import SearchInput from "../../../../../components/mui/search-input"; +import MuiTableEditable from "../../../../../components/mui/editable-table/mui-table-editable"; +import SponsorInventoryDialog from "../../../../sponsors_inventory/popup/sponsor-inventory-popup"; +import SponsorFormItemFromInventoryPopup from "./sponsor-form-item-from-inventory"; +import { DEFAULT_CURRENT_PAGE } from "../../../../../utils/constants"; +import { rateCellValidation } from "../../../../../utils/yup"; + +const SponsorFormsManageItems = ({ + term, + match, + hideArchived, + items, + order, + orderDir, + perPage, + currentPage, + totalCount, + getSponsorCustomizedFormItems, + currentInventoryItem, + resetSponsorFormManagedItem, + addSponsorManagedFormItems, + saveSponsorFormManagedItem, + deleteSponsorFormManagedItem, + archiveSponsorCustomizedFormItem, + unarchiveSponsorCustomizedFormItem, + getSponsorFormManagedItem +}) => { + const [openPopup, setOpenPopup] = useState(null); + + const handleClose = () => { + setOpenPopup(null); + }; + + const formId = match.params.form_id; + + useEffect(() => { + getSponsorCustomizedFormItems(formId); + }, []); + + const handlePageChange = (page) => { + getSponsorCustomizedFormItems( + formId, + term, + page, + perPage, + order, + orderDir, + hideArchived + ); + }; + + const handlePerPageChange = (newPerPage) => { + getSponsorCustomizedFormItems( + formId, + term, + DEFAULT_CURRENT_PAGE, + newPerPage, + order, + orderDir, + hideArchived + ); + }; + + const handleSort = (key, dir) => { + getSponsorCustomizedFormItems( + formId, + term, + DEFAULT_CURRENT_PAGE, + perPage, + key, + dir, + hideArchived + ); + }; + + const handleSearch = (searchTerm) => { + getSponsorCustomizedFormItems( + formId, + searchTerm, + DEFAULT_CURRENT_PAGE, + perPage, + order, + orderDir, + hideArchived + ); + }; + + const handleItemSave = (item) => { + saveSponsorFormManagedItem(formId, item).then(() => { + getSponsorCustomizedFormItems( + formId, + term, + DEFAULT_CURRENT_PAGE, + perPage, + order, + orderDir, + hideArchived + ); + resetSponsorFormManagedItem(); + setOpenPopup(null); + }); + }; + + const handleOpenItemPopup = () => { + resetSponsorFormManagedItem(); + setOpenPopup("add_item"); + }; + + const handleArchiveItem = (item) => + item.is_archived + ? unarchiveSponsorCustomizedFormItem(formId, item.id) + : archiveSponsorCustomizedFormItem(formId, item.id); + + const handleHideArchivedItems = (ev) => { + getSponsorCustomizedFormItems( + formId, + term, + DEFAULT_CURRENT_PAGE, + perPage, + order, + orderDir, + ev.target.checked + ); + }; + + const handleAddFromInventory = (itemsId) => { + addSponsorManagedFormItems(formId, itemsId).then(() => handleClose()); + }; + + const handleCellEdit = (rowId, column, value) => { + const valueWithNoSign = String(value).replace(/^[^\d.-]+/, ""); + const tmpEntity = { + id: rowId, + [column]: valueWithNoSign + }; + saveSponsorFormManagedItem(formId, tmpEntity); + }; + + const handleRowEdit = (row) => { + getSponsorFormManagedItem(formId, row.id).then(() => + setOpenPopup("add_item") + ); + }; + + const handleRowDelete = (rowId) => { + deleteSponsorFormManagedItem(formId, rowId).then(() => + getSponsorCustomizedFormItems( + formId, + term, + DEFAULT_CURRENT_PAGE, + perPage, + order, + orderDir, + hideArchived + ) + ); + }; + + const sponsorItemColumns = [ + { + columnKey: "code", + header: T.translate("edit_sponsor.forms_tab.form_manage_items.code"), + sortable: false + }, + { + columnKey: "name", + header: T.translate("edit_sponsor.forms_tab.form_manage_items.name"), + sortable: false + }, + { + columnKey: "early_bird_rate", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.early_bird_rate" + ), + sortable: false, + editable: true, + validation: { + schema: rateCellValidation() + } + }, + { + columnKey: "standard_rate", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.standard_rate" + ), + sortable: false, + editable: true, + validation: { + schema: rateCellValidation() + } + }, + { + columnKey: "onsite_rate", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.onsite_rate" + ), + sortable: false, + editable: true, + validation: { + schema: rateCellValidation() + } + }, + { + columnKey: "default_quantity", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.default_quantity" + ), + sortable: false + }, + { + columnKey: "images", + header: "", + width: 40, + align: "center", + render: (row) => + row.images?.length > 0 ? ( + + + + window.open( + row.images[0].file_url, + "_blank", + "noopener,noreferrer" + ) + } + /> + + + ) : null + } + ]; + + return ( + + + + + {totalCount} items + + + + + } + label={T.translate( + "edit_sponsor.forms_tab.form_manage_items.hide_archived" + )} + /> + + + + + + + + + + + + + +
+ +
+ + {/* ADD ITEM */} + {openPopup === "add_item" && ( + + )} + + +
+ ); +}; + +const mapStateToProps = ({ sponsorCustomizedFormItemsListState }) => ({ + ...sponsorCustomizedFormItemsListState, + currentInventoryItem: sponsorCustomizedFormItemsListState.currentItem +}); + +export default connect(mapStateToProps, { + getSponsorCustomizedFormItems, + resetSponsorFormManagedItem, + addSponsorManagedFormItems, + saveSponsorFormManagedItem, + deleteSponsorFormManagedItem, + getSponsorFormManagedItem, + archiveSponsorCustomizedFormItem, + unarchiveSponsorCustomizedFormItem +})(SponsorFormsManageItems); diff --git a/src/pages/sponsors/sponsor-forms-tab/index.js b/src/pages/sponsors/sponsor-forms-tab/index.js index 06920a737..b4b679cf8 100644 --- a/src/pages/sponsors/sponsor-forms-tab/index.js +++ b/src/pages/sponsors/sponsor-forms-tab/index.js @@ -41,6 +41,7 @@ import { DEFAULT_CURRENT_PAGE } from "../../../utils/constants"; const SponsorFormsTab = ({ term, + history, hideArchived, managedForms, customizedForms, @@ -111,7 +112,9 @@ const SponsorFormsTab = ({ : archiveSponsorCustomizedForm(item.id); const handleManageItems = (item) => { - console.log("MANAGE ITEMS : ", item); + history.push( + `/app/summits/${summitId}/sponsors/${sponsor.id}/sponsor-forms/${item.id}/items` + ); }; const handleCustomizedEdit = (item) => { diff --git a/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js b/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js index c079e005a..ffa3bcf2d 100644 --- a/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js +++ b/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js @@ -30,7 +30,7 @@ import { import showConfirmDialog from "../../../components/mui/showConfirmDialog"; import MetaFieldValues from "./meta-field-values"; import MuiFormikTextField from "../../../components/mui/formik-inputs/mui-formik-textfield"; -import MuiFormikPriceField from "../../../components/mui/formik-inputs/mui-formik-pricefield.js"; +import MuiFormikPriceField from "../../../components/mui/formik-inputs/mui-formik-pricefield"; import useScrollToError from "../../../hooks/useScrollToError"; import MuiFormikSelect from "../../../components/mui/formik-inputs/mui-formik-select"; import MuiFormikCheckbox from "../../../components/mui/formik-inputs/mui-formik-checkbox"; @@ -69,7 +69,7 @@ const SponsorItemDialog = ({ code: yup.string().required(T.translate("validation.required")), name: yup.string().required(T.translate("validation.required")), description: yup.string().required(T.translate("validation.required")), - images: yup.array().min(1, T.translate("validation.required")), + images: yup.array(), early_bird_rate: decimalValidation(), standard_rate: decimalValidation(), onsite_rate: decimalValidation(), @@ -182,7 +182,7 @@ const SponsorItemDialog = ({ } }; - if (fieldType.id) { + if (fieldType.id && onMetaFieldTypeDeleted) { onMetaFieldTypeDeleted(initialEntity.id, fieldType.id) .then(() => removeOrResetField()) .catch((err) => console.log("Error at delete field from API", err)); @@ -243,7 +243,9 @@ const SponsorItemDialog = ({ disableRestoreFocus > - Edit Item + {initialEntity.id + ? T.translate("edit_inventory_item.edit_item") + : T.translate("edit_inventory_item.new_item")} @@ -582,7 +584,7 @@ const SponsorItemDialog = ({ > - {T.translate("edit_inventory_item.images")} * + {T.translate("edit_inventory_item.images")} {formik.touched.images && formik.errors.images && ( {formik.errors.images} diff --git a/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js b/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js new file mode 100644 index 000000000..a1f436f84 --- /dev/null +++ b/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js @@ -0,0 +1,195 @@ +/** + * 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, + currencyAmountFromCents +} from "openstack-uicore-foundation/lib/utils/money"; +import { + RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEMS, + REQUEST_SPONSOR_CUSTOMIZED_FORM_ITEMS, + RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM, + SPONSOR_CUSTOMIZED_FORM_ITEM_ARCHIVED, + SPONSOR_CUSTOMIZED_FORM_ITEM_DELETED, + SPONSOR_CUSTOMIZED_FORM_ITEM_UNARCHIVED, + SPONSOR_FORM_MANAGED_ITEM_UPDATED, + SPONSOR_CUSTOMIZED_FORM_ITEMS_ADDED, + RESET_SPONSOR_FORM_MANAGED_ITEM +} from "../../actions/sponsor-forms-actions"; +import { SET_CURRENT_SUMMIT } from "../../actions/summit-actions"; + +const DEFAULT_ITEM_ENTITY = { + code: "", + name: "", + description: "", + early_bird_rate: 0, + standard_rate: 0, + onsite_rate: 0, + quantity_limit_per_show: 0, + quantity_limit_per_sponsor: 0, + default_quantity: 0, + images: [], + meta_fields: [ + { + name: "", + type: "Text", + is_required: false, + values: [] + } + ] +}; + +const DEFAULT_STATE = { + items: [], + hideArchived: false, + term: "", + order: "name", + orderDir: 1, + currentPage: 1, + lastPage: 1, + perPage: 10, + totalCount: 0, + currentItem: DEFAULT_ITEM_ENTITY +}; + +const sponsorCustomizedFormItemsListReducer = ( + state = DEFAULT_STATE, + action +) => { + const { type, payload } = action; + + switch (type) { + case SET_CURRENT_SUMMIT: + case LOGOUT_USER: { + return DEFAULT_STATE; + } + case REQUEST_SPONSOR_CUSTOMIZED_FORM_ITEMS: { + const { term, order, orderDir, page, perPage, hideArchived } = payload; + + return { + ...state, + term, + order, + orderDir, + items: [], + currentPage: page, + perPage, + hideArchived + }; + } + case RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEMS: { + const { + current_page: currentPage, + total, + last_page: lastPage + } = payload.response; + + const items = payload.response.data.map((a) => ({ + id: a.id, + code: a.code, + name: a.name, + early_bird_rate: currencyAmountFromCents(a.early_bird_rate), + standard_rate: currencyAmountFromCents(a.standard_rate), + onsite_rate: currencyAmountFromCents(a.onsite_rate), + default_quantity: a.default_quantity, + is_archived: a.is_archived, + images: a.images + })); + + return { + ...state, + items, + currentPage, + totalCount: total, + lastPage + }; + } + case RECEIVE_SPONSOR_CUSTOMIZED_FORM_ITEM: { + const item = payload.response; + + const currentItem = { + ...item, + early_bird_rate: amountFromCents(item.early_bird_rate), + standard_rate: amountFromCents(item.standard_rate), + onsite_rate: amountFromCents(item.onsite_rate), + meta_fields: + item.meta_fields.length > 0 + ? item.meta_fields + : [ + { + name: "", + type: "Text", + is_required: false, + values: [] + } + ] + }; + return { ...state, currentItem }; + } + case SPONSOR_CUSTOMIZED_FORM_ITEM_DELETED: { + const { itemId } = payload; + const items = state.items.filter((it) => it.id !== itemId); + + return { ...state, items }; + } + case SPONSOR_CUSTOMIZED_FORM_ITEM_ARCHIVED: { + const { id: itemId } = payload.response; + + const items = state.items.map((item) => + item.id === itemId ? { ...item, is_archived: true } : item + ); + + return { ...state, items }; + } + case SPONSOR_CUSTOMIZED_FORM_ITEM_UNARCHIVED: { + const { itemId } = payload; + + const items = state.items.map((item) => + item.id === itemId ? { ...item, is_archived: false } : item + ); + + return { ...state, items }; + } + case SPONSOR_FORM_MANAGED_ITEM_UPDATED: { + const updatedItem = payload.response; + const items = state.items.map((item) => + item.id === updatedItem.id + ? { + id: updatedItem.id, + code: updatedItem.code, + name: updatedItem.name, + early_bird_rate: currencyAmountFromCents( + updatedItem.early_bird_rate + ), + standard_rate: currencyAmountFromCents(updatedItem.standard_rate), + onsite_rate: currencyAmountFromCents(updatedItem.onsite_rate), + default_quantity: updatedItem.default_quantity, + is_archived: updatedItem.is_archived, + images: updatedItem.images + } + : item + ); + return { ...state, items }; + } + case SPONSOR_CUSTOMIZED_FORM_ITEMS_ADDED: { + return { ...state }; + } + case RESET_SPONSOR_FORM_MANAGED_ITEM: + return { ...state, currentItem: DEFAULT_ITEM_ENTITY }; + default: + return state; + } +}; + +export default sponsorCustomizedFormItemsListReducer; diff --git a/src/store.js b/src/store.js index 64762f8ac..ef7e2e218 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 sponsorCustomizedFormItemsListReducer from "./reducers/sponsors/sponsor-customized-form-items-list-reducer.js"; // default: localStorage if web, AsyncStorage if react-native @@ -250,6 +251,7 @@ const reducers = persistCombineReducers(config, { sponsorUsersListState: sponsorUsersListReducer, sponsorPageFormsListState: sponsorPageFormsListReducer, sponsorCustomizedFormState: sponsorCustomizedFormReducer, + sponsorCustomizedFormItemsListState: sponsorCustomizedFormItemsListReducer, currentSponsorPromocodeListState: sponsorPromocodeListReducer, currentSponsorExtraQuestionState: sponsorExtraQuestionReducer, currentSponsorAdvertisementState: sponsorAdvertisementReducer, diff --git a/src/utils/constants.js b/src/utils/constants.js index a6d2d7139..360988a92 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -223,3 +223,14 @@ export const ROOM_OCCUPANCY_OPTIONS = [ ]; export const BADGE_QR_MINIMUM_EXPECTED_FIELDS = 3; + +export const SPONSOR_TABS = { + GENERAL: 0, + USERS: 1, + PAGES: 2, + MEDIA_UPLOADS: 3, + FORMS: 4, + CART: 5, + PURCHASES: 6, + BADGE_SCANS: 7 +}; diff --git a/src/utils/yup.js b/src/utils/yup.js index 0b3999935..86c770b39 100644 --- a/src/utils/yup.js +++ b/src/utils/yup.js @@ -47,6 +47,33 @@ export const decimalValidation = () => .typeError(T.translate("validation.number")) .positive(T.translate("validation.number_positive")) .required(T.translate("validation.required")) + +export const rateCellValidation = () => + yup + .number() + // allow $ at the start + .transform((value, originalValue) => { + if (typeof originalValue === "string") { + const cleaned = originalValue.replace(/^\$/, ""); + return cleaned === "" ? undefined : parseFloat(cleaned); + } + return value; + }) + // check if there's letters or characters + .test({ + name: "valid-format", + message: T.translate("validation.number"), + test: (value, { originalValue }) => { + if ( + originalValue === undefined || + originalValue === null || + originalValue === "" + ) + return true; + return /^\$?-?\d+(\.\d+)?$/.test(originalValue); + } + }) + .positive(T.translate("validation.number_positive")) .test("max-decimals", T.translate("validation.two_decimals"), (value) => { if (value === undefined || value === null) return true; return /^\d+(\.\d{1,2})?$/.test(value.toString());