diff --git a/src/actions/sponsor-forms-actions.js b/src/actions/sponsor-forms-actions.js index 9253b92fc..7dc067bf8 100644 --- a/src/actions/sponsor-forms-actions.js +++ b/src/actions/sponsor-forms-actions.js @@ -31,6 +31,7 @@ import { DEFAULT_PER_PAGE } from "../utils/constants"; import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; +import { amountToCents } from "../utils/currency"; export const REQUEST_SPONSOR_FORMS = "REQUEST_SPONSOR_FORMS"; export const RECEIVE_SPONSOR_FORMS = "RECEIVE_SPONSOR_FORMS"; @@ -64,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"; @@ -632,6 +658,60 @@ 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 + )(params)(dispatch).then(() => { + dispatch(stopLoading()); + }); + }; + export const saveSponsorCustomizedForm = (entity) => async (dispatch, getState) => { const { currentSummitState, currentSponsorState } = getState(); @@ -1208,3 +1288,241 @@ 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(stopLoading()); + dispatch( + snackbarSuccessHandler({ + title: T.translate("general.success"), + html: T.translate( + "edit_sponsor.forms_tab.form_manage_items.item_updated" + ) + }) + ); + }); + } + + 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)); + }); + }; + +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 + ); + + if (entity.early_bird_rate === "" || entity.early_bird_rate === undefined) + delete normalizedEntity.early_bird_rate; + else + normalizedEntity.early_bird_rate = amountToCents( + normalizedEntity.early_bird_rate + ); + + if (entity.standard_rate === "" || entity.standard_rate === undefined) + delete normalizedEntity.standard_rate; + else + normalizedEntity.standard_rate = amountToCents( + normalizedEntity.standard_rate + ); + + if (entity.onsite_rate === "" || entity.onsite_rate === undefined) + delete normalizedEntity.onsite_rate; + else + normalizedEntity.onsite_rate = amountToCents(normalizedEntity.onsite_rate); + + return normalizedEntity; +}; + +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" + ) + }) + ); + }) + .catch(snackbarErrorHandler) // need to catch promise reject + .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/components/mui/editable-table/mui-table-editable.js b/src/components/mui/editable-table/mui-table-editable.js index f21337f4c..9250aae74 100644 --- a/src/components/mui/editable-table/mui-table-editable.js +++ b/src/components/mui/editable-table/mui-table-editable.js @@ -23,19 +23,51 @@ import { } from "../../../utils/constants"; import showConfirmDialog from "../showConfirmDialog"; +const validateValue = (value, validation) => { + if (!validation) return { isValid: true }; + + // validate with yup schema + if ( + validation.schema && + typeof validation.schema.validateSync === "function" + ) { + try { + validation.schema.validateSync(value); + return { isValid: true, message: null }; + } catch (err) { + return { isValid: false, message: err.message }; + } + } + + return { isValid: true }; +}; + // Updated component to handle editable cells with hover edit icon -const EditableCell = ({ value, isEditing, onBlur }) => { +const EditableCell = ({ value, isEditing, onBlur, validation }) => { const [inputValue, setInputValue] = React.useState(value); const [isHovering, setIsHovering] = React.useState(false); + const [error, setError] = React.useState(null); React.useEffect(() => { setInputValue(value); + setError(null); }, [value]); + const handleValidationAndSave = (newValue) => { + const { isValid, message } = validateValue(newValue, validation); + + if (isValid) { + setError(null); + onBlur(newValue, true); + } else { + setError(message); + } + }; + const handleKeyDown = (e) => { if (e.key === "Enter") { e.preventDefault(); - onBlur(inputValue); + handleValidationAndSave(inputValue); } }; @@ -44,14 +76,19 @@ const EditableCell = ({ value, isEditing, onBlur }) => { setInputValue(e.target.value)} + onChange={(e) => { + setInputValue(e.target.value); + if (error) setError(null); + }} onBlur={() => { - onBlur(inputValue); + handleValidationAndSave(inputValue); }} onKeyDown={handleKeyDown} size="small" fullWidth variant="standard" + error={!!error} + helperText={error} /> ); } @@ -150,8 +187,8 @@ const MuiTableEditable = ({ }; // Handler for saving changes when editing is complete - const handleCellBlur = (rowId, columnKey, newValue) => { - if (onCellChange) { + const handleCellBlur = (rowId, columnKey, newValue, isValid) => { + if (onCellChange && isValid) { onCellChange(rowId, columnKey, newValue); } setEditingCell(null); @@ -229,9 +266,15 @@ const MuiTableEditable = ({ editingCell.rowId === row.id && editingCell.columnKey === col.columnKey } - onBlur={(newValue) => - handleCellBlur(row.id, col.columnKey, newValue) + onBlur={(newValue, isValid) => + handleCellBlur( + row.id, + col.columnKey, + newValue, + isValid + ) } + validation={col.validation} /> ) : col.render ? ( col.render(row) diff --git a/src/i18n/en.json b/src/i18n/en.json index 8a04b78c8..eb149cd98 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2429,6 +2429,24 @@ "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.", + "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 {item} created successfully", + "placeholder": { + "search": "Search..." + } } }, "placeholders": { @@ -2524,7 +2542,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", @@ -2566,7 +2584,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." @@ -3613,6 +3631,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 a488d5585..20af2cee8 100644 --- a/src/layouts/sponsor-id-layout.js +++ b/src/layouts/sponsor-id-layout.js @@ -1,10 +1,10 @@ import React from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; +import { Breadcrumb } from "react-breadcrumbs"; import { Switch, Route } from "react-router-dom"; import EditSponsorPage from "../pages/sponsors/edit-sponsor-page"; import { getSponsor, resetSponsorForm } from "../actions/sponsor-actions"; -import { Breadcrumb } from "react-breadcrumbs"; import EditAdSponsorPage from "../pages/sponsors/edit-advertisement-sponsor-page"; import EditMaterialSponsorPage from "../pages/sponsors/edit-material-sponsor-page"; import EditSocialNetworkSponsorPage from "../pages/sponsors/edit-social-network-sponsor-page"; @@ -23,7 +23,7 @@ class SponsorIdLayout extends React.Component { } } - componentDidUpdate(prevProps, prevState, snapshot) { + componentDidUpdate(prevProps) { const oldId = prevProps.match.params.sponsor_id; const newId = this.props.match.params.sponsor_id; @@ -38,7 +38,7 @@ class SponsorIdLayout extends React.Component { render() { const { match, currentSponsor } = this.props; - let sponsorId = this.props.match.params.sponsor_id; + const sponsorId = this.props.match.params.sponsor_id; const breadcrumb = currentSponsor.id ? currentSponsor.company.name : T.translate("general.new"); @@ -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-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..4fe13fca6 --- /dev/null +++ b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-form-item-from-inventory.js @@ -0,0 +1,280 @@ +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 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 { amountFromCents } from "../../../../../utils/currency"; +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) => `$ ${amountFromCents(row.early_bird_rate)}` + }, + { + columnKey: "standard_rate", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.standard_rate" + ), + sortable: false, + render: (row) => `$ ${amountFromCents(row.standard_rate)}` + }, + { + columnKey: "onsite_rate", + header: T.translate( + "edit_sponsor.forms_tab.form_manage_items.onsite_rate" + ), + sortable: false, + render: (row) => `$ ${amountFromCents(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: "Z-A", 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..c5cc4b29a --- /dev/null +++ b/src/pages/sponsors/sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items.js @@ -0,0 +1,419 @@ +/** + * 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 * as yup from "yup"; +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 { parsePrice } from "../../../../../utils/currency"; +import { DEFAULT_CURRENT_PAGE } from "../../../../../utils/constants"; + +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 handleManagedPageChange = (page) => { + const { perPage, order, orderDir } = items; + getSponsorCustomizedFormItems( + formId, + term, + page, + perPage, + order, + orderDir, + hideArchived + ); + }; + + const handleManagedSort = (key, dir) => { + const { currentPage, perPage } = items; + getSponsorCustomizedFormItems( + formId, + term, + currentPage, + perPage, + key, + dir, + hideArchived + ); + }; + + const handleSearch = (searchTerm) => { + getSponsorCustomizedFormItems( + formId, + searchTerm, + currentPage, + perPage, + order, + orderDir, + hideArchived + ); + }; + + const handleItemSave = (item) => { + saveSponsorFormManagedItem(formId, item).then(() => { + getSponsorCustomizedFormItems( + formId, + term, + currentPage, + 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 handleHideArchivedItens = (ev) => { + getSponsorCustomizedFormItems( + formId, + term, + items.currentPage, + items.perPage, + items.order, + items.orderDir, + ev.target.checked + ); + }; + + const handleAddFromInventory = (itemsId) => { + addSponsorManagedFormItems(formId, itemsId).then(() => handleClose()); + }; + + const handleCellEdit = (rowId, column, value) => { + const tmpEntity = { + id: rowId, + [column]: parsePrice(value) + }; + 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 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 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.hide_archived")} + /> + + + + + + + + + + + + + +
+ +
+ + {/* 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 91f56a286..ce8b14a98 100644 --- a/src/pages/sponsors/sponsor-forms-tab/index.js +++ b/src/pages/sponsors/sponsor-forms-tab/index.js @@ -40,6 +40,7 @@ import CustomizedFormPopup from "./components/customized-form/customized-form-po const SponsorFormsTab = ({ term, + history, hideArchived, managedForms, customizedForms, @@ -110,7 +111,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 e59f3a254..434288705 100644 --- a/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js +++ b/src/pages/sponsors_inventory/popup/sponsor-inventory-popup.js @@ -34,6 +34,7 @@ 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"; import FormikTextEditor from "../../../components/inputs/formik-text-editor"; +import MuiFormikPriceField from "../../../components/mui/formik-inputs/mui-formik-pricefield"; const SponsorItemDialog = ({ open, @@ -69,8 +70,8 @@ const SponsorItemDialog = ({ name: "", type: "Text", is_required: false, - minimum_quantity: null, - maximum_quantity: null, + minimum_quantity: 0, + maximum_quantity: 0, values: [] } ], @@ -80,7 +81,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(), @@ -97,11 +98,12 @@ const SponsorItemDialog = ({ yup.object().shape({ name: yup .string() - .when(["values", "minimum_quantity", "maximum_quantity"], { - is: (values, minQty, maxQty) => { + .when(["type", "values", "minimum_quantity", "maximum_quantity"], { + is: (type, values, minQty, maxQty) => { // required only if has values or quantities const hasValues = values && values.length > 0; - const hasQuantities = minQty !== null || maxQty !== null; + const hasQuantities = + type === "Quantity" && (minQty != null || maxQty != null); return hasValues || hasQuantities; }, then: (schema) => @@ -193,7 +195,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)); @@ -255,7 +257,9 @@ const SponsorItemDialog = ({ disableRestoreFocus > - Edit Item + {initialEntity.id + ? T.translate("edit_inventory_item.edit_item") + : T.translate("edit_inventory_item.new_item")} @@ -313,7 +317,7 @@ const SponsorItemDialog = ({ {T.translate("edit_inventory_item.early_bird_rate")} - {T.translate("edit_inventory_item.standard_rate")} - {T.translate("edit_inventory_item.onsite_rate")} - 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..304589c76 --- /dev/null +++ b/src/reducers/sponsors/sponsor-customized-form-items-list-reducer.js @@ -0,0 +1,200 @@ +/** + * 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 { + 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"; +import { CENTS_FACTOR, DECIMAL_DIGITS } from "../../utils/constants"; +import { amountFromCents } from "../../utils/currency"; + +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, + 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 { order, orderDir, page, hideArchived } = payload; + + return { + ...state, + order, + orderDir, + items: [], + currentPage: page, + 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: `$${(a.early_bird_rate / CENTS_FACTOR).toFixed( + DECIMAL_DIGITS + )}`, + standard_rate: `$${(a.standard_rate / CENTS_FACTOR).toFixed( + DECIMAL_DIGITS + )}`, + onsite_rate: `$${(a.onsite_rate / CENTS_FACTOR).toFixed( + DECIMAL_DIGITS + )}`, + 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: `$${( + updatedItem.early_bird_rate / CENTS_FACTOR + ).toFixed(DECIMAL_DIGITS)}`, + standard_rate: `$${( + updatedItem.standard_rate / CENTS_FACTOR + ).toFixed(DECIMAL_DIGITS)}`, + onsite_rate: `$${(updatedItem.onsite_rate / CENTS_FACTOR).toFixed( + DECIMAL_DIGITS + )}`, + 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 e273e38ce..643c5be14 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -142,6 +142,8 @@ export const DECIMAL_DIGITS = 2; export const TWO = 2; +export const FOUR = 4; + export const TEN = 10; export const ONE_HUNDRED = 100; @@ -224,3 +226,14 @@ export const ROOM_OCCUPANCY_OPTIONS = [ "FULL", "OVERFLOW" ]; + +export const SPONSOR_TABS = { + GENERAL: 0, + USERS: 1, + PAGES: 2, + MEDIA_UPLOADS: 3, + FORMS: 4, + CART: 5, + PURCHASES: 6, + BADGE_SCANS: 7 +};