diff --git a/src/components/KitchenandInventory/KitchenReports/KitchenReports.jsx b/src/components/KitchenandInventory/KitchenReports/KitchenReports.jsx new file mode 100644 index 00000000000..8d0169b3c3d --- /dev/null +++ b/src/components/KitchenandInventory/KitchenReports/KitchenReports.jsx @@ -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 ( +
+
+
+
+

Reports & Analytics

+

Comprehensive insights on usage, spending, waste, and sustainability

+
+ +
+ + + + + +
+
+ +
+ {REPORT_TABS.map(tab => ( + + ))} +
+ +
+ {activeTabLabel} content coming soon. +
+
+
+ ); +} + +export default KitchenReports; diff --git a/src/components/KitchenandInventory/KitchenReports/KitchenReports.module.css b/src/components/KitchenandInventory/KitchenReports/KitchenReports.module.css new file mode 100644 index 00000000000..110b43921dc --- /dev/null +++ b/src/components/KitchenandInventory/KitchenReports/KitchenReports.module.css @@ -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; +} diff --git a/src/components/KitchenandInventory/KitchenReports/kitchenReportsData.js b/src/components/KitchenandInventory/KitchenReports/kitchenReportsData.js new file mode 100644 index 00000000000..577f52df3ef --- /dev/null +++ b/src/components/KitchenandInventory/KitchenReports/kitchenReportsData.js @@ -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; diff --git a/src/components/KitchenandInventory/KitchenReports/useReportExport.js b/src/components/KitchenandInventory/KitchenReports/useReportExport.js new file mode 100644 index 00000000000..f50d86908e2 --- /dev/null +++ b/src/components/KitchenandInventory/KitchenReports/useReportExport.js @@ -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 }; +} diff --git a/src/routes.jsx b/src/routes.jsx index df47fba3d99..3a90ee43292 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -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'; @@ -1129,6 +1130,7 @@ export default ( exact component={GardenManagement} /> + diff --git a/src/utils/URL.js b/src/utils/URL.js index 9a183aef8c6..0c996f9d8c6 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -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}`,