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
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useState } from 'react';
import { useSelector } from 'react-redux';
import styles from './KitchenReports.module.css';
import { REPORT_TABS, DATE_RANGE_OPTIONS, EXPORT_ENDPOINTS_READY } from './kitchenReportsData';
import useReportExport from './useReportExport';

const EXPORT_UNAVAILABLE_MESSAGE =
'Export is unavailable until the backend export endpoints are deployed.';

export function KitchenReports() {
const darkMode = useSelector(state => state.theme.darkMode);
const dm = darkMode ? styles.dark : '';

const [activeTab, setActiveTab] = useState(REPORT_TABS[0].id);
const [dateRange, setDateRange] = useState(DATE_RANGE_OPTIONS[0].value);
const { exportReport, loadingFormat } = useReportExport();

const activeTabLabel = REPORT_TABS.find(tab => tab.id === activeTab)?.label;

return (
<div className={`${styles.pageWrapper} ${dm}`}>
<div className={`${styles.container} ${dm}`}>
<div className={styles.header}>
<div className={styles.titleBlock}>
<h1>Reports &amp; Analytics</h1>
<p>Comprehensive insights on usage, spending, waste, and sustainability</p>
</div>

<div className={styles.controls}>
<select
className={`${styles.dateSelect} ${dm}`}
value={dateRange}
onChange={e => setDateRange(e.target.value)}
aria-label="Select date range"
>
{DATE_RANGE_OPTIONS.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>

<button
type="button"
className={`${styles.exportButton} ${dm}`}
onClick={() => exportReport('pdf', dateRange)}
disabled={!EXPORT_ENDPOINTS_READY || loadingFormat !== null}
title={EXPORT_ENDPOINTS_READY ? undefined : EXPORT_UNAVAILABLE_MESSAGE}
aria-busy={loadingFormat === 'pdf'}
>
{loadingFormat === 'pdf' ? 'Exporting...' : 'Export PDF'}
</button>

<button
type="button"
className={`${styles.exportButton} ${dm}`}
onClick={() => exportReport('csv', dateRange)}
disabled={!EXPORT_ENDPOINTS_READY || loadingFormat !== null}
title={EXPORT_ENDPOINTS_READY ? undefined : EXPORT_UNAVAILABLE_MESSAGE}
aria-busy={loadingFormat === 'csv'}
>
{loadingFormat === 'csv' ? 'Exporting...' : 'Export CSV'}
</button>
</div>
</div>

<div className={`${styles.tabs} ${dm}`} role="tablist">
{REPORT_TABS.map(tab => (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={activeTab === tab.id}
className={`${styles.tabButton} ${dm} ${
activeTab === tab.id ? `${styles.active} ${dm}` : ''
}`}
onClick={() => setActiveTab(tab.id)}
>
{tab.label}
</button>
))}
</div>

<div className={`${styles.tabPanel} ${dm}`} role="tabpanel">
{activeTabLabel} content coming soon.
</div>
</div>
</div>
);
}

export default KitchenReports;
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
.pageWrapper {
min-height: 100vh;
width: 100%;
background-color: #fff;
}

.pageWrapper.dark {
background-color: #1a1816;
}

.container {
max-width: 1200px;
margin: 0 auto;
padding: 24px 16px;
}

.header {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: flex-start;
gap: 16px;
margin-bottom: 20px;
}

.titleBlock h1 {
font-size: 1.5rem;
font-weight: 700;
margin: 0 0 4px;
color: #1f2937;
}

.titleBlock p {
margin: 0;
font-size: 0.9rem;
color: #6b7280;
}

.dark .titleBlock h1 {
color: #f3f4f6;
}

.dark .titleBlock p {
color: #9ca3af;
}

.controls {
display: flex;
align-items: center;
gap: 10px;
flex-wrap: wrap;
}

.dateSelect {
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #d1d5db;
background-color: #fff;
color: #1f2937;
font-size: 0.875rem;
}

.dark .dateSelect {
background-color: #2a2622;
border-color: #4b4642;
color: #f3f4f6;
}

.exportButton {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border-radius: 6px;
border: 1px solid #d1d5db;
background-color: #fff;
color: #1f2937;
font-size: 0.875rem;
cursor: pointer;
}

.exportButton:disabled {
opacity: 0.6;
cursor: not-allowed;
}

.dark .exportButton {
background-color: #2a2622;
border-color: #4b4642;
color: #f3f4f6;
}

.tabs {
display: flex;
flex-wrap: wrap;
gap: 4px;
border-bottom: 1px solid #e5e7eb;
margin-bottom: 20px;
}

.dark .tabs {
border-bottom-color: #3a3632;
}

.tabButton {
padding: 10px 16px;
border: none;
background: none;
font-size: 0.875rem;
font-weight: 500;
color: #4b5563;
cursor: pointer;
border-bottom: 2px solid transparent;
}

.tabButton.active {
color: #047857;
border-bottom-color: #047857;
}

.dark .tabButton {
color: #9ca3af;
}

.dark .tabButton.active {
color: #34d399;
border-bottom-color: #34d399;
}

.tabPanel {
min-height: 200px;
color: #374151;
}

.dark .tabPanel {
color: #d1d5db;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const REPORT_TABS = [
{ id: 'overview', label: 'Overview' },
{ id: 'sustainability', label: 'Sustainability Dashboard' },
{ id: 'preservation', label: 'Preservation Efficiency' },
{ id: 'forecast', label: 'Forecast Accuracy' },
];

export const DATE_RANGE_OPTIONS = [
{ value: 'thisMonth', label: 'This Month' },
{ value: 'lastMonth', label: 'Last Month' },
{ value: 'last3Months', label: 'Last 3 Months' },
{ value: 'thisYear', label: 'This Year' },
];

// Flip to true once the backend /kitchenandinventory/reports/export/* routes exist in HGNRest.
export const EXPORT_ENDPOINTS_READY = false;
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useCallback, useState } from 'react';
import axios from 'axios';
import { toast } from 'react-toastify';
import { ENDPOINTS } from '~/utils/URL';

const EXPORT_CONFIG = {
pdf: {
url: ENDPOINTS.KITCHEN_REPORTS_EXPORT_PDF,
mimeType: 'application/pdf',
fileName: 'report-overview.pdf',
},
csv: {
url: ENDPOINTS.KITCHEN_REPORTS_EXPORT_CSV,
mimeType: 'text/csv',
fileName: 'report-overview.csv',
},
};

export default function useReportExport() {
const [loadingFormat, setLoadingFormat] = useState(null);

const exportReport = useCallback(async (format, dateRange) => {
const config = EXPORT_CONFIG[format];
setLoadingFormat(format);

try {
const response = await axios.get(config.url(), {
params: { dateRange },
responseType: 'blob',
});

if (!response.data || response.data.size === 0) {
throw new Error('Received an empty report file.');
}

const blob = new Blob([response.data], { type: config.mimeType });
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = downloadUrl;
link.download = config.fileName;
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => window.URL.revokeObjectURL(downloadUrl), 100);

toast.success(`${format.toUpperCase()} report downloaded successfully!`);
} catch (error) {
const message = error?.message || `Failed to export ${format.toUpperCase()} report.`;
toast.error(`${message} Please try again.`);
} finally {
setLoadingFormat(null);
}
}, []);

return { exportReport, loadingFormat };
}
2 changes: 2 additions & 0 deletions src/routes.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ import GardenManagement from './components/KitchenandInventory/GardenManagement/
import RecipesLandingPage from './components/KitchenandInventory/Recipes';

import KIDashboard from './components/KitchenandInventory/KIDashboard/KIDashboard';
import KitchenReports from './components/KitchenandInventory/KitchenReports/KitchenReports';
import KIINVENTORY from './components/KitchenandInventory/KIInventory/KIInventory';
import KICalendar from './components/KitchenandInventory/KICalendar/KICalendar';

Expand Down Expand Up @@ -1129,6 +1130,7 @@ export default (
exact
component={GardenManagement}
/>
<ProtectedRoute path="/kitchenandinventory/reports" exact component={KitchenReports} />
<Route path="/kitchenandinventory/orders" component={OrdersPage} />
<Route path="/kitchenandinventory/login" exact component={KitchenandInventoryLogin} />
<ProtectedRoute path="/kitchenandinventory/recipes" exact component={RecipesLandingPage} />
Expand Down
2 changes: 2 additions & 0 deletions src/utils/URL.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export const ENDPOINTS = {
WEEKLY_SUMMARIES_FILTER_REPLACE_INDIVIDUAL_CODES: `${APIEndpoint}/weeklySummariesFilters/replaceIndividualTeamcodes`,
SAVE_SUMMARY_RECEPIENTS: userid => `${APIEndpoint}/reports/recepients/${userid}`,
GET_SUMMARY_RECEPIENTS: () => `${APIEndpoint}/reports/getrecepients`,
KITCHEN_REPORTS_EXPORT_PDF: () => `${APIEndpoint}/kitchenandinventory/reports/export/pdf`,
KITCHEN_REPORTS_EXPORT_CSV: () => `${APIEndpoint}/kitchenandinventory/reports/export/csv`,
GET_CURRENT_WARNINGS: () => `${APIEndpoint}/currentWarnings`,
POST_NEW_WARNING: () => `${APIEndpoint}/currentWarnings`,
UPDATE_WARNING_DESCRIPTION: warningId => `${APIEndpoint}/currentWarnings/${warningId}`,
Expand Down
Loading