Edit duplicate component to be able to find duplicate by name or by phone
This commit is contained in:
parent
ed0aedcadd
commit
505104202c
@ -110,10 +110,46 @@ 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_
|
||||
|
||||
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()
|
||||
|
||||
# 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,
|
||||
@ -134,9 +170,11 @@ def find_duplicate_guests(db: Session):
|
||||
models.Guest.phone_number == phone_number
|
||||
).all()
|
||||
result.append({
|
||||
'key': phone_number,
|
||||
'phone_number': phone_number,
|
||||
'count': count,
|
||||
'guests': guests
|
||||
'guests': guests,
|
||||
'type': 'phone'
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@ -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/")
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user