From 97e09740da0ce4cc8972c1a8b4b08e98db0bd21c Mon Sep 17 00:00:00 2001 From: PCBZ Date: Tue, 22 Apr 2025 21:47:13 -0700 Subject: [PATCH 1/3] use batch get event donors by id --- Server/src/routes/donor.js | 74 +++++++++++++++++++++++++++++ client/src/services/donorService.js | 74 ++++++++++++++--------------- 2 files changed, 110 insertions(+), 38 deletions(-) diff --git a/Server/src/routes/donor.js b/Server/src/routes/donor.js index 6a0bae2c..dd0c9394 100644 --- a/Server/src/routes/donor.js +++ b/Server/src/routes/donor.js @@ -833,4 +833,78 @@ router.delete('/batch', protect, async (req, res) => { } }); +/** + * Get multiple donors by their IDs + * + * @name GET /api/donors/batch + * @function + * @memberof module:DonorAPI + * @inner + * @param {string} req.query.ids - Comma-separated list of donor IDs + * @param {string} req.headers.authorization - Bearer token for authentication + * @returns {Object} 200 - Array of donor details + * @returns {Error} 400 - Invalid donor IDs format + * @returns {Error} 401 - Unauthorized access + * @returns {Error} 500 - Server error + * + * @example + * // Request + * GET /api/donors/batch?ids=1,2,3 + * Authorization: Bearer + * + * // Success Response + * { + * "donors": [ + * { + * "id": "1", + * "constituentId": "D-10023", + * "firstName": "Mei", + * "lastName": "Lee", + * ... + * }, + * ... + * ] + * } + */ +router.get('/batch', protect, async (req, res) => { + try { + const { ids } = req.query; + + if (!ids) { + return res.status(400).json({ message: 'Missing donor IDs parameter' }); + } + + // Split and convert string IDs to numbers + const numericIds = ids.split(',').map(id => { + const numId = parseInt(id.trim()); + if (isNaN(numId)) { + throw new Error(`Invalid donor ID format: ${id}`); + } + return numId; + }); + + const donors = await prisma.donor.findMany({ + where: { + id: { + in: numericIds + } + }, + include: { eventDonors: true } + }); + + // Format the donors with parsed tags + const formattedDonors = donors.map(donor => ({ + ...donor, + tags: donor.tags && Array.isArray(donor.tags) ? donor.tags.map(tag => tag.name) : [] + })); + + res.json({ + donors: formatDonor(formattedDonors) + }); + } catch (error) { + console.error('Error fetching batch donors:', error); + res.status(500).json({ message: 'Internal server error', error: error.message }); + } +}); + export default router; diff --git a/client/src/services/donorService.js b/client/src/services/donorService.js index f47c38d0..4a6edd88 100644 --- a/client/src/services/donorService.js +++ b/client/src/services/donorService.js @@ -330,48 +330,30 @@ export const exportEventDonorsToCsv = async (eventId) => { throw new Error('No donors to export'); } - const donorPromises = eventDonors.map(async (eventDonor) => { - // Get donor ID from eventDonor - const donorId = eventDonor.donor?.id || eventDonor.donor_id || eventDonor.donorId; - - if (!donorId) { - console.warn('Could not find donor ID:', eventDonor); - return null; - } - - // Skip excluded donors - if (eventDonor.status === 'Excluded') { - return null; - } - - try { - // Get complete donor data - const donorData = await fetchWithAuth(`/api/donors/${donorId}`); - - // Skip deceased donors - if (donorData.deceased === true || donorData.is_deceased === true) { - console.log(`Skipping deceased donor ID=${donorId}`); - return null; - } - - // Return only donor data, not event donor relationship data - return donorData; - } catch (error) { - console.error(`Error fetching data for donor ID=${donorId}:`, error.message); - return null; - } - }); - - // Wait for all donor data to be fetched and filter out nulls - let enrichedDonors = await Promise.all(donorPromises); - enrichedDonors = enrichedDonors.filter(donor => donor !== null); - - if (enrichedDonors.length === 0) { + // Filter out excluded donors and collect valid donor IDs + const validDonorIds = eventDonors + .filter(eventDonor => eventDonor.status !== 'Excluded') + .map(eventDonor => eventDonor.donor?.id || eventDonor.donor_id || eventDonor.donorId) + .filter(id => id !== undefined); + + if (validDonorIds.length === 0) { throw new Error('No valid donors to export'); } + // Get all donor data in one batch request + const donors = await getBatchDonors(validDonorIds); + + // Filter out deceased donors + const validDonors = donors.filter(donor => + !donor.deceased && !donor.is_deceased + ); + + if (validDonors.length === 0) { + throw new Error('No valid donors to export after filtering deceased donors'); + } + // Use the generic jsonToCsv function - return jsonToCsv(enrichedDonors, { + return jsonToCsv(validDonors, { excludeFields: ['eventDonors', 'tags', 'deceased', 'is_deceased'] }); } catch (error) { @@ -587,4 +569,20 @@ export const getRecommendedDonors = async (eventId) => { console.error('Error in getRecommendedDonors:', error); throw error; } +}; + +/** + * Get multiple donors by their IDs + * @param {Array} donorIds - Array of donor IDs + * @returns {Promise} Array of donor data + */ +export const getBatchDonors = async (donorIds) => { + try { + const ids = donorIds.join(','); + const result = await fetchWithAuth(`/api/donors/batch?ids=${ids}`); + return result.donors || []; + } catch (error) { + console.error('Error fetching batch donors:', error); + throw error; + } }; \ No newline at end of file From 546b1c31a641f96edaa2a181075df2057eef1eb9 Mon Sep 17 00:00:00 2001 From: PCBZ Date: Tue, 22 Apr 2025 23:09:22 -0700 Subject: [PATCH 2/3] get donor ids from event --- Server/src/routes/donor.js | 72 ++++++++---------------- Server/src/routes/event.js | 85 +++++++++++++++++++++++++++++ client/src/services/donorService.js | 33 ++++------- 3 files changed, 119 insertions(+), 71 deletions(-) diff --git a/Server/src/routes/donor.js b/Server/src/routes/donor.js index dd0c9394..00ba3e86 100644 --- a/Server/src/routes/donor.js +++ b/Server/src/routes/donor.js @@ -835,71 +835,47 @@ router.delete('/batch', protect, async (req, res) => { /** * Get multiple donors by their IDs - * - * @name GET /api/donors/batch + * @name POST /api/donors/batch * @function * @memberof module:DonorAPI * @inner - * @param {string} req.query.ids - Comma-separated list of donor IDs - * @param {string} req.headers.authorization - Bearer token for authentication - * @returns {Object} 200 - Array of donor details - * @returns {Error} 400 - Invalid donor IDs format - * @returns {Error} 401 - Unauthorized access - * @returns {Error} 500 - Server error - * - * @example - * // Request - * GET /api/donors/batch?ids=1,2,3 - * Authorization: Bearer - * - * // Success Response - * { - * "donors": [ - * { - * "id": "1", - * "constituentId": "D-10023", - * "firstName": "Mei", - * "lastName": "Lee", - * ... - * }, - * ... - * ] - * } */ -router.get('/batch', protect, async (req, res) => { +router.post('/batch', protect, async (req, res) => { try { - const { ids } = req.query; + const { donorIds } = req.body; - if (!ids) { - return res.status(400).json({ message: 'Missing donor IDs parameter' }); + if (!donorIds || !Array.isArray(donorIds)) { + return res.status(400).json({ message: 'Invalid donor IDs format' }); } - // Split and convert string IDs to numbers - const numericIds = ids.split(',').map(id => { - const numId = parseInt(id.trim()); - if (isNaN(numId)) { - throw new Error(`Invalid donor ID format: ${id}`); - } - return numId; - }); - + // Convert string IDs to numbers + const numericIds = donorIds.map(id => Number(id)); + + // Get donors with their event donor status const donors = await prisma.donor.findMany({ where: { id: { in: numericIds - } + }, + deceased: false }, - include: { eventDonors: true } + include: { + eventDonors: { + where: { + status: { + in: ['Pending', 'Approved'] + } + } + } + } }); - // Format the donors with parsed tags - const formattedDonors = donors.map(donor => ({ - ...donor, - tags: donor.tags && Array.isArray(donor.tags) ? donor.tags.map(tag => tag.name) : [] - })); + // Filter donors that have at least one pending or approved event donor status + const validDonors = donors.filter(donor => donor.eventDonors.length > 0); + // Format response res.json({ - donors: formatDonor(formattedDonors) + donors: formatDonor(validDonors) }); } catch (error) { console.error('Error fetching batch donors:', error); diff --git a/Server/src/routes/event.js b/Server/src/routes/event.js index 17e7b811..12ea8cea 100644 --- a/Server/src/routes/event.js +++ b/Server/src/routes/event.js @@ -1924,4 +1924,89 @@ router.get('/:eventId/recommended-donors', protect, async (req, res) => { } }); +/** + * Get all donor IDs for a specific event + * + * @name GET /api/events/:id/donor-ids + * @function + * @memberof module:EventAPI + * @inner + * @param {string} req.params.id - Event ID + * @param {string} req.headers.authorization - Bearer token for authentication + * @returns {Object} 200 - Array of donor IDs + * @returns {Error} 400 - Invalid event ID format + * @returns {Error} 401 - Unauthorized access + * @returns {Error} 404 - Event not found + * @returns {Error} 500 - Server error + * + * @example + * // Request + * GET /api/events/1/donor-ids + * Authorization: Bearer + * + * // Success Response + * { + * "donorIds": [1, 2, 3, 4, 5] + * } + */ +router.get('/:id/donor-ids', protect, async (req, res) => { + try { + let eventId; + try { + eventId = parseInt(req.params.id); + if (isNaN(eventId)) { + return res.status(400).json({ message: 'Invalid event ID format' }); + } + } catch (error) { + return res.status(400).json({ message: 'Invalid event ID format' }); + } + + // Verify if event exists + const event = await prisma.event.findUnique({ + where: { id: eventId, isDeleted: false }, + include: { + donorLists: { + select: { + id: true + } + } + } + }); + + if (!event) { + return res.status(404).json({ message: 'Event not found' }); + } + + // Return empty array if event has no donor list + if (!event.donorLists || event.donorLists.length === 0) { + return res.json({ + donorIds: [] + }); + } + + // Get the first donor list ID of the event + const donorListId = event.donorLists[0].id; + + // Get all donor IDs + const eventDonors = await prisma.eventDonor.findMany({ + where: { + donorListId: donorListId + }, + select: { + donorId: true + } + }); + + // Extract donor IDs array + const donorIds = eventDonors.map(ed => ed.donorId); + + res.json({ + donorIds + }); + } catch (error) { + console.error('Error fetching event donor IDs:', error); + res.status(500).json({ message: 'Internal server error', error: error.message }); + } +}); + export default router; \ No newline at end of file diff --git a/client/src/services/donorService.js b/client/src/services/donorService.js index 4a6edd88..e33cabc2 100644 --- a/client/src/services/donorService.js +++ b/client/src/services/donorService.js @@ -322,38 +322,23 @@ export const exportDonorsToCsv = async () => { */ export const exportEventDonorsToCsv = async (eventId) => { try { - // Get event details - const donorsData = await fetchWithAuth(`/api/events/${eventId}/donors`); - let eventDonors = donorsData.donors || []; + // Get donor IDs for the event + const donorIdsData = await fetchWithAuth(`/api/events/${eventId}/donor-ids`); + const validDonorIds = donorIdsData.donorIds || []; - if (eventDonors.length === 0) { - throw new Error('No donors to export'); - } - - // Filter out excluded donors and collect valid donor IDs - const validDonorIds = eventDonors - .filter(eventDonor => eventDonor.status !== 'Excluded') - .map(eventDonor => eventDonor.donor?.id || eventDonor.donor_id || eventDonor.donorId) - .filter(id => id !== undefined); - if (validDonorIds.length === 0) { throw new Error('No valid donors to export'); } // Get all donor data in one batch request const donors = await getBatchDonors(validDonorIds); - - // Filter out deceased donors - const validDonors = donors.filter(donor => - !donor.deceased && !donor.is_deceased - ); - if (validDonors.length === 0) { - throw new Error('No valid donors to export after filtering deceased donors'); + if (donors.length === 0) { + throw new Error('No valid donors to export after filtering deceased and excluded donors'); } // Use the generic jsonToCsv function - return jsonToCsv(validDonors, { + return jsonToCsv(donors, { excludeFields: ['eventDonors', 'tags', 'deceased', 'is_deceased'] }); } catch (error) { @@ -578,8 +563,10 @@ export const getRecommendedDonors = async (eventId) => { */ export const getBatchDonors = async (donorIds) => { try { - const ids = donorIds.join(','); - const result = await fetchWithAuth(`/api/donors/batch?ids=${ids}`); + const result = await fetchWithAuth(`/api/donors/batch`, { + method: 'POST', + body: JSON.stringify({ donorIds }) + }); return result.donors || []; } catch (error) { console.error('Error fetching batch donors:', error); From cfdfe4ecdf76489e1d2293d25178fe4e342e0336 Mon Sep 17 00:00:00 2001 From: PCBZ Date: Tue, 22 Apr 2025 23:14:30 -0700 Subject: [PATCH 3/3] do not display null company in change status --- client/src/components/donors/Donors.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/donors/Donors.jsx b/client/src/components/donors/Donors.jsx index 0a941309..b1151efb 100644 --- a/client/src/components/donors/Donors.jsx +++ b/client/src/components/donors/Donors.jsx @@ -894,7 +894,7 @@ const Donors = () => {

{selectedDonor.donor?.firstName || selectedDonor.firstName} {selectedDonor.donor?.lastName || selectedDonor.lastName}

- {(selectedDonor.donor?.organizationName || selectedDonor.organizationName) && ( + {((selectedDonor.donor?.organizationName || selectedDonor.organizationName)?.trim()) && (

{selectedDonor.donor?.organizationName || selectedDonor.organizationName}

)}