diff --git a/src/actions/KIInventoryActions.js b/src/actions/KIInventoryActions.js new file mode 100644 index 0000000000..b93738befd --- /dev/null +++ b/src/actions/KIInventoryActions.js @@ -0,0 +1,62 @@ +import axios from 'axios'; +import { ENDPOINTS } from '~/utils/URL'; +import { + KI_INVENTORY_FETCH_REQUEST, + KI_INVENTORY_FETCH_SUCCESS, + KI_INVENTORY_FETCH_FAILURE, + KI_INVENTORY_STATS_REQUEST, + KI_INVENTORY_STATS_SUCCESS, + KI_INVENTORY_STATS_FAILURE, + KI_PRESERVED_ITEMS_REQUEST, + KI_PRESERVED_ITEMS_SUCCESS, + KI_PRESERVED_ITEMS_FAILURE, +} from '../constants/KIInventoryConstants'; + +const createFetchAction = (requestType, successType, failureType, endpoint, defaultErrorMsg) => async dispatch => { + dispatch({ type: requestType }); + try { + const res = await axios.get(endpoint); + dispatch({ type: successType, payload: res.data.data }); + } catch (err) { + dispatch({ + type: failureType, + payload: err.response?.data?.message || defaultErrorMsg, + }); + } +}; + +/** + * Fetch all inventory items across all categories. + * GET /api/kitchenandinventory/inventory/items + */ +export const fetchInventoryItems = () => createFetchAction( + KI_INVENTORY_FETCH_REQUEST, + KI_INVENTORY_FETCH_SUCCESS, + KI_INVENTORY_FETCH_FAILURE, + ENDPOINTS.KI_INVENTORY_ITEMS, + 'Failed to fetch inventory items.' +); + +/** + * Fetch inventory stats — total items, critical stock count, low stock count. + * GET /api/kitchenandinventory/inventory/items/stats + */ +export const fetchInventoryStats = () => createFetchAction( + KI_INVENTORY_STATS_REQUEST, + KI_INVENTORY_STATS_SUCCESS, + KI_INVENTORY_STATS_FAILURE, + ENDPOINTS.KI_INVENTORY_STATS, + 'Failed to fetch inventory stats.' +); + +/** + * Fetch preserved ingredient items (expiry >= 1 year from now). + * GET /api/kitchenandinventory/inventory/items/ingredients/preserved + */ +export const fetchPreservedItems = () => createFetchAction( + KI_PRESERVED_ITEMS_REQUEST, + KI_PRESERVED_ITEMS_SUCCESS, + KI_PRESERVED_ITEMS_FAILURE, + ENDPOINTS.KI_INVENTORY_PRESERVED, + 'Failed to fetch preserved items.' +); diff --git a/src/components/KitchenandInventory/KIInventory/KIInventory.jsx b/src/components/KitchenandInventory/KIInventory/KIInventory.jsx index 395586d968..433ec34663 100644 --- a/src/components/KitchenandInventory/KIInventory/KIInventory.jsx +++ b/src/components/KitchenandInventory/KIInventory/KIInventory.jsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useSelector } from 'react-redux'; +import { useSelector, useDispatch } from 'react-redux'; import { Nav, NavItem, NavLink, TabContent, TabPane } from 'reactstrap'; import styles from './KIInventory.module.css'; import MetricCard from '../MetricCards/MetricCard'; @@ -20,20 +20,25 @@ import { import { RiLeafLine } from 'react-icons/ri'; import KIItemCard from './KIItemCard'; import { - ingredients, - preservedItems, - lowStock, - totalItems, - criticalStock, - onsiteGrown, - equipmentAndSupplies, - seeds, - canningSupplies, - animalSupplies, -} from './KIInventorySampleItems.js'; + fetchInventoryItems, + fetchInventoryStats, + fetchPreservedItems, +} from '../../../actions/KIInventoryActions'; + +// Category enum values — must match backend model enum exactly +const CATEGORY_MAP = { + ingredients: 'INGREDIENT', + 'equipment & supplies': 'EQUIPEMENTANDSUPPLIES', + seeds: 'SEEDS', + 'canning supplies': 'CANNINGSUPPLIES', + 'animal supplies': 'ANIMALSUPPLIES', +}; const KIInventory = () => { + const dispatch = useDispatch(); const darkMode = useSelector(state => state.theme.darkMode); + const { items, preservedItems, stats, loading } = useSelector(state => state.kiInventory); + const tabs = [ 'ingredients', 'equipment & supplies', @@ -43,19 +48,55 @@ const KIInventory = () => { ]; const [activeTab, setActiveTab] = useState(tabs[0]); const [searchTerm, setSearchTerm] = useState(''); + const toggleTab = tab => { - if (activeTab !== tabs[tab]) setActiveTab(tabs[tab]); + if (activeTab !== tabs[tab]) { + setActiveTab(tabs[tab]); + setSearchTerm(''); + } }; + + // Fetch all data on mount useEffect(() => { - // This is where you would fetch real data from an API or database - // For this example, we're using static sample data from KIInventorySampleItems.js - }, []); - let preservedDesc = []; - if (preservedItems.length > 0) { - preservedDesc = preservedItems.map( - item => `${item.presentQuantity} ${item.unit} of ${item.name}`, - ); - } + dispatch(fetchInventoryItems()); + dispatch(fetchInventoryStats()); + dispatch(fetchPreservedItems()); + }, [dispatch]); + + // Onsite grown — computed from all items + const onsiteGrown = items.filter(i => i.onsite).length; + + // Items for active tab filtered by category and search term + const activeCategory = CATEGORY_MAP[activeTab]; + const tabItems = items + .filter(i => i.category === activeCategory) + .filter(i => !searchTerm || i.name.toLowerCase().includes(searchTerm.toLowerCase())); + + // Preserved items description for notification banner + const preservedDesc = + preservedItems.length > 0 + ? preservedItems.map(item => `${item.presentQuantity} ${item.unit} of ${item.name}`) + : []; + + const renderItems = tabName => { + if (loading) { + return

Loading...

; + } + if (tabItems.length > 0) { + return tabItems.map(item => ( +
+ +
+ )); + } + if (searchTerm) { + return ( +

No results for "{searchTerm}"

+ ); + } + return

No items in {tabName} yet.

; + }; + return (
@@ -64,17 +105,21 @@ const KIInventory = () => {

Track ingredients, equipment, and supplies across all kitchen operations

- + - + @@ -82,71 +127,51 @@ const KIInventory = () => {
{ type="text" placeholder={`Search ${activeTab}...`} value={searchTerm} - onChange={e => { - setSearchTerm(e.target.value); - }} + onChange={e => setSearchTerm(e.target.value)} /> + {tabs.map((tab, index) => ( + +
+ {/* Preserved items notification — only on the Ingredients tab */} + {index === 0 && preservedItems.length > 0 && ( +
+
+

+ + Preserved Stock Available +

+

+ Extended shelf life items for year-round use +

+
+
+

+ {preservedDesc.join(', ')} +

+
+ +
-
- )} -
- {ingredients.map(item => ( -
- -
- ))} -
-
- - -
-
- {equipmentAndSupplies.map(item => ( -
- -
- ))} -
-
-
- -
-
- {seeds.map(item => ( -
- -
- ))} -
-
-
- -
-
- {canningSupplies.map(item => ( -
- -
- ))} -
-
-
- -
-
- {animalSupplies.map(item => ( -
- -
- ))} + )} +
{renderItems(tab)}
-
-
+ + ))}
); diff --git a/src/components/KitchenandInventory/KIInventory/KIInventory.module.css b/src/components/KitchenandInventory/KIInventory/KIInventory.module.css index 8b86a1db68..c932fbad1f 100644 --- a/src/components/KitchenandInventory/KIInventory/KIInventory.module.css +++ b/src/components/KitchenandInventory/KIInventory/KIInventory.module.css @@ -14,6 +14,10 @@ color: black; } +.darkHeader .inventoryText { + color: white; +} + .inventoryPageHeader { display: flex; flex-direction: column; @@ -128,6 +132,14 @@ background-color: rgb(230 225 225); } +.darkSearchBar .clearSearch { + color: #a0a0a0; +} + +.darkSearchBar .clearSearch:hover { + background-color: #333; +} + .addItemButton { background-color: rgb(3 128 3); } @@ -149,6 +161,7 @@ .darkHeader { background-color: #14253b; + color: white; } .darkNavBar { @@ -164,6 +177,12 @@ .darkSearchInput { background-color: #151515 !important; border: none !important; + color: white !important; +} + +.darkSearchInput::placeholder { + color: #a0a0a0 !important; + opacity: 1; } .darkTabContent { diff --git a/src/components/KitchenandInventory/KIInventory/KIItemCard.module.css b/src/components/KitchenandInventory/KIInventory/KIItemCard.module.css index 4c18bbd420..49a51ac1c6 100644 --- a/src/components/KitchenandInventory/KIInventory/KIItemCard.module.css +++ b/src/components/KitchenandInventory/KIInventory/KIItemCard.module.css @@ -124,4 +124,5 @@ background-color: #225163; border: 1px solid #444; box-shadow: -4px 2px 4px #252424; + color: #ffffff; } \ No newline at end of file diff --git a/src/components/KitchenandInventory/MetricCards/MetricCard.module.css b/src/components/KitchenandInventory/MetricCards/MetricCard.module.css index 911b2e890c..7a659d16db 100644 --- a/src/components/KitchenandInventory/MetricCards/MetricCard.module.css +++ b/src/components/KitchenandInventory/MetricCards/MetricCard.module.css @@ -34,6 +34,7 @@ .darkModeCard { background-color: #032c50; box-shadow: 4px 0 10px rgb(0 0 0 / 20%), -4px 0 10px rgb(0 0 0 / 20%); + color: #ffffff; } .darkModeIcon svg{ diff --git a/src/components/common/KitchenandInventory/KIProtectedRoute/KIProtectedRoute.jsx b/src/components/common/KitchenandInventory/KIProtectedRoute/KIProtectedRoute.jsx deleted file mode 100644 index 2ab736eb0d..0000000000 --- a/src/components/common/KitchenandInventory/KIProtectedRoute/KIProtectedRoute.jsx +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint-disable react/jsx-props-no-spreading */ -import { Redirect, Route } from 'react-router-dom'; -import { connect } from 'react-redux'; -import { Suspense } from 'react'; - -// eslint-disable-next-line react/function-component-definition -const KIProtectedRoute = ({ component: Component, render, auth, fallback, ...rest }) => { - return ( - { - if (!auth.isAuthenticated) { - return ; - } - if (auth.user.access && !auth.user.access.canAccessBMPortal) { - return ( - - ); - } - // eslint-disable-next-line no-nested-ternary - return Component && fallback ? ( - - - - } - > - {' '} - {' '} - - ) : Component ? ( - - ) : ( - render(props) - ); - }} - /> - ); -}; - -const mapStateToProps = state => ({ - auth: state.auth, - // Note: roles props won't be used until permissions added to Kitchen and Inventory Dashboard - roles: state.role.roles, -}); - -export default connect(mapStateToProps)(KIProtectedRoute); diff --git a/src/components/common/KitchenandInventory/KIProtectedRoute/index.jsx b/src/components/common/KitchenandInventory/KIProtectedRoute/index.jsx deleted file mode 100644 index cdd162818a..0000000000 --- a/src/components/common/KitchenandInventory/KIProtectedRoute/index.jsx +++ /dev/null @@ -1,3 +0,0 @@ -import KIProtectedRoute from './KIProtectedRoute'; - -export default KIProtectedRoute; diff --git a/src/constants/KIInventoryConstants.js b/src/constants/KIInventoryConstants.js new file mode 100644 index 0000000000..ddaf0dfb7b --- /dev/null +++ b/src/constants/KIInventoryConstants.js @@ -0,0 +1,13 @@ +// Action type constants for KI Inventory feature + +export const KI_INVENTORY_FETCH_REQUEST = 'KI_INVENTORY_FETCH_REQUEST'; +export const KI_INVENTORY_FETCH_SUCCESS = 'KI_INVENTORY_FETCH_SUCCESS'; +export const KI_INVENTORY_FETCH_FAILURE = 'KI_INVENTORY_FETCH_FAILURE'; + +export const KI_INVENTORY_STATS_REQUEST = 'KI_INVENTORY_STATS_REQUEST'; +export const KI_INVENTORY_STATS_SUCCESS = 'KI_INVENTORY_STATS_SUCCESS'; +export const KI_INVENTORY_STATS_FAILURE = 'KI_INVENTORY_STATS_FAILURE'; + +export const KI_PRESERVED_ITEMS_REQUEST = 'KI_PRESERVED_ITEMS_REQUEST'; +export const KI_PRESERVED_ITEMS_SUCCESS = 'KI_PRESERVED_ITEMS_SUCCESS'; +export const KI_PRESERVED_ITEMS_FAILURE = 'KI_PRESERVED_ITEMS_FAILURE'; diff --git a/src/reducers/KIInventoryReducer.js b/src/reducers/KIInventoryReducer.js new file mode 100644 index 0000000000..d637202993 --- /dev/null +++ b/src/reducers/KIInventoryReducer.js @@ -0,0 +1,54 @@ +import { + KI_INVENTORY_FETCH_REQUEST, + KI_INVENTORY_FETCH_SUCCESS, + KI_INVENTORY_FETCH_FAILURE, + KI_INVENTORY_STATS_REQUEST, + KI_INVENTORY_STATS_SUCCESS, + KI_INVENTORY_STATS_FAILURE, + KI_PRESERVED_ITEMS_REQUEST, + KI_PRESERVED_ITEMS_SUCCESS, + KI_PRESERVED_ITEMS_FAILURE, +} from '../constants/KIInventoryConstants'; + +const initialState = { + items: [], + preservedItems: [], + stats: { totalItems: 0, criticalStock: 0, lowStock: 0 }, + loading: false, + statsLoading: false, + preservedLoading: false, + error: null, +}; + +const KIInventoryReducer = (state = initialState, action) => { + switch (action.type) { + // ── Items ────────────────────────────────────────────────────────────── + case KI_INVENTORY_FETCH_REQUEST: + return { ...state, loading: true, error: null }; + case KI_INVENTORY_FETCH_SUCCESS: + return { ...state, loading: false, items: action.payload }; + case KI_INVENTORY_FETCH_FAILURE: + return { ...state, loading: false, error: action.payload }; + + // ── Stats ────────────────────────────────────────────────────────────── + case KI_INVENTORY_STATS_REQUEST: + return { ...state, statsLoading: true }; + case KI_INVENTORY_STATS_SUCCESS: + return { ...state, statsLoading: false, stats: action.payload }; + case KI_INVENTORY_STATS_FAILURE: + return { ...state, statsLoading: false }; + + // ── Preserved Items ──────────────────────────────────────────────────── + case KI_PRESERVED_ITEMS_REQUEST: + return { ...state, preservedLoading: true }; + case KI_PRESERVED_ITEMS_SUCCESS: + return { ...state, preservedLoading: false, preservedItems: action.payload }; + case KI_PRESERVED_ITEMS_FAILURE: + return { ...state, preservedLoading: false }; + + default: + return state; + } +}; + +export default KIInventoryReducer; diff --git a/src/reducers/index.js b/src/reducers/index.js index 484037ee36..87ccbf839f 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -110,6 +110,7 @@ import { studentReducer } from './studentProfileReducer'; import { atomReducer } from './educationPortal/atomReducer'; import { weeklySummariesFiltersApi } from '../actions/weeklySummariesFilterAction'; import browseLessonPlanReducer from './educationPortal/broweLPReducer'; +import KIInventoryReducer from './KIInventoryReducer'; // Kitchen and Inventory Management import { kiCalendarApi } from '../actions/kiCalendarAction'; @@ -206,6 +207,7 @@ const localReducers = { // education portal browseLessonPlan: browseLessonPlanReducer, + kiInventory: KIInventoryReducer, // Kitchen and Inventory Management [kiCalendarApi.reducerPath]: kiCalendarApi.reducer, diff --git a/src/routes.jsx b/src/routes.jsx index a587983d2b..72d50973e6 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -171,7 +171,7 @@ import EventStats from './components/CommunityPortal/EventPersonalization/EventS import CommunityCalendar from './components/CommunityPortal/Calendar/CommunityCalendar'; // Kicthen and Inventory Portal import KitchenandInventoryLogin from './components/KitchenandInventory/Login'; -import KIProtectedRoute from './components/common/KitchenandInventory/KIProtectedRoute'; + import KIDashboard from './components/KitchenandInventory/KIDashboard/KIDashboard'; import RecipesLandingPage from './components/KitchenandInventory/Recipes'; import KIINVENTORY from './components/KitchenandInventory/KIInventory/KIInventory'; @@ -1026,25 +1026,21 @@ export default ( {/* */} {/* ----- END BM Dashboard Routing ----- */} {/* ----- Kitchen and Inventory Portal Routes ----- */} - - - - + + + - - + {/* ----- End of Kitchen and Inventory Portal Routes ----- */} diff --git a/src/utils/URL.js b/src/utils/URL.js index 11fa1247a1..0bd4bf072b 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -665,6 +665,10 @@ export const ENDPOINTS = { HGN_FORM_RESPONSES: () => `${APIEndpoint}/hgnform`, // Kitchen and Inventory Management endpoints KI_CALENDAR_EVENTS: (month, year) => `${APIEndpoint}/kitchenandinventory/calendar?month=${month}&year=${year}`, + KI_INVENTORY_ITEMS: `${APIEndpoint}/kitchenandinventory/inventory/items`, + KI_INVENTORY_STATS: `${APIEndpoint}/kitchenandinventory/inventory/items/stats`, + KI_INVENTORY_PRESERVED: `${APIEndpoint}/kitchenandinventory/inventory/items/ingredients/preserved`, + // Help Request & Feedback Modal endpoints HGN_FORM_RANKED: `${APIEndpoint}/hgnform/ranked`,