diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 462f7593..0e8a0f3d 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -2,9 +2,7 @@ name: CD on: push: - branches: [ main, FE_Dev, Deploy ] - pull_request: - branches: [ main, FE_Dev, Deploy ] + branches: [ main ] workflow_dispatch: jobs: @@ -51,4 +49,11 @@ jobs: publish_branch: gh-pages commit_message: "Deploy: ${{ github.event.head_commit.message }}" force: true - clean: true \ No newline at end of file + clean: true + + - name: Deploy to Render + run: | + curl -X POST \ + -H "Authorization: Bearer ${{ secrets.RENDER_API_KEY }}" \ + -H "Content-Type: application/json" \ + "https://api.render.com/v1/services/${{ secrets.RENDER_SERVICE_ID }}/deploys" \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 611ada4f..557c2c5e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main, FE_Dev ] + branches: [ main ] pull_request: - branches: [ main, FE_Dev ] + branches: [ main ] workflow_dispatch: jobs: diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index f1f4311a..7779f97b 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -2,9 +2,7 @@ name: Build and Publish Docker Image on: push: - branches: [ main, FE_Dev ] - pull_request: - branches: [ main, FE_Dev ] + branches: [ main ] env: REPO_OWNER: ${{ github.repository_owner }} diff --git a/Server/src/routes/donorList.js b/Server/src/routes/donorList.js index ca9b2e85..1104c838 100644 --- a/Server/src/routes/donorList.js +++ b/Server/src/routes/donorList.js @@ -325,157 +325,117 @@ router.put('/:id/status', protect, async (req, res) => { * @memberof module:DonorListAPI * @param {number} req.params.id - Donor list ID * @param {object} req.body - Request body - * @param {Array} req.body.donors - Array of donors - * @param {number} req.body.donors[].donor_id - Donor ID - * @param {string} req.body.donors[].status - Status ('Pending', 'Approved', 'Excluded', 'AutoExcluded') - * @param {number} [req.body.donors[].reviewer_id] - Reviewer ID (optional) - * @param {string} [req.body.donors[].comments] - Comments (optional) + * @param {Array} req.body.donorIds - Array of donor IDs * @param {string} req.headers.authorization - Bearer token for authentication * @returns {object} 201 - Added donors information * @returns {Error} 400 - Invalid request format * @returns {Error} 404 - List not found * @returns {Error} 500 - Server error - * - * @example Request Example: - * POST /api/lists/1/donors - * Authorization: Bearer - * Content-Type: application/json - * - * { - * "donors": [ - * { - * "donor_id": 301, - * "status": "Pending", - * "comments": "Potential major donor" - * }, - * { - * "donor_id": 302, - * "status": "Approved", - * "comments": "Reliable donor" - * } - * ] - * } - * - * @example Success Response: - * { - * "message": "Donors added successfully.", - * "added_donors": [ - * { - * "id": 201, - * "donorListId": 1, - * "donorId": 301, - * "status": "Pending", - * "comments": "Potential major donor", - * "donor": { - * "id": 301, - * "firstName": "John", - * "lastName": "Doe", - * "totalDonations": 5000, - * "largestGift": 2000 - * } - * }, - * { - * "id": 202, - * "donorListId": 1, - * "donorId": 302, - * "status": "Approved", - * "comments": "Reliable donor", - * "donor": { - * "id": 302, - * "firstName": "Jane", - * "lastName": "Smith", - * "totalDonations": 3000, - * "largestGift": 1000 - * } - * } - * ] - * } */ router.post('/:id/donors', protect, async (req, res) => { try { const listId = parseInt(req.params.id); - const { donors } = req.body; + const { donorIds } = req.body; - if (!Array.isArray(donors)) { - return res.status(400).json({ message: 'Invalid request format: donors must be an array' }); + if (!Array.isArray(donorIds) || donorIds.length === 0) { + return res.status(400).json({ message: 'Invalid donor IDs array' }); } + // 确保所有 ID 都是数字类型 + const numericDonorIds = donorIds.map(id => Number(id)); + if (numericDonorIds.some(isNaN)) { + return res.status(400).json({ message: 'Invalid donor ID format' }); + } + + // Check if list exists const list = await prisma.eventDonorList.findUnique({ where: { id: listId } }); if (!list) { - return res.status(404).json({ message: 'List not found' }); + return res.status(404).json({ message: 'Donor list not found' }); } - // Validate donor IDs exist - const donorIds = donors.map(d => parseInt(d.donor_id)); + // Check if donors exist const existingDonors = await prisma.donor.findMany({ where: { id: { - in: donorIds + in: numericDonorIds } + }, + select: { + id: true } }); - if (existingDonors.length !== donorIds.length) { - return res.status(400).json({ message: 'One or more donor IDs do not exist' }); + const existingDonorIds = existingDonors.map(d => d.id); + const invalidDonorIds = numericDonorIds.filter(id => !existingDonorIds.includes(id)); + + if (invalidDonorIds.length > 0) { + return res.status(400).json({ + message: 'Some donor IDs are invalid', + invalidDonorIds + }); } - const added_donors = await prisma.$transaction( - donors.map(donor => - prisma.eventDonor.create({ - data: { - donorListId: listId, - donorId: parseInt(donor.donor_id), - status: donor.status, - reviewerId: donor.reviewer_id ? parseInt(donor.reviewer_id) : null, - comments: donor.comments - }, - include: { - donor: true - } - }) - ) - ); + // Check for existing donors in the list + const existingListDonors = await prisma.eventDonor.findMany({ + where: { + donorListId: listId, + donorId: { + in: numericDonorIds + } + }, + select: { + donorId: true + } + }); - // Update list statistics - await prisma.eventDonorList.update({ + const existingListDonorIds = existingListDonors.map(d => d.donorId); + const newDonorIds = numericDonorIds.filter(id => !existingListDonorIds.includes(id)); + + if (newDonorIds.length === 0) { + return res.status(400).json({ message: 'All donors are already in the list' }); + } + + // Add new donors to the list + const addedDonors = await prisma.eventDonor.createMany({ + data: newDonorIds.map(donorId => ({ + donorListId: listId, + donorId, + status: 'Pending' + })) + }); + + // Update list counts + const updatedList = await prisma.eventDonorList.update({ where: { id: listId }, data: { totalDonors: { - increment: donors.length + increment: addedDonors.count }, pending: { - increment: donors.filter(d => d.status === 'Pending').length - }, - approved: { - increment: donors.filter(d => d.status === 'Approved').length - }, - excluded: { - increment: donors.filter(d => d.status === 'Excluded').length - }, - autoExcluded: { - increment: donors.filter(d => d.status === 'AutoExcluded').length + increment: addedDonors.count + } + }, + include: { + _count: { + select: { + eventDonors: true + } } } }); - res.status(201).json({ - message: 'Donors added successfully.', - added_donors: added_donors + res.json({ + message: `Successfully added ${addedDonors.count} donors to the list`, + added: addedDonors.count, + totalDonors: updatedList.totalDonors, + pending: updatedList.pending }); } catch (error) { - console.error('Error adding donors:', error); - console.error('Error details:', { - code: error.code, - message: error.message, - meta: error.meta - }); - res.status(500).json({ - message: 'Internal server error', - error: error.message - }); + console.error('Error adding donors to list:', error); + res.status(500).json({ message: 'Internal server error' }); } }); @@ -880,123 +840,4 @@ router.get('/:id/donors', protect, async (req, res) => { } }); -/** - * Add multiple donors to a list - * - * @name POST /api/lists/:id/donors - * @function - * @memberof module:DonorListAPI - * @param {number} req.params.id - Donor list ID - * @param {Array} req.body.donorIds - Array of donor IDs to add - * @param {string} req.headers.authorization - Bearer token for authentication - * @returns {object} 200 - Success message with added donors count - * @returns {Error} 400 - Invalid request data - * @returns {Error} 404 - List not found - * @returns {Error} 500 - Server error - * - * @example Request Example: - * POST /api/lists/1/donors - * Authorization: Bearer - * { - * "donorIds": [1, 2, 3] - * } - * - * @example Success Response: - * { - * "message": "Successfully added 3 donors to the list", - * "added": 3 - * } - */ -router.post('/:id/donors', protect, async (req, res) => { - try { - const listId = parseInt(req.params.id); - const { donorIds } = req.body; - - if (!Array.isArray(donorIds) || donorIds.length === 0) { - return res.status(400).json({ message: 'Invalid donor IDs array' }); - } - - // Check if list exists - const list = await prisma.eventDonorList.findUnique({ - where: { id: listId } - }); - - if (!list) { - return res.status(404).json({ message: 'Donor list not found' }); - } - - // Check if donors exist - const existingDonors = await prisma.donor.findMany({ - where: { - id: { - in: donorIds - } - }, - select: { - id: true - } - }); - - const existingDonorIds = existingDonors.map(d => d.id); - const invalidDonorIds = donorIds.filter(id => !existingDonorIds.includes(id)); - - if (invalidDonorIds.length > 0) { - return res.status(400).json({ - message: 'Some donor IDs are invalid', - invalidDonorIds - }); - } - - // Check for existing donors in the list - const existingListDonors = await prisma.eventDonor.findMany({ - where: { - donorListId: listId, - donorId: { - in: donorIds - } - }, - select: { - donorId: true - } - }); - - const existingListDonorIds = existingListDonors.map(d => d.donorId); - const newDonorIds = donorIds.filter(id => !existingListDonorIds.includes(id)); - - if (newDonorIds.length === 0) { - return res.status(400).json({ message: 'All donors are already in the list' }); - } - - // Add new donors to the list - const addedDonors = await prisma.eventDonor.createMany({ - data: newDonorIds.map(donorId => ({ - donorListId: listId, - donorId, - status: 'Pending' - })) - }); - - // Update list counts - await prisma.eventDonorList.update({ - where: { id: listId }, - data: { - totalDonors: { - increment: addedDonors.count - }, - pending: { - increment: addedDonors.count - } - } - }); - - res.json({ - message: `Successfully added ${addedDonors.count} donors to the list`, - added: addedDonors.count - }); - } catch (error) { - console.error('Error adding donors to list:', error); - res.status(500).json({ message: 'Internal server error' }); - } -}); - export default router; \ No newline at end of file diff --git a/client/src/components/auth/Register.jsx b/client/src/components/auth/Register.jsx index 71978368..e00a95cb 100644 --- a/client/src/components/auth/Register.jsx +++ b/client/src/components/auth/Register.jsx @@ -32,36 +32,6 @@ const Register = () => { } }; - // Form validation - const validateForm = () => { - const newErrors = {}; - - if (!formData.name) { - newErrors.name = 'Please enter your name'; - } else if (formData.name.length < 2) { - newErrors.name = 'Name must be at least 2 characters'; - } - - if (!formData.email) { - newErrors.email = 'Please enter your email'; - } else if (!/\S+@\S+\.\S+/.test(formData.email)) { - newErrors.email = 'Please enter a valid email address'; - } - - if (!formData.password) { - newErrors.password = 'Please enter your password'; - } else if (formData.password.length < 6) { - newErrors.password = 'Password must be at least 6 characters'; - } - - if (!formData.role) { - newErrors.role = 'Please select a role'; - } - - setErrors(newErrors); - return Object.keys(newErrors).length === 0; - }; - // Handle form submission const handleSubmit = async (e) => { e.preventDefault(); diff --git a/client/src/components/donors/AddDonorModal.jsx b/client/src/components/donors/AddDonorModal.jsx index 5ab92253..4963ba0b 100644 --- a/client/src/components/donors/AddDonorModal.jsx +++ b/client/src/components/donors/AddDonorModal.jsx @@ -1,6 +1,6 @@ -import React, { useState, useEffect } from 'react'; -import { FaTimes, FaSearch, FaSync, FaPlus, FaSpinner, FaInfoCircle } from 'react-icons/fa'; -import { addDonorToEvent, addDonorsToList, getAvailableDonors } from '../../services/donorService'; +import React, { useState, useEffect, useCallback } from 'react'; +import { FaTimes, FaSearch, FaSync, FaPlus, FaInfoCircle } from 'react-icons/fa'; +import { addDonorsToList, getAvailableDonors } from '../../services/donorService'; import { ToastContainer, toast } from 'react-toastify'; import 'react-toastify/dist/ReactToastify.css'; import './AddDonorModal.css'; @@ -25,6 +25,29 @@ const AddDonorModal = ({ const [loadingRecommended, setLoadingRecommended] = useState(false); const [filteredRecommendedDonors, setFilteredRecommendedDonors] = useState([]); + const fetchAvailableDonors = useCallback(async (customSearchQuery = searchQuery, pageNumber = currentPage) => { + if (!eventId) return; + + setLoading(true); + setError(null); + + try { + const response = await getAvailableDonors(eventId, { + page: pageNumber, + limit: 10, + search: customSearchQuery + }); + + setAvailableDonors(response.data || []); + setTotalPages(response.total_pages || 1); + } catch (err) { + console.error('Error fetching available donors:', err); + setError(err.message || 'Failed to load available donors'); + } finally { + setLoading(false); + } + }, [eventId, searchQuery, currentPage]); + useEffect(() => { if (isOpen) { setSelectedDonors([]); @@ -32,23 +55,60 @@ const AddDonorModal = ({ setCurrentPage(1); fetchAvailableDonors(); } - }, [isOpen, eventId]); + }, [isOpen, eventId, fetchAvailableDonors]); useEffect(() => { if (isOpen && eventId) { fetchAvailableDonors(); } - }, [currentPage]); + }, [currentPage, isOpen, eventId, fetchAvailableDonors]); + + const fetchRecommendedDonors = useCallback(async () => { + try { + setLoadingRecommended(true); + const token = localStorage.getItem('token'); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await fetch( + `${process.env.REACT_APP_API_URL || 'http://localhost:5001'}/api/events/${eventId}/recommended-donors`, + { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + + // Filter out already participating donors + const filteredDonors = data.recommendedDonors.filter(donor => + !currentEventDonors.some(eventDonor => eventDonor.id === donor.id) + ); + + setRecommendedDonors(filteredDonors); + } catch (err) { + console.error('Error fetching recommended donors:', err); + } finally { + setLoadingRecommended(false); + } + }, [eventId, currentEventDonors]); useEffect(() => { - if (eventId) { + if (eventId && isOpen) { fetchRecommendedDonors(); } - }, [eventId]); + }, [eventId, isOpen, fetchRecommendedDonors]); useEffect(() => { setFilteredRecommendedDonors(recommendedDonors); - }, [recommendedDonors]); + }, [recommendedDonors, fetchRecommendedDonors]); const handleSearch = (e) => { setTempSearchQuery(e.target.value); @@ -97,29 +157,6 @@ const AddDonorModal = ({ } }; - const fetchAvailableDonors = async (customSearchQuery = searchQuery, pageNumber = currentPage) => { - if (!eventId) return; - - setLoading(true); - setError(null); - - try { - const response = await getAvailableDonors(eventId, { - page: pageNumber, - limit: 10, - search: customSearchQuery - }); - - setAvailableDonors(response.data || []); - setTotalPages(response.total_pages || 1); - } catch (err) { - console.error('Error fetching available donors:', err); - setError(err.message || 'Failed to load available donors'); - } finally { - setLoading(false); - } - }; - const handleRefresh = async () => { setIsRefreshing(true); await fetchAvailableDonors(); // Use current searchQuery @@ -135,25 +172,6 @@ const AddDonorModal = ({ }); }; - const handleAddDonor = async (donorId) => { - try { - setLoading(true); - await addDonorToEvent(eventId, donorId); - if (onDonorAdded) { - onDonorAdded(); - } - // Remove the added donor from both available and recommended donors - setAvailableDonors(prev => prev.filter(d => d.id !== donorId)); - setRecommendedDonors(prev => prev.filter(d => d.id !== donorId)); - // Remove from selected donors - setSelectedDonors(prev => prev.filter(d => d.id !== donorId)); - } catch (err) { - setError(err.message || 'Failed to add donor'); - } finally { - setLoading(false); - } - }; - const handleAddMultipleDonors = async () => { if (!eventId || selectedDonors.length === 0) return; @@ -189,8 +207,10 @@ const AddDonorModal = ({ throw new Error('Could not find donor list for this event'); } - // Use batch API to add donors + // Extract donor IDs from selected donors const donorIds = selectedDonors.map(donor => donor.id); + + // Use batch API to add donors const response = await addDonorsToList(donorListId, donorIds); // Notify parent component to update @@ -200,16 +220,16 @@ const AddDonorModal = ({ // Remove added donors from both lists setAvailableDonors(prev => - prev.filter(donor => !donorIds.includes(donor.id)) + prev.filter(donor => !selectedDonors.some(d => d.id === donor.id)) ); setRecommendedDonors(prev => - prev.filter(donor => !donorIds.includes(donor.id)) + prev.filter(donor => !selectedDonors.some(d => d.id === donor.id)) ); // Clear selection setSelectedDonors([]); - toast.success(`Successfully added ${response.added || donorIds.length} donors`); + toast.success(`Successfully added ${response.added || selectedDonors.length} donors`); onClose(); } catch (err) { console.error('Failed to add donors:', err); @@ -232,43 +252,6 @@ const AddDonorModal = ({ } }; - const fetchRecommendedDonors = async () => { - try { - setLoadingRecommended(true); - const token = localStorage.getItem('token'); - if (!token) { - throw new Error('No authentication token found'); - } - - const response = await fetch( - `${process.env.REACT_APP_API_URL || 'http://localhost:5001'}/api/events/${eventId}/recommended-donors`, - { - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json' - } - } - ); - - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - const data = await response.json(); - - // Filter out already participating donors - const filteredDonors = data.recommendedDonors.filter(donor => - !currentEventDonors.some(eventDonor => eventDonor.id === donor.id) - ); - - setRecommendedDonors(filteredDonors); - } catch (err) { - console.error('Error fetching recommended donors:', err); - } finally { - setLoadingRecommended(false); - } - }; - if (!isOpen) return null; return ( diff --git a/client/src/components/donors/AllDonors.jsx b/client/src/components/donors/AllDonors.jsx index 353eda8e..278e5b84 100644 --- a/client/src/components/donors/AllDonors.jsx +++ b/client/src/components/donors/AllDonors.jsx @@ -1,24 +1,17 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { FaSearch, FaSpinner, FaDownload, FaSync, FaCheckCircle, - FaFilter, - FaUpload, - FaEdit, - FaTrash, - FaPlus } from 'react-icons/fa'; import { getAllDonors, exportDonorsToCsv, updateDonor, - importDonors, deleteDonor } from '../../services/donorService'; -import { formatCurrency } from '../../utils/formatters'; import DonorListItem from './DonorListItem'; import EditDonorModal from './EditDonorModal'; import ImportDonors from './ImportDonors'; // Import the component @@ -32,7 +25,7 @@ const AllDonors = () => { const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [totalDonors, setTotalDonors] = useState(0); - const [itemsPerPage, setItemsPerPage] = useState(10); + const [itemsPerPage] = useState(10); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [exporting, setExporting] = useState(false); @@ -42,7 +35,7 @@ const AllDonors = () => { const [isDeleting, setIsDeleting] = useState(false); const [selectedDonor, setSelectedDonor] = useState(null); const [isEditModalOpen, setIsEditModalOpen] = useState(false); - const [importing, setImporting] = useState(false); + const [importing] = useState(false); const navigate = useNavigate(); @@ -58,12 +51,7 @@ const AllDonors = () => { // Remove toggling; the filter panel will always be visible. // (If you still have a toggleFilters function/state elsewhere, ignore it.) - // Fetch all donor data when searchQuery, currentPage, or filters change - useEffect(() => { - fetchDonors(); - }, [searchQuery, currentPage, filters]); - - const fetchDonors = async () => { + const fetchDonors = useCallback(async () => { setLoading(true); setError(null); try { @@ -94,7 +82,11 @@ const AllDonors = () => { } finally { setLoading(false); } - }; + }, [currentPage, itemsPerPage, searchQuery, filters]); + + useEffect(() => { + fetchDonors(); + }, [fetchDonors]); // Filter and search handlers const handleFilterChange = (e) => { diff --git a/client/src/components/donors/DonorList.jsx b/client/src/components/donors/DonorList.jsx index b0a51514..745dcae2 100644 --- a/client/src/components/donors/DonorList.jsx +++ b/client/src/components/donors/DonorList.jsx @@ -7,8 +7,7 @@ const DonorList = ({ onRemove, onStatusUpdate, isEventReady, - loading, - formatDate + loading, }) => { return (
diff --git a/client/src/components/donors/DonorListItem.jsx b/client/src/components/donors/DonorListItem.jsx index 919d828a..0c368775 100644 --- a/client/src/components/donors/DonorListItem.jsx +++ b/client/src/components/donors/DonorListItem.jsx @@ -1,7 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import { FaEdit, FaTrash } from 'react-icons/fa'; -import { formatDonorName, formatCurrency, formatAddress } from '../../utils/formatters'; import './DonorListItem.css'; const DonorListItem = ({ donor, onEdit, onDelete, isDeleting }) => { diff --git a/client/src/components/donors/Donors.jsx b/client/src/components/donors/Donors.jsx index 2bdc3ca9..fff17ac5 100644 --- a/client/src/components/donors/Donors.jsx +++ b/client/src/components/donors/Donors.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from 'react'; -import { FaUser, FaCalendarAlt, FaMapMarkerAlt, FaUsers, FaClock, FaPlus, FaTrash, FaAngleDown, FaSpinner, FaEdit, FaComment, FaDownload, FaSync, FaEnvelope, FaPhone } from 'react-icons/fa'; +import React, { useState, useEffect, useCallback } from 'react'; +import { FaUser, FaPlus, FaAngleDown, FaSpinner, FaDownload} from 'react-icons/fa'; import { getEvents, getEventById, getEventDonors } from '../../services/eventService'; -import { getAvailableDonors, addDonorToEvent, removeDonorFromEvent, getEventDonorStats, updateDonorStatus, updateEventDonor, exportEventDonorsToCsv } from '../../services/donorService'; +import { getAvailableDonors, removeDonorFromEvent, getEventDonorStats, updateEventDonor, exportEventDonorsToCsv } from '../../services/donorService'; import { useLocation } from 'react-router-dom'; import './Donors.css'; import DonorList from './DonorList'; @@ -9,22 +9,12 @@ import EventDetail from './EventDetail'; import AddDonorModal from './AddDonorModal'; -// Temporary workaround to ensure mock data works without authentication -// REMOVE THIS FOR PRODUCTION -const setupMockToken = () => { - if (!localStorage.getItem('token')) { - console.warn('Setting temporary mock token for development'); - localStorage.setItem('token', 'mock-token-for-development-only'); - } -}; - const Donors = () => { const location = useLocation(); const [searchQuery, setSearchQuery] = useState(''); const [currentPage, setCurrentPage] = useState(1); const [selectedEvent, setSelectedEvent] = useState(null); const [showEventDropdown, setShowEventDropdown] = useState(false); - const [availableDonors, setAvailableDonors] = useState([]); const [showAddDonorModal, setShowAddDonorModal] = useState(false); const [events, setEvents] = useState([]); const [eventDonors, setEventDonors] = useState([]); @@ -43,7 +33,7 @@ const Donors = () => { }); const [totalPages, setTotalPages] = useState(1); const [totalDonors, setTotalDonors] = useState(0); - const [itemsPerPage, setItemsPerPage] = useState(10); + const [itemsPerPage] = useState(10); const [success, setSuccess] = useState(''); const [editComments, setEditComments] = useState(''); const [editExcludeReason, setEditExcludeReason] = useState(''); @@ -52,14 +42,10 @@ const Donors = () => { const [editStatus, setEditStatus] = useState('Pending'); const [exporting, setExporting] = useState(false); const [statusFilter, setStatusFilter] = useState(''); - const [isRefreshing, setIsRefreshing] = useState(false); - const [isAddingDonorToList, setIsAddingDonorToList] = useState(null); - const [modalSearchQuery, setModalSearchQuery] = useState(''); - const [modalCurrentPage, setModalCurrentPage] = useState(1); - const [modalTotalPages, setModalTotalPages] = useState(1); - const [modalTotalDonors, setModalTotalDonors] = useState(0); - const [modalItemsPerPage, setModalItemsPerPage] = useState(10); - const [selectedDonors, setSelectedDonors] = useState([]); + const [setModalCurrentPage] = useState(1); + const [setModalTotalPages] = useState(1); + const [setModalTotalDonors] = useState(0); + const [modalItemsPerPage] = useState(10); const [dropdownSearch, setDropdownSearch] = useState(''); const [showExcludeSuggestions, setShowExcludeSuggestions] = useState(false); @@ -73,52 +59,8 @@ const Donors = () => { 'Requested removal' ]; - - // Set up mock token for development - useEffect(() => { - setupMockToken(); - }, []); - - // Fetch events on component mount - useEffect(() => { - fetchEvents(); - }, []); - - // Handle event selection from location state - useEffect(() => { - if (location.state?.selectedEventId) { - const eventId = location.state.selectedEventId; - const event = events.find(e => e.id === eventId); - if (event) { - setSelectedEvent(event); - } else { - // If event not found in current events list, fetch it - getEventById(eventId) - .then(response => { - if (response.data) { - setSelectedEvent(response.data); - } - }) - .catch(error => { - console.error('Error fetching event:', error); - setError(prev => ({ ...prev, events: 'Failed to load selected event' })); - }); - } - } - }, [location.state?.selectedEventId, events]); - - // Fetch donors when selected event changes or search/page changes - useEffect(() => { - if (selectedEvent) { - fetchEventDonors(); - if (!searchQuery) { - fetchEventStats(); - } - } - }, [selectedEvent, searchQuery, currentPage]); - // Fetch events - const fetchEvents = async () => { + const fetchEvents = useCallback(async () => { setLoading(prev => ({ ...prev, events: true })); setError(prev => ({ ...prev, events: null })); @@ -138,19 +80,10 @@ const Donors = () => { } finally { setLoading(prev => ({ ...prev, events: false })); } - }; - - const handleRelatedEventSelect = (event) => { - if (!event || event.id === selectedEvent?.id) return; - - setSelectedEvent(event); - setCurrentPage(1); - setSearchQuery(''); - setStatusFilter(''); - }; + }, [selectedEvent]); // Fetch donors for selected event - const fetchEventDonors = async () => { + const fetchEventDonors = useCallback(async () => { if (!selectedEvent) return; setLoading(prev => ({ ...prev, donors: true })); @@ -282,10 +215,10 @@ const Donors = () => { } finally { setLoading(prev => ({ ...prev, donors: false })); } - }; + }, [selectedEvent, currentPage, itemsPerPage, searchQuery, statusFilter]); - // Fetch event statistics - const fetchEventStats = async () => { + // Fetch event stats + const fetchEventStats = useCallback(async () => { if (!selectedEvent) return; setLoading(prev => ({ ...prev, stats: true })); @@ -293,20 +226,52 @@ const Donors = () => { try { const response = await getEventDonorStats(selectedEvent.id); - setStats({ - pending: response.pending_review || 0, - approved: response.approved || 0, - excluded: response.excluded || 0 - }); + setStats(response.data || { pending: 0, approved: 0, excluded: 0 }); } catch (err) { - console.error('Failed to fetch event statistics:', err); - setError(prev => ({ ...prev, stats: 'Failed to load statistics: ' + (err.message || 'Unknown error') })); - // 设置默认的空统计信息 - setStats({ pending: 0, approved: 0, excluded: 0 }); + console.error('Failed to fetch event stats:', err); + setError(prev => ({ ...prev, stats: 'Failed to load event stats' })); } finally { setLoading(prev => ({ ...prev, stats: false })); } - }; + }, [selectedEvent]); + + // Fetch events on component mount + useEffect(() => { + fetchEvents(); + }, [fetchEvents]); + + // Handle event selection from location state + useEffect(() => { + if (location.state?.selectedEventId) { + const eventId = location.state.selectedEventId; + const event = events.find(e => e.id === eventId); + if (event) { + setSelectedEvent(event); + } else { + // If event not found in current events list, fetch it + getEventById(eventId) + .then(response => { + if (response.data) { + setSelectedEvent(response.data); + } + }) + .catch(error => { + console.error('Error fetching event:', error); + setError(prev => ({ ...prev, events: 'Failed to load selected event' })); + }); + } + } + }, [location.state?.selectedEventId, events]); + + // Fetch donors when selected event changes or search/page changes + useEffect(() => { + if (selectedEvent) { + fetchEventDonors(); + if (!searchQuery) { + fetchEventStats(); + } + } + }, [selectedEvent, searchQuery, currentPage, fetchEventDonors, fetchEventStats]); /** * Open the add donor modal @@ -332,7 +297,6 @@ const Donors = () => { search: '' }); - setAvailableDonors(result.data || []); setModalTotalPages(result.total_pages || 1); setModalTotalDonors(result.total_count || 0); } catch (error) { @@ -372,19 +336,6 @@ const Donors = () => { setLoading(prev => ({ ...prev, donors: false })); } }; - - // Handle close modal - const handleCloseModal = () => { - setShowAddDonorModal(false); - }; - - // 添加关闭添加捐赠者模态框的函数 - const handleCloseAddDonorModal = () => { - setShowAddDonorModal(false); - setModalSearchQuery(''); - setIsAddingDonorToList(null); - }; - // Handle pagination const handlePageChange = async (page) => { if (page < 1 || page > totalPages) return; @@ -470,70 +421,6 @@ const Donors = () => { setSearchQuery(''); // Clear search when changing events }; - /** - * Add a donor to the currently selected event - * @param {number} donorId - The ID of the donor to add - */ - const handleAddDonor = async (donorId) => { - if (!selectedEvent || !donorId) return; - - try { - setLoading(prev => ({ ...prev, donors: true })); - - // Call API to add donor to event - const result = await addDonorToEvent(selectedEvent.id, donorId); - - // If response includes information about newly created list, update UI - if (result.donorList) { - console.log('Donor list created or updated:', result.donorList); - } - - // Update event donor data - await fetchEventDonors(); - await fetchEventStats(); - - // Remove this donor from available donors list - setAvailableDonors(prev => prev.filter(donor => { - const id = donor.id || donor.donor_id || donor.donorId; - return id !== donorId; - })); - - // Show success message - setSuccess('Donor added successfully'); - setTimeout(() => setSuccess(''), 3000); - - // Refresh available donors list - try { - const updatedAvailableDonors = await getAvailableDonors(selectedEvent.id, { - page: 1, - limit: 100 - }); - setAvailableDonors(updatedAvailableDonors.data || []); - } catch (refreshError) { - console.error('Error refreshing available donors:', refreshError); - // Error already handled, but does not affect main process - } - } catch (error) { - console.error('Error adding donor to event:', error); - - // Special handling for donors already in the event - if (error.message && error.message.includes('already in this event')) { - setError(prev => ({ ...prev, donors: 'This donor is already in this event' })); - } else { - setError(prev => ({ ...prev, donors: 'Failed to add donor: ' + (error.message || 'Unknown error') })); - } - - // Refresh donor list to ensure UI consistency - try { - await fetchEventDonors(); - } catch (fetchError) { - console.error('Failed to refresh donors after error:', fetchError); - } - } finally { - setLoading(prev => ({ ...prev, donors: false })); - } - }; - /** * Remove a donor from the currently selected event * @param {number} eventDonorId - The ID of the event donor record to remove @@ -746,166 +633,6 @@ const Donors = () => { } }; - /** - * Refresh available donors list - */ - const handleRefreshAvailableDonors = async () => { - if (!selectedEvent) return; - - setIsRefreshing(true); - - try { - // refresh available donors list - const refreshedDonors = await getAvailableDonors(selectedEvent.id, { - page: 1, - limit: 100 - }); - setAvailableDonors(refreshedDonors.data || []); - } catch (error) { - console.error('Error refreshing available donors:', error); - setError(prev => ({ ...prev, availableDonors: 'Failed to refresh donor list' })); - } finally { - setIsRefreshing(false); - } - }; - - // 添加模态框搜索处理函数 - const handleModalSearch = async (e) => { - const searchValue = e.target.value; - setModalSearchQuery(searchValue); - - if (!selectedEvent) return; - - try { - setLoading(prev => ({ ...prev, availableDonors: true })); - setError(prev => ({ ...prev, availableDonors: null })); - - const result = await getAvailableDonors(selectedEvent.id, { - page: 1, - limit: modalItemsPerPage, - search: searchValue - }); - - setAvailableDonors(result.data || []); - setModalCurrentPage(1); - setModalTotalPages(result.total_pages || 1); - setModalTotalDonors(result.total_count || 0); - } catch (error) { - console.error('Error searching available donors:', error); - setError(prev => ({ ...prev, availableDonors: error.message })); - } finally { - setLoading(prev => ({ ...prev, availableDonors: false })); - } - }; - - // 过滤可用捐赠者列表 - const filteredAvailableDonors = availableDonors.filter(donor => { - if (!donor || typeof donor !== 'object') { - console.warn('Invalid donor object:', donor); - return false; - } - - const searchTerm = modalSearchQuery.toLowerCase(); - - // 安全地访问可能为null的字段 - const firstName = String(donor.firstName || '').toLowerCase(); - const lastName = String(donor.lastName || '').toLowerCase(); - const organizationName = String(donor.organizationName || '').toLowerCase(); - - return firstName.includes(searchTerm) || - lastName.includes(searchTerm) || - organizationName.includes(searchTerm); - }); - - const handleAddDonorToList = async (donor) => { - if (!selectedEvent) return; - - try { - setIsAddingDonorToList(donor.id); - await addDonorToEvent(selectedEvent.id, donor.id); - setSuccess('Donor added successfully!'); - setTimeout(() => setSuccess(''), 3000); - - await Promise.all([ - fetchEventDonors(), - handleRefreshAvailableDonors(), - fetchEventStats() - ]); - } catch (err) { - setError(prev => ({ ...prev, donors: err.message })); - } finally { - setIsAddingDonorToList(null); - } - }; - - const handleModalPageChange = async (newPage) => { - if (!selectedEvent) return; - - try { - setLoading(prev => ({ ...prev, availableDonors: true })); - setError(prev => ({ ...prev, availableDonors: null })); - - const result = await getAvailableDonors(selectedEvent.id, { - page: newPage, - limit: modalItemsPerPage, - search: modalSearchQuery - }); - - setAvailableDonors(result.data || []); - setModalCurrentPage(newPage); - setModalTotalPages(result.total_pages || 1); - setModalTotalDonors(result.total_count || 0); - } catch (error) { - console.error('Error fetching available donors:', error); - setError(prev => ({ ...prev, availableDonors: error.message })); - } finally { - setLoading(prev => ({ ...prev, availableDonors: false })); - } - }; - - // 添加批量添加捐赠者的函数 - const handleAddMultipleDonors = async () => { - if (!selectedEvent || selectedDonors.length === 0) return; - - try { - setLoading(prev => ({ ...prev, donors: true })); - - // 批量添加捐赠者 - await Promise.all(selectedDonors.map(donor => - addDonorToEvent(selectedEvent.id, donor.id) - )); - - setSuccess(`${selectedDonors.length} donors added successfully!`); - setTimeout(() => setSuccess(''), 3000); - - // 更新UI - await Promise.all([ - fetchEventDonors(), - handleRefreshAvailableDonors(), - fetchEventStats() - ]); - - // 清空选中状态 - setSelectedDonors([]); - } catch (err) { - setError(prev => ({ ...prev, donors: err.message })); - } finally { - setLoading(prev => ({ ...prev, donors: false })); - } - }; - - // 添加处理选中状态的函数 - const handleDonorSelect = (donor) => { - setSelectedDonors(prev => { - const isSelected = prev.some(d => d.id === donor.id); - if (isSelected) { - return prev.filter(d => d.id !== donor.id); - } else { - return [...prev, donor]; - } - }); - }; - const handleDonorAdded = async () => { try { // 重新获取活动的捐赠者列表 @@ -925,12 +652,20 @@ const Donors = () => { event.name.toLowerCase().includes(dropdownSearch.toLowerCase()) ); - // 添加处理建议选择的函数 const handleSuggestionSelect = (suggestion) => { setEditExcludeReason(suggestion); setShowExcludeSuggestions(false); }; + const handleRelatedEventSelect = (event) => { + if (!event || event.id === selectedEvent?.id) return; + + setSelectedEvent(event); + setCurrentPage(1); + setSearchQuery(''); + setStatusFilter(''); + }; + return (
diff --git a/client/src/components/donors/EditDonorModal.jsx b/client/src/components/donors/EditDonorModal.jsx index 60215b0d..c8a23fb3 100644 --- a/client/src/components/donors/EditDonorModal.jsx +++ b/client/src/components/donors/EditDonorModal.jsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react'; import PropTypes from 'prop-types'; -import { FaTimes, FaSave, FaSpinner, FaChevronDown, FaChevronUp } from 'react-icons/fa'; +import { FaSave, FaSpinner, FaChevronDown, FaChevronUp } from 'react-icons/fa'; import './EditDonorModal.css'; const EditDonorModal = ({ donor, onSave, onClose, isOpen }) => { diff --git a/client/src/components/events/CreateNewEvent.jsx b/client/src/components/events/CreateNewEvent.jsx index 167d9f01..e06089e4 100644 --- a/client/src/components/events/CreateNewEvent.jsx +++ b/client/src/components/events/CreateNewEvent.jsx @@ -1,7 +1,6 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import './CreateNewEvent.css'; import { createEvent } from '../../services/eventService'; -import authService from '../../services/authService.js'; function CreateNewEvent({ onClose, onEventCreated }) { const [formData, setFormData] = useState({ @@ -34,27 +33,32 @@ function CreateNewEvent({ onClose, onEventCreated }) { startDate: null, endDate: null }); - const calendarRefs = { - date: useRef(null), - startDate: useRef(null), - endDate: useRef(null) - }; + + const dateRef = useRef(null); + const startDateRef = useRef(null); + const endDateRef = useRef(null); + + const calendarRefs = useMemo(() => ({ + date: dateRef, + startDate: startDateRef, + endDate: endDateRef + }), []); + + const handleClickOutside = useCallback((event) => { + Object.keys(calendarRefs).forEach(key => { + if (calendarRefs[key].current && !calendarRefs[key].current.contains(event.target)) { + setShowCalendar(prev => ({ ...prev, [key]: false })); + } + }); + }, [calendarRefs]); // 处理点击外部关闭日历 useEffect(() => { - function handleClickOutside(event) { - Object.keys(calendarRefs).forEach(key => { - if (calendarRefs[key].current && !calendarRefs[key].current.contains(event.target)) { - setShowCalendar(prev => ({ ...prev, [key]: false })); - } - }); - } - document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; - }, []); + }, [handleClickOutside]); // 获取当前月份的日历数据 const getCalendarDays = (date) => { diff --git a/client/src/components/events/EventDetails.jsx b/client/src/components/events/EventDetails.jsx index fc581370..e7df2814 100644 --- a/client/src/components/events/EventDetails.jsx +++ b/client/src/components/events/EventDetails.jsx @@ -1,4 +1,3 @@ -import React from 'react'; import './EventDetails.css'; import { FaCalendarAlt, FaMapMarkerAlt, FaUsers, FaClock } from 'react-icons/fa'; diff --git a/client/src/components/events/EventManagement.jsx b/client/src/components/events/EventManagement.jsx index 0e0437c8..aa0fe1db 100644 --- a/client/src/components/events/EventManagement.jsx +++ b/client/src/components/events/EventManagement.jsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react'; -import { FaCalendarAlt, FaMapMarkerAlt, FaUsers, FaClock, FaTrash, FaFilter, FaSearch, FaEdit, FaEye, FaCheckCircle } from 'react-icons/fa'; +import { FaCalendarAlt, FaMapMarkerAlt, FaUsers, FaClock, FaTrash, FaFilter, FaSearch, FaEdit, FaEye } from 'react-icons/fa'; import { useNavigate } from 'react-router-dom'; import './EventManagement.css'; import '../../styles/common.css'; diff --git a/client/src/services/donorService.js b/client/src/services/donorService.js index 8faefb51..3cbfcc5e 100644 --- a/client/src/services/donorService.js +++ b/client/src/services/donorService.js @@ -1,6 +1,4 @@ // Import mock data from mockData module -// import { MOCK_DONORS, MOCK_EVENT_DONORS, MOCK_EVENT_STATS } from './mockData'; -import { getEventDonors } from './eventService'; const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:5001'; @@ -729,11 +727,7 @@ export const addDonorsToList = async (listId, donorIds) => { throw new Error('No authentication token found'); } - // Convert donorIds array to the required donors array format - const donors = donorIds.map(id => ({ - donor_id: id, - status: 'Pending' - })); + const numericDonorIds = donorIds.map(id => Number(id)); const response = await fetch(`${API_URL}/api/lists/${listId}/donors`, { method: 'POST', @@ -741,7 +735,7 @@ export const addDonorsToList = async (listId, donorIds) => { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, - body: JSON.stringify({ donors }) + body: JSON.stringify({ donorIds: numericDonorIds }) }); if (!response.ok) {