diff --git a/backend/crud.py b/backend/crud.py index d950be5..411b280 100644 --- a/backend/crud.py +++ b/backend/crud.py @@ -110,34 +110,72 @@ def get_unique_owners(db: Session): return sorted(list(owners)) -def find_duplicate_guests(db: Session): - """Find guests with duplicate phone numbers""" - from sqlalchemy import func +def find_duplicate_guests(db: Session, by: str = "phone"): + """Find guests with duplicate phone numbers or names""" + from sqlalchemy import func, and_ - # Find phone numbers that appear more than once - duplicates = db.query( - models.Guest.phone_number, - func.count(models.Guest.id).label('count') - ).filter( - models.Guest.phone_number.isnot(None), - models.Guest.phone_number != '' - ).group_by( - models.Guest.phone_number - ).having( - func.count(models.Guest.id) > 1 - ).all() - - # Get full guest details for each duplicate phone number - result = [] - for phone_number, count in duplicates: - guests = db.query(models.Guest).filter( - models.Guest.phone_number == phone_number + if by == "name": + # Find duplicate full names (first + last name combination) + duplicates = db.query( + models.Guest.first_name, + models.Guest.last_name, + func.count(models.Guest.id).label('count') + ).filter( + models.Guest.first_name.isnot(None), + models.Guest.first_name != '', + models.Guest.last_name.isnot(None), + models.Guest.last_name != '' + ).group_by( + models.Guest.first_name, + models.Guest.last_name + ).having( + func.count(models.Guest.id) > 1 ).all() - result.append({ - 'phone_number': phone_number, - 'count': count, - 'guests': guests - }) + + # Get full guest details for each duplicate name + result = [] + for first_name, last_name, count in duplicates: + guests = db.query(models.Guest).filter( + and_( + models.Guest.first_name == first_name, + models.Guest.last_name == last_name + ) + ).all() + result.append({ + 'key': f"{first_name} {last_name}", + 'first_name': first_name, + 'last_name': last_name, + 'count': count, + 'guests': guests, + 'type': 'name' + }) + else: # by == "phone" + # Find phone numbers that appear more than once + duplicates = db.query( + models.Guest.phone_number, + func.count(models.Guest.id).label('count') + ).filter( + models.Guest.phone_number.isnot(None), + models.Guest.phone_number != '' + ).group_by( + models.Guest.phone_number + ).having( + func.count(models.Guest.id) > 1 + ).all() + + # Get full guest details for each duplicate phone number + result = [] + for phone_number, count in duplicates: + guests = db.query(models.Guest).filter( + models.Guest.phone_number == phone_number + ).all() + result.append({ + 'key': phone_number, + 'phone_number': phone_number, + 'count': count, + 'guests': guests, + 'type': 'phone' + }) return result diff --git a/backend/main.py b/backend/main.py index c280b1a..ddefb2e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -104,12 +104,16 @@ def get_owners(db: Session = Depends(get_db)): @app.get("/guests/duplicates/") -def get_duplicates(db: Session = Depends(get_db)): +def get_duplicates(by: str = "phone", db: Session = Depends(get_db)): """ - Find guests with duplicate phone numbers + Find guests with duplicate phone numbers or names + by: 'phone' or 'name' - method to find duplicates """ - duplicates = crud.find_duplicate_guests(db) - return {"duplicates": duplicates} + if by not in ["phone", "name"]: + raise HTTPException(status_code=400, detail="Parameter 'by' must be 'phone' or 'name'") + + duplicates = crud.find_duplicate_guests(db, by=by) + return {"duplicates": duplicates, "by": by} @app.post("/guests/merge/") diff --git a/frontend/src/api/api.js b/frontend/src/api/api.js index b71f334..2fa9c3e 100644 --- a/frontend/src/api/api.js +++ b/frontend/src/api/api.js @@ -80,8 +80,8 @@ export const updateGuestByPhone = async (phoneNumber, guestData) => { } // Duplicate management -export const getDuplicates = async () => { - const response = await api.get('/guests/duplicates/') +export const getDuplicates = async (by = 'phone') => { + const response = await api.get(`/guests/duplicates/?by=${by}`) return response.data } diff --git a/frontend/src/components/DuplicateManager.css b/frontend/src/components/DuplicateManager.css index cb6105d..6e36df8 100644 --- a/frontend/src/components/DuplicateManager.css +++ b/frontend/src/components/DuplicateManager.css @@ -21,6 +21,40 @@ font-size: 1.5rem; } +.duplicate-controls { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + padding: 15px; + background: #f9fafb; + border-radius: 8px; + border: 1px solid #e5e7eb; +} + +.duplicate-controls label { + font-weight: 600; + color: #374151; + font-size: 14px; +} + +.duplicate-controls select { + padding: 8px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + background: white; + cursor: pointer; + color: #1f2937; + min-width: 150px; +} + +.duplicate-controls select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + .btn-close { background: #f3f4f6; border: none; diff --git a/frontend/src/components/DuplicateManager.jsx b/frontend/src/components/DuplicateManager.jsx index faeb17e..81f2783 100644 --- a/frontend/src/components/DuplicateManager.jsx +++ b/frontend/src/components/DuplicateManager.jsx @@ -7,15 +7,16 @@ function DuplicateManager({ onUpdate, onClose }) { const [loading, setLoading] = useState(true) const [selectedKeep, setSelectedKeep] = useState({}) const [merging, setMerging] = useState(false) + const [duplicateBy, setDuplicateBy] = useState('phone') // 'phone' or 'name' useEffect(() => { loadDuplicates() - }, []) + }, [duplicateBy]) const loadDuplicates = async () => { try { setLoading(true) - const response = await getDuplicates() + const response = await getDuplicates(duplicateBy) setDuplicates(response.duplicates || []) } catch (error) { console.error('Error loading duplicates:', error) @@ -25,8 +26,8 @@ function DuplicateManager({ onUpdate, onClose }) { } } - const handleMerge = async (phoneNumber, guests) => { - const keepId = selectedKeep[phoneNumber] + const handleMerge = async (key, guests) => { + const keepId = selectedKeep[key] if (!keepId) { alert('אנא בחר איזה אורח לשמור') return @@ -66,6 +67,13 @@ function DuplicateManager({ onUpdate, onClose }) {

🔍 ניהול כפילויות

+
+ + +
טוען כפילויות...
) @@ -78,6 +86,13 @@ function DuplicateManager({ onUpdate, onClose }) {

🔍 ניהול כפילויות

+
+ + +

✅ לא נמצאו כפילויות! כל האורחים ייחודיים.

@@ -88,15 +103,23 @@ function DuplicateManager({ onUpdate, onClose }) { return (
-

🔍 ניהול כפילויות ({duplicates.length} מספרי טלפון)

+

🔍 ניהול כפילויות ({duplicates.length} {duplicateBy === 'phone' ? 'מספרי טלפון' : 'שמות'})

+
+ + +
+
{duplicates.map((dup, index) => (
-

📞 {dup.phone_number}

+

{duplicateBy === 'phone' ? `📞 ${dup.phone_number}` : `👤 ${dup.first_name} ${dup.last_name}`}

{dup.count} אורחים
@@ -104,21 +127,22 @@ function DuplicateManager({ onUpdate, onClose }) { {dup.guests.map((guest) => (
setSelectedKeep({...selectedKeep, [dup.phone_number]: guest.id})} + className={`guest-card ${selectedKeep[dup.key] === guest.id ? 'selected' : ''}`} + onClick={() => setSelectedKeep({...selectedKeep, [dup.key]: guest.id})} >
setSelectedKeep({...selectedKeep, [dup.phone_number]: guest.id})} + name={`keep-${dup.key}`} + checked={selectedKeep[dup.key] === guest.id} + onChange={() => setSelectedKeep({...selectedKeep, [dup.key]: guest.id})} />

{guest.first_name} {guest.last_name}

אימייל: {guest.email || '-'}

+

טלפון: {guest.phone_number || '-'}

אישור: {guest.rsvp_status}

ארוחה: {guest.meal_preference || '-'}

פלאס ואן: {guest.has_plus_one ? `כן${guest.plus_one_name ? ` (${guest.plus_one_name})` : ''}` : 'לא'}

@@ -132,8 +156,8 @@ function DuplicateManager({ onUpdate, onClose }) {