diff --git a/Server/src/routes/donor.js b/Server/src/routes/donor.js index 6a0bae2c..00ba3e86 100644 --- a/Server/src/routes/donor.js +++ b/Server/src/routes/donor.js @@ -833,4 +833,54 @@ router.delete('/batch', protect, async (req, res) => { } }); +/** + * Get multiple donors by their IDs + * @name POST /api/donors/batch + * @function + * @memberof module:DonorAPI + * @inner + */ +router.post('/batch', protect, async (req, res) => { + try { + const { donorIds } = req.body; + + if (!donorIds || !Array.isArray(donorIds)) { + return res.status(400).json({ message: 'Invalid donor IDs format' }); + } + + // 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: { + where: { + status: { + in: ['Pending', 'Approved'] + } + } + } + } + }); + + // 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(validDonors) + }); + } 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/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/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}

)}
diff --git a/client/src/services/donorService.js b/client/src/services/donorService.js index f47c38d0..e33cabc2 100644 --- a/client/src/services/donorService.js +++ b/client/src/services/donorService.js @@ -322,56 +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'); - } - - 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) { + 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); + + 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(enrichedDonors, { + return jsonToCsv(donors, { excludeFields: ['eventDonors', 'tags', 'deceased', 'is_deceased'] }); } catch (error) { @@ -587,4 +554,22 @@ 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 result = await fetchWithAuth(`/api/donors/batch`, { + method: 'POST', + body: JSON.stringify({ donorIds }) + }); + return result.donors || []; + } catch (error) { + console.error('Error fetching batch donors:', error); + throw error; + } }; \ No newline at end of file