From f5fc00c5493f545a7df56ea88ac4d7c0d0be1a4e Mon Sep 17 00:00:00 2001 From: charlesjin123 Date: Wed, 8 Jan 2025 22:42:52 +0800 Subject: [PATCH] added filters for kitchen outcomes --- .../src/Visualizations/KitchenOutcomeViz.tsx | 85 +++++++++++++++++-- .../kitchen.outcomes.controller.ts | 21 +++-- server/src/routes/kitchen.outcomes.route.ts | 9 +- .../src/services/kitchen.outcomes.service.ts | 62 ++++++++++++-- 4 files changed, 155 insertions(+), 22 deletions(-) diff --git a/client/src/Visualizations/KitchenOutcomeViz.tsx b/client/src/Visualizations/KitchenOutcomeViz.tsx index 51e798c4..c91576ff 100644 --- a/client/src/Visualizations/KitchenOutcomeViz.tsx +++ b/client/src/Visualizations/KitchenOutcomeViz.tsx @@ -143,6 +143,8 @@ function KitchenOutcomesVisualization() { const [orgName, setOrgName] = useState(''); const [orgId, setOrgId] = useState(''); const [year, setYear] = useState(''); + const [mealType, setMealType] = useState('All'); + const [mealRange, setMealRange] = useState('All'); const tabNames = [ 'Hunger Relief', @@ -150,6 +152,36 @@ function KitchenOutcomesVisualization() { 'Capital Projects', 'Organization Info', ]; + + const mealTypes = [ + 'All', + 'Childcare Meals', + 'School Meals', + 'Soup Kitchen (onsite)', + 'Shelter Meals (offsite)', + 'Meals for Supportive/Transitional Housing', + 'Meals For Seniors', + 'Medically Tailored Meals', + ]; + + const mealRanges = [ + 'All', + '0-100000', + '100000-500000', + '500000-1000000', + '1000000+', + ]; + + const handleMealTypeChange = (event: React.ChangeEvent) => { + setMealType(event.target.value); + }; + + const handleMealRangeChange = ( + event: React.ChangeEvent, + ) => { + setMealRange(event.target.value); + }; + useEffect(() => { const fetchOrgList = async () => { try { @@ -730,7 +762,11 @@ function KitchenOutcomesVisualization() { }, }; - const fetchAllNetworkAverages = async (selectedYear: number) => { + const fetchAllNetworkAverages = async ( + selectedYear: number, + mealTypeFilter: string, + mealRangeFilter: string, + ) => { console.log('Fetching network averages for year:', selectedYear); const fields = [ @@ -754,10 +790,10 @@ function KitchenOutcomesVisualization() { fields.map(async (field) => { try { console.log( - `trying to get network avg route with ${field} ${selectedYear}`, + `trying to get network avg route with ${field} ${selectedYear} ${mealTypeFilter} ${mealRangeFilter}`, ); const response = await getData( - `kitchen_outcomes/network-average/${field}/${selectedYear}`, + `kitchen_outcomes/network-average/${field}/${selectedYear}/${mealTypeFilter}/${mealRangeFilter}`, ); averages[field] = response.data.average; } catch (error) { @@ -770,7 +806,7 @@ function KitchenOutcomesVisualization() { try { console.log('trying to call route with year: ', selectedYear); const response2 = await getData( - `kitchen_outcomes/distri/${selectedYear}`, + `kitchen_outcomes/distri/${selectedYear}/${mealTypeFilter}/${mealRangeFilter}`, ); console.log('response data: ', response2.data); const ageRaceData = response2.data; @@ -789,9 +825,9 @@ function KitchenOutcomesVisualization() { useEffect(() => { if (year) { - fetchAllNetworkAverages(Number(year)); + fetchAllNetworkAverages(Number(year), mealType, mealRange); } - }, [year]); + }, [year, mealType, mealRange]); return ( @@ -846,6 +882,43 @@ function KitchenOutcomesVisualization() { ))} + + + Network Average Filters + + + + + {mealTypes.map((type) => ( + + {type} + + ))} + + + + + {mealRanges.map((range) => ( + + {range} + + ))} + + {/* Tabs - now left justified */} diff --git a/server/src/controllers/kitchen.outcomes.controller.ts b/server/src/controllers/kitchen.outcomes.controller.ts index 46a0993b..2cd4d234 100644 --- a/server/src/controllers/kitchen.outcomes.controller.ts +++ b/server/src/controllers/kitchen.outcomes.controller.ts @@ -20,7 +20,7 @@ const distriController = async ( res: express.Response, next: express.NextFunction, ) => { - const { year } = req.params; + const { year, mealType, mealRange } = req.params; if (!year) { next(ApiError.missingFields(['year'])); @@ -39,7 +39,11 @@ const distriController = async ( year, ); - const ageDistribution = await calculateAgeAndRaceDistributions(yearNum); + const ageDistribution = await calculateAgeAndRaceDistributions( + yearNum, + mealType, + mealRange, + ); res.status(StatusCode.OK).json({ year: yearNum, @@ -61,10 +65,10 @@ const getNetworkAverageController = async ( res: express.Response, next: express.NextFunction, ) => { - const { field, year } = req.params; + const { field, year, mealType, mealRange } = req.params; - if (!field || !year) { - next(ApiError.missingFields(['field', 'year'])); + if (!field || !year || !mealType || !mealRange) { + next(ApiError.missingFields(['field', 'year', 'mealType', 'mealRange'])); return; } @@ -75,7 +79,12 @@ const getNetworkAverageController = async ( return; } - const average = await getNetworkAverage(field, yearNum); + const average = await getNetworkAverage( + field, + yearNum, + mealType, + mealRange, + ); res.status(StatusCode.OK).json({ field, diff --git a/server/src/routes/kitchen.outcomes.route.ts b/server/src/routes/kitchen.outcomes.route.ts index e0816084..2ecee7f2 100644 --- a/server/src/routes/kitchen.outcomes.route.ts +++ b/server/src/routes/kitchen.outcomes.route.ts @@ -16,8 +16,11 @@ import { const router = express.Router(); -router.get('/distri/:year', isAuthenticated, distriController); - +router.get( + '/distri/:year/:mealType/:mealRange', + isAuthenticated, + distriController, +); // router.get('/:year/:orgName', isAuthenticated, getOneKitchenOutcomesController); router.get('/:year/:orgId', isAuthenticated, getOneKitchenOutcomesController); // no authentication for now @@ -40,7 +43,7 @@ router.delete('/delete/:id', isAdmin, deleteKitchenOutcomeByIdController); router.post('/add/', isAuthenticated, addKitchenOutcomesController); router.get( - '/network-average/:field/:year', + '/network-average/:field/:year/:mealType/:mealRange', isAuthenticated, getNetworkAverageController, ); diff --git a/server/src/services/kitchen.outcomes.service.ts b/server/src/services/kitchen.outcomes.service.ts index 4ddd9209..5ccae016 100644 --- a/server/src/services/kitchen.outcomes.service.ts +++ b/server/src/services/kitchen.outcomes.service.ts @@ -4,7 +4,11 @@ import { KitchenOutcomes, } from '../models/kitchen.outcomes.model.ts'; -const calculateAgeAndRaceDistributions = async (year: number) => { +const calculateAgeAndRaceDistributions = async ( + year: number, + mealType: string, + mealRange: string, +) => { try { console.log( 'calculating age and race distribution service for year:', @@ -13,9 +17,28 @@ const calculateAgeAndRaceDistributions = async (year: number) => { const startDate = new Date(Date.UTC(year, 0, 1)); const endDate = new Date(Date.UTC(year + 1, 0, 1)); - const outcomes = await KitchenOutcomes.find({ + const matchConditions: any = { year: { $gte: startDate, $lt: endDate }, - }); + }; + + if (mealType !== 'All') { + matchConditions.typeOfMealsServed = { $in: [mealType] }; + } + + if (mealRange !== 'All') { + if (mealRange.includes('+')) { + const minRange = parseInt(mealRange.replace('+', ''), 10); + matchConditions.hungerReliefsMealsServed = { $gte: minRange }; + } else { + const [minRange, maxRange] = mealRange.split('-').map(Number); + matchConditions.hungerReliefsMealsServed = { + $gte: minRange, + ...(maxRange && { $lte: maxRange }), + }; + } + } + + const outcomes = await KitchenOutcomes.find(matchConditions); let totalMealsAdults = 0; let totalMealsInfants = 0; @@ -124,6 +147,8 @@ export { calculateAgeAndRaceDistributions }; const getNetworkAverage = async ( field: string, year: number, + mealType: string, + mealRange: string, ): Promise => { const startDate = new Date(Date.UTC(year, 0, 1)); const endDate = new Date(Date.UTC(year + 1, 0, 1)); @@ -134,13 +159,36 @@ const getNetworkAverage = async ( year, startDate, endDate, + mealType, + mealRange, }); + const matchConditions: any = { + year: { $gte: startDate, $lt: endDate }, + [field]: { $exists: true, $ne: NaN }, + }; + + if (mealType !== 'All') { + matchConditions.typeOfMealsServed = { $in: [mealType] }; + } + + if (mealRange !== 'All') { + if (mealRange.includes('+')) { + const minRange = parseInt(mealRange.replace('+', ''), 10); + matchConditions.hungerReliefsMealsServed = { $gte: minRange }; + } else { + const [minRange, maxRange] = mealRange.split('-').map(Number); + matchConditions.hungerReliefsMealsServed = { + $gte: minRange, + ...(maxRange && { $lte: maxRange }), + }; + } + } + + console.log('Service - Match conditions:', matchConditions); + const result = await KitchenOutcomes.aggregate([ { - $match: { - year: { $gte: startDate, $lt: endDate }, - [field]: { $exists: true, $ne: NaN }, - }, + $match: matchConditions, }, { $group: {