Edit duplicate component to be able to find duplicate by name or by phone

This commit is contained in:
dvirlabs 2026-02-03 16:45:49 +02:00
parent ed0aedcadd
commit 505104202c
5 changed files with 145 additions and 45 deletions

View File

@ -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

View File

@ -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/")

View File

@ -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
}

View File

@ -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;

View File

@ -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 }) {
<h2>🔍 ניהול כפילויות</h2>
<button className="btn-close" onClick={onClose}></button>
</div>
<div className="duplicate-controls">
<label>חפש כפילויות לפי:</label>
<select value={duplicateBy} onChange={(e) => setDuplicateBy(e.target.value)}>
<option value="phone">מספר טלפון</option>
<option value="name">שם מלא</option>
</select>
</div>
<div className="loading">טוען כפילויות...</div>
</div>
)
@ -78,6 +86,13 @@ function DuplicateManager({ onUpdate, onClose }) {
<h2>🔍 ניהול כפילויות</h2>
<button className="btn-close" onClick={onClose}></button>
</div>
<div className="duplicate-controls">
<label>חפש כפילויות לפי:</label>
<select value={duplicateBy} onChange={(e) => setDuplicateBy(e.target.value)}>
<option value="phone">מספר טלפון</option>
<option value="name">שם מלא</option>
</select>
</div>
<div className="no-duplicates">
<p> לא נמצאו כפילויות! כל האורחים ייחודיים.</p>
</div>
@ -88,15 +103,23 @@ function DuplicateManager({ onUpdate, onClose }) {
return (
<div className="duplicate-manager" dir="rtl">
<div className="duplicate-header">
<h2>🔍 ניהול כפילויות ({duplicates.length} מספרי טלפון)</h2>
<h2>🔍 ניהול כפילויות ({duplicates.length} {duplicateBy === 'phone' ? 'מספרי טלפון' : 'שמות'})</h2>
<button className="btn-close" onClick={onClose}></button>
</div>
<div className="duplicate-controls">
<label>חפש כפילויות לפי:</label>
<select value={duplicateBy} onChange={(e) => setDuplicateBy(e.target.value)}>
<option value="phone">מספר טלפון</option>
<option value="name">שם מלא</option>
</select>
</div>
<div className="duplicates-list">
{duplicates.map((dup, index) => (
<div key={index} className="duplicate-group">
<div className="duplicate-info">
<h3>📞 {dup.phone_number}</h3>
<h3>{duplicateBy === 'phone' ? `📞 ${dup.phone_number}` : `👤 ${dup.first_name} ${dup.last_name}`}</h3>
<span className="count-badge">{dup.count} אורחים</span>
</div>
@ -104,21 +127,22 @@ function DuplicateManager({ onUpdate, onClose }) {
{dup.guests.map((guest) => (
<div
key={guest.id}
className={`guest-card ${selectedKeep[dup.phone_number] === guest.id ? 'selected' : ''}`}
onClick={() => setSelectedKeep({...selectedKeep, [dup.phone_number]: guest.id})}
className={`guest-card ${selectedKeep[dup.key] === guest.id ? 'selected' : ''}`}
onClick={() => setSelectedKeep({...selectedKeep, [dup.key]: guest.id})}
>
<div className="radio-wrapper">
<input
type="radio"
name={`keep-${dup.phone_number}`}
checked={selectedKeep[dup.phone_number] === guest.id}
onChange={() => setSelectedKeep({...selectedKeep, [dup.phone_number]: guest.id})}
name={`keep-${dup.key}`}
checked={selectedKeep[dup.key] === guest.id}
onChange={() => setSelectedKeep({...selectedKeep, [dup.key]: guest.id})}
/>
<label>שמור אורח זה</label>
</div>
<div className="guest-details">
<h4>{guest.first_name} {guest.last_name}</h4>
<p><strong>אימייל:</strong> {guest.email || '-'}</p>
<p><strong>טלפון:</strong> {guest.phone_number || '-'}</p>
<p><strong>אישור:</strong> {guest.rsvp_status}</p>
<p><strong>ארוחה:</strong> {guest.meal_preference || '-'}</p>
<p><strong>פלאס ואן:</strong> {guest.has_plus_one ? `כן${guest.plus_one_name ? ` (${guest.plus_one_name})` : ''}` : 'לא'}</p>
@ -132,8 +156,8 @@ function DuplicateManager({ onUpdate, onClose }) {
<div className="merge-actions">
<button
className="btn btn-primary"
onClick={() => handleMerge(dup.phone_number, dup.guests)}
disabled={!selectedKeep[dup.phone_number] || merging}
onClick={() => handleMerge(dup.key, dup.guests)}
disabled={!selectedKeep[dup.key] || merging}
>
{merging ? 'ממזג...' : `🔗 מזג אורחים`}
</button>