diff --git a/src/actions/sponsor-purchases-actions.js b/src/actions/sponsor-purchases-actions.js index 2b38f833d..6e9ca2d7a 100644 --- a/src/actions/sponsor-purchases-actions.js +++ b/src/actions/sponsor-purchases-actions.js @@ -19,7 +19,8 @@ import { postRequest, putRequest, startLoading, - stopLoading + stopLoading, + getCSV } from "openstack-uicore-foundation/lib/utils/actions"; import T from "i18n-react/dist/i18n-react"; import { escapeFilterValue, getAccessTokenSafely } from "../utils/methods"; @@ -31,6 +32,8 @@ import { } from "../utils/constants"; import { snackbarErrorHandler, snackbarSuccessHandler } from "./base-actions"; +export const REQUEST_ALL_SPONSOR_PURCHASES = "REQUEST_ALL_SPONSOR_PURCHASES"; +export const RECEIVE_ALL_SPONSOR_PURCHASES = "RECEIVE_ALL_SPONSOR_PURCHASES"; export const REQUEST_SPONSOR_PURCHASES = "REQUEST_SPONSOR_PURCHASES"; export const RECEIVE_SPONSOR_PURCHASES = "RECEIVE_SPONSOR_PURCHASES"; export const SPONSOR_PURCHASE_STATUS_UPDATED = @@ -40,6 +43,127 @@ export const CLEAR_SPONSOR_ORDER = "CLEAR_SPONSOR_ORDER"; export const SPONSOR_CLIENT_ADDRESS_UPDATED = "SPONSOR_CLIENT_ADDRESS_UPDATED"; export const SPONSOR_CLIENT_UPDATED = "SPONSOR_CLIENT_UPDATED"; +export const getAllSponsorPurchases = + ( + term = "", + page = DEFAULT_CURRENT_PAGE, + perPage = DEFAULT_PER_PAGE, + order = "created", + orderDir = -1 + ) => + async (dispatch, getState) => { + const { currentSummitState } = getState(); + const { currentSummit } = currentSummitState; + const accessToken = await getAccessTokenSafely(); + const filter = []; + + dispatch(startLoading()); + + if (term) { + const escapedTerm = escapeFilterValue(term); + filter.push( + `number==${escapedTerm},company_name=@${escapedTerm},purchased_by_email=@${escapedTerm},purchased_by_full_name=@${escapedTerm}` + ); + } + + const params = { + page, + per_page: perPage, + access_token: accessToken, + expand: "sponsor", + relations: "sponsor", + fields: + "number,payment_id,purchased_date,sponsor.id,sponsor.company_name,payment_method,status,net_amount" + }; + + if (filter.length > 0) { + params["filter[]"] = filter; + } + + // order + if (order != null && orderDir != null) { + const orderDirSign = orderDir === 1 ? "" : "-"; + switch (order) { + case "purchased": + params.order = `${orderDirSign}created`; + break; + case "amount": + params.order = `${orderDirSign}net_amount`; + break; + case "sponsor_name": + params.order = `${orderDirSign}sponsor_company_name`; + break; + default: + params.order = `${orderDirSign}${order}`; + } + } + + return getRequest( + createAction(REQUEST_ALL_SPONSOR_PURCHASES), + createAction(RECEIVE_ALL_SPONSOR_PURCHASES), + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/purchases`, + authErrorHandler, + { order, orderDir, page, perPage, term } + )(params)(dispatch).finally(() => { + dispatch(stopLoading()); + }); + }; + +export const exportAllSponsorPurchases = + (term = null, order, orderDir) => + async (dispatch, getState) => { + const { currentSummitState } = getState(); + const accessToken = await getAccessTokenSafely(); + const { currentSummit } = currentSummitState; + const filter = []; + const filename = `${currentSummit.id}-purchases.csv`; + + if (term) { + const escapedTerm = escapeFilterValue(term); + filter.push( + `number==${escapedTerm},company_name=@${escapedTerm},purchased_by_email=@${escapedTerm},purchased_by_full_name=@${escapedTerm}` + ); + } + + const params = { + access_token: accessToken, + expand: "sponsor", + relations: "sponsor", + fields: + "number,payment_id,purchased_date,sponsor.id,sponsor.company_name,payment_method,status,net_amount" + }; + + if (filter.length > 0) { + params["filter[]"] = filter; + } + + // order + if (order != null && orderDir != null) { + const orderDirSign = orderDir === 1 ? "" : "-"; + switch (order) { + case "purchased": + params.order = `${orderDirSign}created`; + break; + case "amount": + params.order = `${orderDirSign}net_amount`; + break; + case "sponsor_name": + params.order = `${orderDirSign}sponsor_company_name`; + break; + default: + params.order = `${orderDirSign}${order}`; + } + } + + dispatch( + getCSV( + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/purchases/csv`, + params, + filename + ) + ); + }; + export const getSponsorPurchases = ( term = "", @@ -82,7 +206,7 @@ export const getSponsorPurchases = params.order = `${orderDirSign}created`; break; case "amount": - params.order = `${orderDirSign}raw_amount`; + params.order = `${orderDirSign}net_amount`; break; default: params.order = `${orderDirSign}${order}`; @@ -101,10 +225,9 @@ export const getSponsorPurchases = }; export const approveSponsorPurchase = - (paymentId) => async (dispatch, getState) => { - const { currentSummitState, currentSponsorState } = getState(); + (sponsorId, paymentId) => async (dispatch, getState) => { + const { currentSummitState } = getState(); const { currentSummit } = currentSummitState; - const { entity: sponsor } = currentSponsorState; const accessToken = await getAccessTokenSafely(); const params = { @@ -119,7 +242,7 @@ export const approveSponsorPurchase = paymentId, status: PURCHASE_STATUS.PAID }), - `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/payments/${paymentId}/approve`, + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/payments/${paymentId}/approve`, {}, snackbarErrorHandler )(params)(dispatch) @@ -138,10 +261,9 @@ export const approveSponsorPurchase = }; export const rejectSponsorPurchase = - (paymentId) => async (dispatch, getState) => { - const { currentSummitState, currentSponsorState } = getState(); + (sponsorId, paymentId) => async (dispatch, getState) => { + const { currentSummitState } = getState(); const { currentSummit } = currentSummitState; - const { entity: sponsor } = currentSponsorState; const accessToken = await getAccessTokenSafely(); const params = { @@ -156,7 +278,7 @@ export const rejectSponsorPurchase = paymentId, status: PURCHASE_STATUS.CANCELLED }), - `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/payments/${paymentId}/cancel`, + `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsorId}/payments/${paymentId}/cancel`, null, snackbarErrorHandler )(params)(dispatch) diff --git a/src/components/menu/menu-definition.js b/src/components/menu/menu-definition.js index d853d6d58..0baaa4985 100644 --- a/src/components/menu/menu-definition.js +++ b/src/components/menu/menu-definition.js @@ -218,6 +218,11 @@ export const getSummitItems = (summitId) => [ linkUrl: `summits/${summitId}/sponsors/pages`, accessRoute: "admin-sponsors" }, + { + name: "sponsor_purchases", + linkUrl: `summits/${summitId}/sponsors/purchases`, + accessRoute: "admin-sponsors" + }, { name: "sponsorship_list", linkUrl: `summits/${summitId}/sponsorships`, diff --git a/src/i18n/en.json b/src/i18n/en.json index f5a408eb3..ff25400a5 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -90,6 +90,7 @@ "to": "To", "from": "From", "placeholders": { + "search": "Search...", "search_speakers": "Search Speakers by Name, Email, Speaker Id or Member Id", "select_acceptance_criteria": "Select acceptance criteria", "select_invitation_status": "Select status" @@ -186,6 +187,7 @@ "sponsor_list": "Sponsor List", "sponsor_forms": "Forms", "sponsor_pages": "Pages", + "sponsor_purchases": "Purchases", "sponsorship_list": "Tiers", "sponsor_users": "Users", "sponsors_promocodes": "Promo Codes", @@ -2866,6 +2868,16 @@ } } }, + "sponsor_show_purchases": { + "order": "Order", + "purchased": "Purchased", + "sponsor": "Sponsor", + "payment_method": "Payment Method", + "status": "Status", + "amount": "Amount", + "details": "Details", + "purchases": "Purchases" + }, "sponsor_users": { "users": "Users", "access_request": "access request", diff --git a/src/layouts/sponsor-layout.js b/src/layouts/sponsor-layout.js index cba7da2e6..68dd29cd6 100644 --- a/src/layouts/sponsor-layout.js +++ b/src/layouts/sponsor-layout.js @@ -44,6 +44,9 @@ const SponsorUsersListPage = React.lazy(() => const ShowPagesListPage = React.lazy(() => import("../pages/sponsors/show-pages-list-page") ); +const SponsorOrdersListPage = React.lazy(() => + import("../pages/sponsors/show-purchase-list-page") +); const SponsorLayout = ({ match }) => (
@@ -85,20 +88,16 @@ const SponsorLayout = ({ match }) => ( /> ( -
- -
- )} strict exact component={ShowPagesListPage} /> + ({ ...overrides }); -const renderWithConfirmDialog = (ui, options) => renderWithRedux( +const renderWithConfirmDialog = (ui, options) => + renderWithRedux( <> {ui} @@ -90,15 +91,18 @@ describe("ShowPagesListPage", () => { describe("Component", () => { it("should render empty state when no pages exist", () => { - renderWithConfirmDialog(, { - initialState: { - showPagesListState: { - ...showPagesListDefaultState, - showPages: [], - totalCount: 0 + renderWithConfirmDialog( + , + { + initialState: { + showPagesListState: { + ...showPagesListDefaultState, + showPages: [], + totalCount: 0 + } } } - }); + ); expect( screen.getByText("show_pages.no_sponsors_pages") @@ -106,30 +110,36 @@ describe("ShowPagesListPage", () => { }); it("should render table when pages exist", () => { - renderWithConfirmDialog(, { - initialState: { - showPagesListState: { - ...showPagesListDefaultState, - showPages: [createShowPage(1), createShowPage(2)], - totalCount: 2 + renderWithConfirmDialog( + , + { + initialState: { + showPagesListState: { + ...showPagesListDefaultState, + showPages: [createShowPage(1), createShowPage(2)], + totalCount: 2 + } } } - }); + ); expect(screen.getByText("Page 1")).toBeInTheDocument(); expect(screen.getByText("Page 2")).toBeInTheDocument(); }); it("should call getShowPage and open popup when edit is clicked", async () => { - renderWithConfirmDialog(, { - initialState: { - showPagesListState: { - ...showPagesListDefaultState, - showPages: [createShowPage(1)], - totalCount: 1 + renderWithConfirmDialog( + , + { + initialState: { + showPagesListState: { + ...showPagesListDefaultState, + showPages: [createShowPage(1)], + totalCount: 1 + } } } - }); + ); const editButton = screen.getByTestId("EditIcon").closest("button"); await act(async () => { await userEvent.click(editButton); @@ -145,15 +155,18 @@ describe("ShowPagesListPage", () => { }); it("should refresh list after save", async () => { - renderWithConfirmDialog(, { - initialState: { - showPagesListState: { - ...showPagesListDefaultState, - showPages: [createShowPage(1)], - totalCount: 1 + renderWithConfirmDialog( + , + { + initialState: { + showPagesListState: { + ...showPagesListDefaultState, + showPages: [createShowPage(1)], + totalCount: 1 + } } } - }); + ); const editButton = screen.getByTestId("EditIcon").closest("button"); await act(async () => { await userEvent.click(editButton); @@ -178,19 +191,22 @@ describe("ShowPagesListPage", () => { }); it("should call deleteShowPage and refresh list when delete is confirmed", async () => { - renderWithConfirmDialog(, { - initialState: { - showPagesListState: { - ...showPagesListDefaultState, - showPages: [ - createShowPage(1), - createShowPage(2), - createShowPage(3) - ], - totalCount: 3 + renderWithConfirmDialog( + , + { + initialState: { + showPagesListState: { + ...showPagesListDefaultState, + showPages: [ + createShowPage(1), + createShowPage(2), + createShowPage(3) + ], + totalCount: 3 + } } } - }); + ); const deleteButtons = screen.getAllByTestId("DeleteIcon"); const secondDeleteButton = deleteButtons[1].closest("button"); await act(async () => { @@ -215,15 +231,18 @@ describe("ShowPagesListPage", () => { }); it("should not call deleteShowPage when delete is cancelled", async () => { - renderWithConfirmDialog(, { - initialState: { - showPagesListState: { - ...showPagesListDefaultState, - showPages: [createShowPage(1)], - totalCount: 1 + renderWithConfirmDialog( + , + { + initialState: { + showPagesListState: { + ...showPagesListDefaultState, + showPages: [createShowPage(1)], + totalCount: 1 + } } } - }); + ); const deleteButton = screen.getByTestId("DeleteIcon").closest("button"); await act(async () => { await userEvent.click(deleteButton); @@ -243,15 +262,18 @@ describe("ShowPagesListPage", () => { }); it("should call archiveShowPage for non-archived item", async () => { - renderWithConfirmDialog(, { - initialState: { - showPagesListState: { - ...showPagesListDefaultState, - showPages: [createShowPage(1, { is_archived: false })], - totalCount: 1 + renderWithConfirmDialog( + , + { + initialState: { + showPagesListState: { + ...showPagesListDefaultState, + showPages: [createShowPage(1, { is_archived: false })], + totalCount: 1 + } } } - }); + ); const archiveButton = screen.getByText("general.archive"); await act(async () => { await userEvent.click(archiveButton); @@ -260,15 +282,18 @@ describe("ShowPagesListPage", () => { }); it("should call unarchiveShowPage for archived item", async () => { - renderWithConfirmDialog(, { - initialState: { - showPagesListState: { - ...showPagesListDefaultState, - showPages: [createShowPage(1, { is_archived: true })], - totalCount: 1 + renderWithConfirmDialog( + , + { + initialState: { + showPagesListState: { + ...showPagesListDefaultState, + showPages: [createShowPage(1, { is_archived: true })], + totalCount: 1 + } } } - }); + ); const unarchiveButton = screen.getByText("general.unarchive"); await act(async () => { await userEvent.click(unarchiveButton); diff --git a/src/pages/sponsors/show-pages-list-page/index.js b/src/pages/sponsors/show-pages-list-page/index.js index f9c64b527..7bddc646b 100644 --- a/src/pages/sponsors/show-pages-list-page/index.js +++ b/src/pages/sponsors/show-pages-list-page/index.js @@ -14,6 +14,7 @@ import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import T from "i18n-react/dist/i18n-react"; +import { Breadcrumb } from "react-breadcrumbs"; import { Box, Button, @@ -26,13 +27,13 @@ import AddIcon from "@mui/icons-material/Add"; import MuiTable from "openstack-uicore-foundation/lib/components/mui/table"; import SearchInput from "openstack-uicore-foundation/lib/components/mui/search-input"; import { - getShowPages, archiveShowPage, - unarchiveShowPage, + deleteShowPage, getShowPage, + getShowPages, + resetShowPageForm, saveShowPage, - deleteShowPage, - resetShowPageForm + unarchiveShowPage } from "../../../actions/show-pages-actions"; import { getSponsorships } from "../../../actions/sponsor-forms-actions"; import CustomAlert from "../../../components/mui/custom-alert"; @@ -41,6 +42,7 @@ import PageTemplatePopup from "../../sponsors-global/page-templates/page-templat import { DEFAULT_CURRENT_PAGE, MAX_PER_PAGE } from "../../../utils/constants"; const ShowPagesListPage = ({ + match, showPages, currentPage, perPage, @@ -179,6 +181,14 @@ const ShowPagesListPage = ({ return (
+
+ +

{T.translate("show_pages.pages")}

{ + useEffect(() => { + getAllSponsorPurchases(); + }, []); + + const handlePageChange = (page) => { + getAllSponsorPurchases(term, page, perPage, order, orderDir); + }; + + const handleSort = (key, dir) => { + getAllSponsorPurchases(term, currentPage, perPage, key, dir); + }; + + const handlePerPageChange = (newPerPage) => { + getAllSponsorPurchases( + term, + DEFAULT_CURRENT_PAGE, + newPerPage, + order, + orderDir + ); + }; + + const handleExport = () => { + exportAllSponsorPurchases(term, order, orderDir); + }; + + const handleSearch = (searchTerm) => { + getAllSponsorPurchases(searchTerm); + }; + + const handleDetails = (item) => { + history.push(`${item.sponsor_id}/purchases/${item.id}`); + }; + + const handleMenu = (item) => { + console.log("MENU : ", item); + }; + + const handleStatusChange = (sponsorId, purchaseId, newStatus) => { + if (newStatus === PURCHASE_STATUS.PAID) + approveSponsorPurchase(sponsorId, purchaseId); + if (newStatus === PURCHASE_STATUS.CANCELLED) + rejectSponsorPurchase(sponsorId, purchaseId); + }; + + const tableColumns = [ + { + columnKey: "number", + header: T.translate("sponsor_show_purchases.order"), + sortable: true + }, + { + columnKey: "purchased", + header: T.translate("sponsor_show_purchases.purchased"), + sortable: true + }, + { + columnKey: "sponsor_name", + header: T.translate("sponsor_show_purchases.sponsor"), + sortable: true + }, + { + columnKey: "payment_method", + header: T.translate("sponsor_show_purchases.payment_method"), + sortable: true + }, + { + columnKey: "status", + header: T.translate("sponsor_show_purchases.status"), + sortable: true, + render: (row) => { + if ( + row.payment_method === PURCHASE_METHODS.INVOICE && + row.status === PURCHASE_STATUS.PENDING + ) { + return ( + + ); + } + + return row.status; + } + }, + { + columnKey: "amount", + header: T.translate("sponsor_show_purchases.amount"), + sortable: true + }, + { + columnKey: "details", + header: "", + width: 100, + align: "center", + render: (row) => ( + + ) + }, + { + columnKey: "menu", + header: "", + width: 100, + align: "center", + render: (row) => ( + handleMenu(row)} + > + + + ) + } + ]; + + return ( +
+
+ +
+

{T.translate("sponsor_show_purchases.purchases")}

+ + + + {totalCount}{" "} + {T.translate("sponsor_show_purchases.purchases").toLowerCase()} + + + + + + + + + +
+ +
+
+ ); +}; + +const mapStateToProps = ({ showPurchaseListState }) => ({ + ...showPurchaseListState +}); + +export default connect(mapStateToProps, { + getAllSponsorPurchases, + exportAllSponsorPurchases, + approveSponsorPurchase, + rejectSponsorPurchase +})(ShowPurchaseListPage); diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/__tests__/sponsor-purchases-list.test.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/__tests__/sponsor-purchases-list.test.js index 2c89b5956..564ba84ec 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/__tests__/sponsor-purchases-list.test.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/__tests__/sponsor-purchases-list.test.js @@ -247,7 +247,10 @@ describe("SponsorPurchasesTab", () => { ); }); - expect(approveSponsorPurchase).toHaveBeenCalledWith(purchase.payment_id); + expect(approveSponsorPurchase).toHaveBeenCalledWith( + 123, + purchase.payment_id + ); expect(rejectSponsorPurchase).not.toHaveBeenCalled(); }); @@ -270,7 +273,10 @@ describe("SponsorPurchasesTab", () => { ); }); - expect(rejectSponsorPurchase).toHaveBeenCalledWith(purchase.payment_id); + expect(rejectSponsorPurchase).toHaveBeenCalledWith( + 123, + purchase.payment_id + ); expect(approveSponsorPurchase).not.toHaveBeenCalled(); }); @@ -326,7 +332,10 @@ describe("SponsorPurchasesTab", () => { ); }); - expect(approveSponsorPurchase).toHaveBeenCalledWith(purchase.payment_id); + expect(approveSponsorPurchase).toHaveBeenCalledWith( + 123, + purchase.payment_id + ); // Dropdown is still present — component did not unmount on error expect(withinTableBody().getByRole("combobox")).toBeInTheDocument(); }); @@ -356,7 +365,10 @@ describe("SponsorPurchasesTab", () => { ); }); - expect(rejectSponsorPurchase).toHaveBeenCalledWith(purchase.payment_id); + expect(rejectSponsorPurchase).toHaveBeenCalledWith( + 123, + purchase.payment_id + ); expect(withinTableBody().getByRole("combobox")).toBeInTheDocument(); }); }); diff --git a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/index.js b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/index.js index 9e7de9dfa..c7d0770a2 100644 --- a/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/index.js +++ b/src/pages/sponsors/sponsor-page/tabs/sponsor-purchases-tab/index.js @@ -85,9 +85,10 @@ const SponsorPurchasesTab = ({ }; const handleStatusChange = (purchaseId, newStatus) => { - if (newStatus === PURCHASE_STATUS.PAID) approveSponsorPurchase(purchaseId); + if (newStatus === PURCHASE_STATUS.PAID) + approveSponsorPurchase(sponsor.id, purchaseId); if (newStatus === PURCHASE_STATUS.CANCELLED) - rejectSponsorPurchase(purchaseId); + rejectSponsorPurchase(sponsor.id, purchaseId); }; const tableColumns = [ @@ -149,7 +150,7 @@ const SponsorPurchasesTab = ({ render: (row) => (