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
64 changes: 64 additions & 0 deletions src/actions/KIInventoryActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
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';

/**
* Fetch all inventory items across all categories.
* GET /api/kitchenandinventory/inventory/items
*/
export const fetchInventoryItems = () => async dispatch => {
dispatch({ type: KI_INVENTORY_FETCH_REQUEST });
try {
const res = await axios.get(ENDPOINTS.KI_INVENTORY_ITEMS);
dispatch({ type: KI_INVENTORY_FETCH_SUCCESS, payload: res.data.data });
} catch (err) {
dispatch({
type: KI_INVENTORY_FETCH_FAILURE,
payload: err.response?.data?.message || '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 = () => async dispatch => {
dispatch({ type: KI_INVENTORY_STATS_REQUEST });
try {
const res = await axios.get(ENDPOINTS.KI_INVENTORY_STATS);
dispatch({ type: KI_INVENTORY_STATS_SUCCESS, payload: res.data.data });
} catch (err) {
dispatch({
type: KI_INVENTORY_STATS_FAILURE,
payload: err.response?.data?.message || 'Failed to fetch inventory stats.',
});
}
};

/**
* Fetch preserved ingredient items (expiry >= 1 year from now).
* GET /api/kitchenandinventory/inventory/items/ingredients/preserved
*/
export const fetchPreservedItems = () => async dispatch => {
dispatch({ type: KI_PRESERVED_ITEMS_REQUEST });
try {
const res = await axios.get(ENDPOINTS.KI_INVENTORY_PRESERVED);
dispatch({ type: KI_PRESERVED_ITEMS_SUCCESS, payload: res.data.data });
} catch (err) {
dispatch({
type: KI_PRESERVED_ITEMS_FAILURE,
payload: err.response?.data?.message || 'Failed to fetch preserved items.',
});
}
};
207 changes: 99 additions & 108 deletions src/components/KitchenandInventory/KIInventory/KIInventory.jsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,20 +20,25 @@
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',
Expand All @@ -43,19 +48,36 @@
];
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}`)
: [];

return (
<div className={classnames(styles.inventoryContainer, darkMode ? styles.darkContainer : '')}>
<header className={classnames(styles.inventoryPageHeader, darkMode ? styles.darkHeader : '')}>
Expand All @@ -64,17 +86,21 @@
<p>Track ingredients, equipment, and supplies across all kitchen operations</p>
</div>
<div className={styles.inventoryMetricCards}>
<MetricCard metricname={'Total Items'} metricvalue={totalItems} iconcolor={'#023f80'}>
<MetricCard
metricname={'Total Items'}
metricvalue={stats.totalItems}
iconcolor={'#023f80'}
>
<FiPackage />
</MetricCard>
<MetricCard
metricname={'Critical Stock'}
metricvalue={criticalStock}
metricvalue={stats.criticalStock}
iconcolor={'#ef2d2dff'}
>
<FiAlertCircle />
</MetricCard>
<MetricCard metricname={'Low Stock'} metricvalue={lowStock} iconcolor={'#dea208ff'}>
<MetricCard metricname={'Low Stock'} metricvalue={stats.lowStock} iconcolor={'#dea208ff'}>
<FiAlertTriangle />
</MetricCard>
<MetricCard metricname={'Onsite Grown'} metricvalue={onsiteGrown} iconcolor={'#12ad36ff'}>
Expand Down Expand Up @@ -105,7 +131,7 @@
onClick={() => toggleTab(1)}
>
<FiPackage className={styles.inventoryNavBarIcon} />
Equipment & Supplies
Equipment &amp; Supplies
</NavLink>
</NavItem>
<NavItem>
Expand Down Expand Up @@ -161,9 +187,7 @@
type="text"
placeholder={`Search ${activeTab}...`}
value={searchTerm}
onChange={e => {
setSearchTerm(e.target.value);
}}
onChange={e => setSearchTerm(e.target.value)}
/>
<button className={`${styles.clearSearch}`} onClick={() => setSearchTerm('')}>
x
Expand All @@ -183,89 +207,56 @@
activeTab={activeTab}
className={`${styles.inventoryTabContent} ${darkMode ? styles.darkTabContent : ''}`}
>
<TabPane tabId={tabs[0]}>
<div className={styles.tabContainer}>
{preservedItems.length > 0 && (
<div
className={`${styles.notificationContainer} ${
darkMode ? styles.darkModeNotification : ''
}`}
>
<div className={styles.notificationHeader}>
<p style={{ margin: 0, padding: 0 }}>
<FiArchive style={{ marginRight: '10px' }} />
Preserved Stock Available
</p>
<p style={{ margin: 0, padding: 0, fontSize: 'small' }}>
Extended shelf life items for year-round use
</p>
</div>
<div className={styles.notificationBody}>
<p style={{ color: 'rgb(175, 124, 62)' }}>{preservedDesc.join(', ')}</p>
<div>
<button
className={styles.viewAllButton}
style={darkMode ? { backgroundColor: 'rgb(245, 162, 61)' } : {}}
>
View All
</button>
{tabs.map((tab, index) => (
<TabPane key={tab} tabId={tab}>
<div className={styles.tabContainer}>
{/* Preserved items notification — only on the Ingredients tab */}
{index === 0 && preservedItems.length > 0 && (
<div
className={`${styles.notificationContainer} ${
darkMode ? styles.darkModeNotification : ''
}`}
>
<div className={styles.notificationHeader}>
<p style={{ margin: 0, padding: 0 }}>
<FiArchive style={{ marginRight: '10px' }} />
Preserved Stock Available
</p>
<p style={{ margin: 0, padding: 0, fontSize: 'small' }}>
Extended shelf life items for year-round use
</p>
</div>
<div className={styles.notificationBody}>
<p style={{ color: 'rgb(175, 124, 62)' }}>{preservedDesc.join(', ')}</p>
<div>
<button
className={styles.viewAllButton}
style={darkMode ? { backgroundColor: 'rgb(245, 162, 61)' } : {}}
>
View All
</button>
</div>
</div>
</div>
)}
<div className={styles.ingredientsContainer}>
{loading ? (
<p style={{ padding: '1rem' }}>Loading...</p>
) : tabItems.length > 0 ? (
tabItems.map(item => (
<div key={item._id}>
<KIItemCard item={item} />
</div>
))
) : (
<p style={{ padding: '1rem', opacity: 0.6 }}>
{searchTerm ? `No results for "${searchTerm}"` : `No items in ${tab} yet.`}
</p>
)}

Check warning on line 255 in src/components/KitchenandInventory/KIInventory/KIInventory.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Extract this nested ternary operation into an independent statement.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2IH0kZroWKLK630D0a&open=AZ2IH0kZroWKLK630D0a&pullRequest=5142
</div>
)}
<div className={styles.ingredientsContainer}>
{ingredients.map(item => (
<div key={item._id}>
<KIItemCard item={item} />
</div>
))}
</div>
</div>
</TabPane>
<TabPane tabId={tabs[1]}>
<div className={styles.tabContainer}>
<div className={styles.ingredientsContainer}>
{equipmentAndSupplies.map(item => (
<div key={item._id}>
<KIItemCard item={item} />
</div>
))}
</div>
</div>
</TabPane>
<TabPane tabId={tabs[2]}>
<div className={styles.tabContainer}>
<div className={styles.ingredientsContainer}>
{seeds.map(item => (
<div key={item._id}>
<KIItemCard item={item} />
</div>
))}
</div>
</div>
</TabPane>
<TabPane tabId={tabs[3]}>
<div className={styles.tabContainer}>
<div className={styles.ingredientsContainer}>
{canningSupplies.map(item => (
<div key={item._id}>
<KIItemCard item={item} />
</div>
))}
</div>
</div>
</TabPane>
<TabPane tabId={tabs[4]}>
<div className={styles.tabContainer}>
<div className={styles.ingredientsContainer}>
{animalSupplies.map(item => (
<div key={item._id}>
<KIItemCard item={item} />
</div>
))}
</div>
</div>
</TabPane>
</TabPane>
))}
</TabContent>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@
if (!auth.isAuthenticated) {
return <Redirect to={{ pathname: '/login', state: { from: props.location } }} />;
}
if (auth.user.access && !auth.user.access.canAccessBMPortal) {
return (
<Redirect
to={{ pathname: '/kitchenandinventory/login', state: { from: props.location } }}
/>
);
}
// TODO: Replace with a proper KI-specific permission check (e.g. canAccessKIPortal)

Check warning on line 15 in src/components/common/KitchenandInventory/KIProtectedRoute/KIProtectedRoute.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this "TODO" comment.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2IH0x_roWKLK630D0b&open=AZ2IH0x_roWKLK630D0b&pullRequest=5142
// if (auth.user.access && !auth.user.access.canAccessKIPortal) {
// return (
// <Redirect
// to={{ pathname: '/kitchenandinventory/login', state: { from: props.location } }}
// />
// );
// }
// eslint-disable-next-line no-nested-ternary
return Component && fallback ? (
<Suspense
Expand Down
13 changes: 13 additions & 0 deletions src/constants/KIInventoryConstants.js
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading