Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ PRINT_APP_URL=https://badge-print-app.dev.fnopen.com
PUB_API_BASE_URL=
OS_BASE_URL=
SCOPES_BASE_REALM=${API_BASE_URL}
PURCHASES_API_SCOPES="purchases-show-medata/read purchases-show-medata/write show-form/read show-form/write customized-form/write customized-form/read carts/read carts/write"
PURCHASES_API_SCOPES="purchases-show-medata/read purchases-show-medata/write show-form/read show-form/write customized-form/write customized-form/read carts/read carts/write purchases/read"
SPONSOR_USERS_API_SCOPES="show-medata/read show-medata/write access-requests/read access-requests/write sponsor-users/read sponsor-users/write groups/read groups/write"
EMAIL_SCOPES="clients/read templates/read templates/write emails/read"
FILE_UPLOAD_SCOPES="files/upload"
Expand Down
81 changes: 81 additions & 0 deletions src/actions/sponsor-purchases-actions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* Copyright 2018 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* */

import {
authErrorHandler,
createAction,
getRequest,
startLoading,
stopLoading
} from "openstack-uicore-foundation/lib/utils/actions";

import { escapeFilterValue, getAccessTokenSafely } from "../utils/methods";
import {
DEFAULT_CURRENT_PAGE,
DEFAULT_ORDER_DIR,
DEFAULT_PER_PAGE
} from "../utils/constants";

export const REQUEST_SPONSOR_PURCHASES = "REQUEST_SPONSOR_PURCHASES";
export const RECEIVE_SPONSOR_PURCHASES = "RECEIVE_SPONSOR_PURCHASES";

export const getSponsorPurchases =
(
term = "",
page = DEFAULT_CURRENT_PAGE,
perPage = DEFAULT_PER_PAGE,
order = "id",
orderDir = DEFAULT_ORDER_DIR
) =>
async (dispatch, getState) => {
const { currentSummitState, currentSponsorState } = getState();
const { currentSummit } = currentSummitState;
const { entity: sponsor } = currentSponsorState;
const accessToken = await getAccessTokenSafely();
const filter = [];

dispatch(startLoading());

if (term) {
const escapedTerm = escapeFilterValue(term);
filter.push(
`number==${escapedTerm},purchased_by_email=@${escapedTerm},purchased_by_full_name=@${escapedTerm}`
);
}

const params = {
page,
per_page: perPage,
access_token: accessToken
};

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_PURCHASES),
createAction(RECEIVE_SPONSOR_PURCHASES),
`${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases`,
authErrorHandler,
{ order, orderDir, page, perPage, term }
)(params)(dispatch).then(() => {
dispatch(stopLoading());
});
};
Comment on lines +72 to +81
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing error handling for API failures.

The promise chain only has .then() for success. If the request fails, stopLoading() won't be called, potentially leaving the UI in a loading state.

Suggested fix
     return getRequest(
       createAction(REQUEST_SPONSOR_PURCHASES),
       createAction(RECEIVE_SPONSOR_PURCHASES),
       `${window.PURCHASES_API_URL}/api/v1/summits/${currentSummit.id}/sponsors/${sponsor.id}/purchases`,
       authErrorHandler,
       { order, orderDir, page, perPage, term }
-    )(params)(dispatch).then(() => {
-      dispatch(stopLoading());
-    });
+    )(params)(dispatch)
+      .then(() => {
+        dispatch(stopLoading());
+      })
+      .catch(() => {
+        dispatch(stopLoading());
+      });
   };

Alternatively, use .finally():

-    )(params)(dispatch).then(() => {
-      dispatch(stopLoading());
-    });
+    )(params)(dispatch).finally(() => {
+      dispatch(stopLoading());
+    });
🤖 Prompt for AI Agents
In `@src/actions/sponsor-purchases-actions.js` around lines 72 - 81, The API call
initiated via getRequest(createAction(REQUEST_SPONSOR_PURCHASES),
createAction(RECEIVE_SPONSOR_PURCHASES), ...)(params)(dispatch) currently only
uses .then(), so stopLoading() is never dispatched on errors; update the promise
chain to ensure stopLoading() runs on both success and failure (use .finally()
to dispatch stopLoading() or add a .catch(err => { /* handle or dispatch error
*/ }) that dispatches an error action or logs via authErrorHandler and then
rethrows or returns, followed by .finally(() => dispatch(stopLoading()))),
referencing the getRequest call, authErrorHandler, and stopLoading dispatch in
sponsor-purchases-actions.js.

9 changes: 9 additions & 0 deletions src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2470,6 +2470,15 @@
"pay_cc": "pay with credit card or ach",
"pay_invoice": "pay with invoice"
},
"purchase_tab": {
"order": "Order",
"purchased": "Purchased",
"payment_method": "Payment Method",
"status": "Status",
"amount": "Amount",
"details": "Details",
"purchases": "purchases"
},
"placeholders": {
"select_sponsorship": "Select a Sponsorship",
"sponsorship_type": "Start typing to choose a Tier...",
Expand Down
4 changes: 4 additions & 0 deletions src/pages/sponsors/edit-sponsor-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import SponsorBadgeScans from "./sponsor-badge-scans";
import SponsorCartTab from "./sponsor-cart-tab";
import SponsorFormsManageItems from "./sponsor-forms-tab/components/manage-items/sponsor-forms-manage-items";
import { SPONSOR_TABS } from "../../utils/constants";
import SponsorPurchasesTab from "./sponsor-purchases-tab";

const CustomTabPanel = (props) => {
const { children, value, index, ...other } = props;
Expand Down Expand Up @@ -225,6 +226,9 @@ const EditSponsorPage = (props) => {
<CustomTabPanel value={selectedTab} index={5}>
<SponsorCartTab sponsor={entity} summitId={currentSummit.id} />
</CustomTabPanel>
<CustomTabPanel value={selectedTab} index={6}>
<SponsorPurchasesTab sponsor={entity} summitId={currentSummit.id} />
</CustomTabPanel>
<CustomTabPanel value={selectedTab} index={7}>
<SponsorBadgeScans sponsor={entity} />
</CustomTabPanel>
Expand Down
204 changes: 204 additions & 0 deletions src/pages/sponsors/sponsor-purchases-tab/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* 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 } from "react";
import { connect } from "react-redux";
import T from "i18n-react/dist/i18n-react";
import {
Box,
Button,
Grid2,
IconButton,
MenuItem,
Select
} from "@mui/material";
import MenuIcon from "@mui/icons-material/Menu";
import { getSponsorPurchases } from "../../../actions/sponsor-purchases-actions";
import SearchInput from "../../../components/mui/search-input";
import MuiTable from "../../../components/mui/table/mui-table";
import {
DEFAULT_CURRENT_PAGE,
PURCHASE_STATUS
} from "../../../utils/constants";

const SponsorPurchasesTab = ({
purchases,
term,
order,
orderDir,
currentPage,
perPage,
totalCount,
getSponsorPurchases
}) => {
useEffect(() => {
getSponsorPurchases();
}, []);

const handlePageChange = (page) => {
getSponsorPurchases(term, page, perPage, order, orderDir);
};

const handleSort = (key, dir) => {
getSponsorPurchases(term, currentPage, perPage, key, dir);
};

const handlePerPageChange = (newPerPage) => {
getSponsorPurchases(
term,
DEFAULT_CURRENT_PAGE,
newPerPage,
order,
orderDir
);
};

const handleSearch = (searchTerm) => {
getSponsorPurchases(searchTerm);
};

const handleDetails = (item) => {
console.log("DETAILS : ", item);
};

const handleMenu = (item) => {
console.log("MENU : ", item);
};

const handleStatusChange = (stat) => {
console.log("STATUS : ", stat);
};
Comment on lines +78 to +80
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Status change handler is missing row context.

handleStatusChange only receives the new status value but not which purchase item to update. When the API call is implemented, you'll need the purchase ID to know which record to update.

Suggested fix
-  const handleStatusChange = (stat) => {
-    console.log("STATUS : ", stat);
+  const handleStatusChange = (row, newStatus) => {
+    console.log("STATUS : ", row, newStatus);
+    // TODO: dispatch action to update purchase status
   };

And update the onChange handler:

-              onChange={(ev) => handleStatusChange(ev.target.value)}
+              onChange={(ev) => handleStatusChange(row, ev.target.value)}

Also applies to: 102-121

🤖 Prompt for AI Agents
In `@src/pages/sponsors/sponsor-purchases-tab/index.js` around lines 78 - 80,
handleStatusChange currently only receives the new status (stat) and lacks
context about which purchase to update; change its signature to accept the
purchase identifier (e.g., handleStatusChange(purchaseId, stat)) and update
every onChange usage (e.g., in the row render where you map purchases) to pass
the current row's id along with the new status. Inside handleStatusChange use
the purchaseId to call the API/update state for that specific record; reference
the existing handleStatusChange function and the onChange handlers inside the
purchase rows to locate and update the usages.


const tableColumns = [
{
columnKey: "number",
header: T.translate("edit_sponsor.purchase_tab.order"),
sortable: true
},
Comment on lines +82 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check the reducer mapping for purchases data structure
rg -n -A 15 "const purchases" src/reducers/sponsors/sponsor-page-purchase-list-reducer.js | head -40

Repository: fntechgit/summit-admin

Length of output: 535


🏁 Script executed:

# Also check if there's any mapping of "order" to "number" in the component or elsewhere
rg -n "columnKey.*number" src/pages/sponsors/sponsor-purchases-tab/index.js

Repository: fntechgit/summit-admin

Length of output: 95


🏁 Script executed:

# Check the purchase list reducer more broadly to understand the data shape
cat src/reducers/sponsors/sponsor-page-purchase-list-reducer.js

Repository: fntechgit/summit-admin

Length of output: 2413


Fix column key mismatch for order number.

The reducer maps order: a.order_number (sponsor-page-purchase-list-reducer.js line 65), but this column uses columnKey: "number". The column will display empty values since the property should be order, not number.

Suggested fix
  const tableColumns = [
    {
-     columnKey: "number",
+     columnKey: "order",
      header: T.translate("edit_sponsor.purchase_tab.order"),
      sortable: true
    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const tableColumns = [
{
columnKey: "number",
header: T.translate("edit_sponsor.purchase_tab.order"),
sortable: true
},
const tableColumns = [
{
columnKey: "order",
header: T.translate("edit_sponsor.purchase_tab.order"),
sortable: true
},
🤖 Prompt for AI Agents
In `@src/pages/sponsors/sponsor-purchases-tab/index.js` around lines 82 - 87, The
tableColumns entry uses columnKey "number" but the reducer maps the value as
order: a.order_number, so the column is empty; change the column's columnKey
from "number" to "order" in the tableColumns array so it matches the reducer
mapping (order: a.order_number) and displays the purchase order number
correctly.

{
columnKey: "purchased",
header: T.translate("edit_sponsor.purchase_tab.purchased"),
sortable: true
},
{
columnKey: "payment_method",
header: T.translate("edit_sponsor.purchase_tab.payment_method"),
sortable: true
},
{
columnKey: "status",
header: T.translate("edit_sponsor.purchase_tab.status"),
sortable: true,
render: (row) => {
if (row.status === PURCHASE_STATUS.PENDING) {
return (
<Select
fullWidth
variant="outlined"
value={row.status}
onChange={(ev) => handleStatusChange(ev.target.value)}
>
{Object.values(PURCHASE_STATUS).map((s) => (
<MenuItem key={`purchase-status-${s}`} value={s}>
{s}
</MenuItem>
))}
</Select>
);
}

return row.status;
}
},
{
columnKey: "amount",
header: T.translate("edit_sponsor.purchase_tab.amount"),
sortable: true
},
{
columnKey: "details",
header: "",
width: 100,
align: "center",
render: (row) => (
<Button
variant="text"
color="inherit"
size="small"
onClick={() => handleDetails(row)}
>
{T.translate("edit_sponsor.purchase_tab.details")}
</Button>
)
},
{
columnKey: "menu",
header: "",
width: 100,
align: "center",
render: (row) => (
<IconButton size="large" onClick={() => handleMenu(row)}>
<MenuIcon fontSize="large" />
</IconButton>
)
}
];

return (
<Box sx={{ mt: 2 }}>
<Grid2
container
spacing={2}
sx={{
justifyContent: "center",
alignItems: "center",
mb: 2
}}
>
<Grid2 size={2}>
<Box component="span">
{totalCount} {T.translate("edit_sponsor.purchase_tab.purchases")}
</Box>
</Grid2>
<Grid2 size={2} offset={8}>
<SearchInput
term={term}
onSearch={handleSearch}
placeholder={T.translate("edit_sponsor.placeholders.search")}
/>
</Grid2>
</Grid2>
<div>
<MuiTable
columns={tableColumns}
data={purchases}
options={{ sortCol: order, sortDir: orderDir }}
perPage={perPage}
totalRows={totalCount}
currentPage={currentPage}
onPageChange={handlePageChange}
onPerPageChange={handlePerPageChange}
onSort={handleSort}
/>
</div>
</Box>
);
};

const mapStateToProps = ({ sponsorPagePurchaseListState }) => ({
...sponsorPagePurchaseListState
});

export default connect(mapStateToProps, {
getSponsorPurchases
})(SponsorPurchasesTab);
Loading