Skip to content
Merged
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
25 changes: 19 additions & 6 deletions src/components/BMDashboard/ItemList/ItemListView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function ItemListView({
children,
}) {
const darkMode = useSelector(state => state.theme.darkMode);
const [filteredItems, setFilteredItems] = useState(items);
const [filteredItems, setFilteredItems] = useState(items || []);
const [selectedProject, setSelectedProject] = useState('all');
const [selectedItem, setSelectedItem] = useState('all');
const [isError, setIsError] = useState(false);
Expand Down Expand Up @@ -55,7 +55,7 @@ export function ItemListView({
}, [selectedProject, selectedItem, items]);

useEffect(() => {
setIsError(Object.entries(errors).length > 0);
setIsError(errors ? Object.keys(errors).length > 0 : false);
}, [errors]);

useEffect(() => {
Expand Down Expand Up @@ -204,13 +204,25 @@ export function ItemListView({
)}

<div className={`${styles.buttonsRow}`}>
<button type="button" className={`${styles.btnPrimary}`}>
<button
type="button"
className={`${styles.btnPrimary}`}
onClick={() => console.log('Add Material clicked')}
>
Add Material
</button>
<button type="button" className={`${styles.btnPrimary}`}>
<button
type="button"
className={`${styles.btnPrimary}`}
onClick={() => console.log('Edit Name/Measurement clicked')}
>
Edit Name/Measurement
</button>
<button type="button" className={`${styles.btnPrimary}`}>
<button
type="button"
className={`${styles.btnPrimary}`}
onClick={() => console.log('View Update History clicked')}
>
View Update History
</button>
</div>
Expand Down Expand Up @@ -253,6 +265,7 @@ export function ItemListView({
UpdateItemModal={UpdateItemModal}
dynamicColumns={dynamicColumns}
darkMode={darkMode}
itemType={itemType}
sortConfig={sortConfig}
onSort={handleSort}
totalItems={totalItems}
Expand Down Expand Up @@ -293,7 +306,7 @@ ItemListView.propTypes = {
errors: PropTypes.shape({
message: PropTypes.string,
}),
UpdateItemModal: PropTypes.elementType.isRequired,
UpdateItemModal: PropTypes.elementType,
dynamicColumns: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
Expand Down
165 changes: 117 additions & 48 deletions src/components/BMDashboard/ItemList/ItemsTable.jsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
import { useState } from 'react';
import { Table, Button } from 'reactstrap';
import PropTypes from 'prop-types';
import { Table, Button, Badge } from 'reactstrap';
import { BiPencil } from 'react-icons/bi';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSortDown, faSort, faSortUp } from '@fortawesome/free-solid-svg-icons';
import RecordsModal from './RecordsModal';
import styles from './ItemListView.module.css';

const rowsPerPageOptions = [25, 50, 100];

function generatePageNumbers(current, total) {
if (total <= 7) {
return Array.from({ length: total }, (_, i) => i + 1);
}

if (current <= 3) {
return [1, 2, 3, 4, 5, '...', total];
}

if (current >= total - 2) {
return [1, '...', total - 4, total - 3, total - 2, total - 1, total];
}

if (total <= 7) return Array.from({ length: total }, (_, i) => i + 1);
if (current <= 3) return [1, 2, 3, 4, 5, '...', total];
if (current >= total - 2) return [1, '...', total - 4, total - 3, total - 2, total - 1, total];
return [1, '...', current - 1, current, current + 1, '...', total];
}

Expand All @@ -30,6 +23,7 @@ export default function ItemsTable({
UpdateItemModal,
dynamicColumns,
darkMode = false,
itemType,
sortConfig,
onSort,
totalItems,
Expand Down Expand Up @@ -60,9 +54,8 @@ export default function ItemsTable({
setRecordType(type);
};

const getNestedValue = (obj, path) => {
return path.split('.').reduce((acc, part) => (acc ? acc[part] : null), obj);
};
const getNestedValue = (obj, path) =>
path ? path.split('.').reduce((acc, part) => (acc ? acc[part] : null), obj) : null;

const getIconFor = key => {
if (!sortConfig?.key || sortConfig.key !== key) return faSort;
Expand All @@ -77,6 +70,18 @@ export default function ItemsTable({
Hold: 'hold',
};

const numericKeys = new Set(['stockBought', 'stockUsed', 'stockAvailable', 'stockWasted']);

const getColumnStyle = (key, isAction = false) => {
const base = { verticalAlign: 'middle' };
if (key && numericKeys.has(key)) base.textAlign = 'right';
if (isAction) {
base.borderLeft = '2px solid #dee2e6';
base.textAlign = 'center';
}
return base;
};

return (
<>
<RecordsModal
Expand All @@ -87,53 +92,94 @@ export default function ItemsTable({
recordType={recordType}
itemType={itemType}
/>
<UpdateItemModal modal={updateModal} setModal={setUpdateModal} record={updateRecord} />
{UpdateItemModal && (
<UpdateItemModal modal={updateModal} setModal={setUpdateModal} record={updateRecord} />
)}

<div className={`${styles.itemsTableContainer} ${darkMode ? styles.darkTableWrapper : ''}`}>
<Table className={darkMode ? styles.darkTable : ''}>
<thead className={styles.stickyThead}>
<tr>
<th onClick={() => onSort?.('project')} className={styles.sortableTh}>
<th
onClick={() => onSort?.('project')}
className={styles.sortableTh}
style={{ verticalAlign: 'middle' }}
>
Project <FontAwesomeIcon icon={getIconFor('project')} size="lg" />
</th>

<th onClick={() => onSort?.('name')} className={styles.sortableTh}>
<th
onClick={() => onSort?.('name')}
className={styles.sortableTh}
style={{ verticalAlign: 'middle' }}
>
Name <FontAwesomeIcon icon={getIconFor('name')} size="lg" />
</th>

{dynamicColumns.map(({ label }) => {
{(dynamicColumns || []).map(({ label, key }) => {
const sortKey = dynamicSortKeyByLabel[label];
const clickable = Boolean(sortKey);

return (
<th
key={label}
key={label || key}
onClick={clickable ? () => onSort?.(sortKey) : undefined}
className={clickable ? styles.sortableTh : undefined}
style={getColumnStyle(key)}
>
{label} {clickable && <FontAwesomeIcon icon={getIconFor(sortKey)} size="lg" />}
</th>
);
})}

<th>Usage Record</th>
<th>Updates</th>
<th>Purchases</th>
<th style={getColumnStyle(null, true)} title="View usage history and charts">
Usage Record
</th>
<th
style={{ verticalAlign: 'middle', textAlign: 'center' }}
title="View history of manual updates"
>
Updates
</th>
<th
style={{ verticalAlign: 'middle', textAlign: 'center' }}
title="View procurement history"
>
Purchases
</th>
</tr>
</thead>

<tbody>
{filteredItems && filteredItems.length > 0 ? (
filteredItems.map(el => (
<tr key={el._id}>
<td>{el.project?.name}</td>
<td>{el.itemType?.name}</td>

{dynamicColumns.map(({ label, key }) => (
<td key={label}>{getNestedValue(el, key)}</td>
))}

<td className={`${styles.itemsCell}`}>
<td style={{ verticalAlign: 'middle' }}>{el.project?.name}</td>
<td style={{ verticalAlign: 'middle' }}>{el.itemType?.name}</td>
{(dynamicColumns || []).map(({ label, key }) => {
const value = getNestedValue(el, key);
if (
key === 'stockAvailable' &&
value !== null &&
value !== undefined &&
Number(value) < 10
) {
return (
<td key={label || key} style={getColumnStyle(key)}>
<Badge
color="danger"
pill
className="me-2"
style={{ marginRight: '8px' }}
>
Low
</Badge>
{value}
</td>
);
}
return (
<td key={label || key} style={getColumnStyle(key)}>
{value}
</td>
);
})}
<td className={styles.itemsCell} style={getColumnStyle(null, true)}>
<button
type="button"
onClick={() => handleEditRecordsClick(el, 'UsageRecord')}
Expand All @@ -150,8 +196,10 @@ export default function ItemsTable({
View
</Button>
</td>

<td className={`${styles.itemsCell}`}>
<td
className={styles.itemsCell}
style={{ verticalAlign: 'middle', textAlign: 'center' }}
>
<button
type="button"
onClick={() => handleEditRecordsClick(el, 'Update')}
Expand All @@ -168,8 +216,7 @@ export default function ItemsTable({
View
</Button>
</td>

<td>
<td style={{ verticalAlign: 'middle', textAlign: 'center' }}>
<Button
color="primary"
outline
Expand All @@ -183,7 +230,7 @@ export default function ItemsTable({
))
) : (
<tr>
<td colSpan={dynamicColumns.length + 5} style={{ textAlign: 'center' }}>
<td colSpan={(dynamicColumns?.length || 0) + 5} style={{ textAlign: 'center' }}>
No items data
</td>
</tr>
Expand All @@ -206,24 +253,20 @@ export default function ItemsTable({
))}
</select>
</div>

<div className={styles.rangeInfo}>
{startRow}-{endRow} of {totalItems}
</div>

<div className={styles.pageButtons}>
<button type="button" onClick={() => onPageChange?.(1)} disabled={currentPage === 1}>
{'<<'}
</button>

<button
type="button"
onClick={() => onPageChange?.(currentPage - 1)}
disabled={currentPage === 1}
>
{'<'}
</button>

{generatePageNumbers(currentPage, totalPages).map((p, idx) =>
typeof p === 'number' ? (
<button
Expand All @@ -241,15 +284,13 @@ export default function ItemsTable({
</span>
),
)}

<button
type="button"
onClick={() => onPageChange?.(currentPage + 1)}
disabled={currentPage === totalPages}
>
{'>'}
</button>

<button
type="button"
onClick={() => onPageChange?.(totalPages)}
Expand All @@ -262,3 +303,31 @@ export default function ItemsTable({
</>
);
}

ItemsTable.propTypes = {
selectedProject: PropTypes.string,
selectedItem: PropTypes.string,
filteredItems: PropTypes.arrayOf(PropTypes.object),
UpdateItemModal: PropTypes.elementType,
dynamicColumns: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string,
key: PropTypes.string,
}),
).isRequired,
darkMode: PropTypes.bool,
itemType: PropTypes.string,
sortConfig: PropTypes.shape({
key: PropTypes.string,
direction: PropTypes.string,
}),
onSort: PropTypes.func,
totalItems: PropTypes.number,
currentPage: PropTypes.number,
totalPages: PropTypes.number,
rowsPerPage: PropTypes.number,
startRow: PropTypes.number,
endRow: PropTypes.number,
onPageChange: PropTypes.func,
onRowsPerPageChange: PropTypes.func,
};
5 changes: 5 additions & 0 deletions src/components/BMDashboard/ItemList/RecordsModal.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,8 @@
color: #e6a800;
font-weight: bold;
}

.darkTable .stickyThead th {
background-color: #2f4157;
border-bottom: 1px solid #3f5269;
}
Loading
Loading