Edit duplicate component to be able to find duplicate by name or by phone
This commit is contained in:
parent
ed0aedcadd
commit
505104202c
@ -110,34 +110,72 @@ def get_unique_owners(db: Session):
|
|||||||
return sorted(list(owners))
|
return sorted(list(owners))
|
||||||
|
|
||||||
|
|
||||||
def find_duplicate_guests(db: Session):
|
def find_duplicate_guests(db: Session, by: str = "phone"):
|
||||||
"""Find guests with duplicate phone numbers"""
|
"""Find guests with duplicate phone numbers or names"""
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func, and_
|
||||||
|
|
||||||
# Find phone numbers that appear more than once
|
if by == "name":
|
||||||
duplicates = db.query(
|
# Find duplicate full names (first + last name combination)
|
||||||
models.Guest.phone_number,
|
duplicates = db.query(
|
||||||
func.count(models.Guest.id).label('count')
|
models.Guest.first_name,
|
||||||
).filter(
|
models.Guest.last_name,
|
||||||
models.Guest.phone_number.isnot(None),
|
func.count(models.Guest.id).label('count')
|
||||||
models.Guest.phone_number != ''
|
).filter(
|
||||||
).group_by(
|
models.Guest.first_name.isnot(None),
|
||||||
models.Guest.phone_number
|
models.Guest.first_name != '',
|
||||||
).having(
|
models.Guest.last_name.isnot(None),
|
||||||
func.count(models.Guest.id) > 1
|
models.Guest.last_name != ''
|
||||||
).all()
|
).group_by(
|
||||||
|
models.Guest.first_name,
|
||||||
# Get full guest details for each duplicate phone number
|
models.Guest.last_name
|
||||||
result = []
|
).having(
|
||||||
for phone_number, count in duplicates:
|
func.count(models.Guest.id) > 1
|
||||||
guests = db.query(models.Guest).filter(
|
|
||||||
models.Guest.phone_number == phone_number
|
|
||||||
).all()
|
).all()
|
||||||
result.append({
|
|
||||||
'phone_number': phone_number,
|
# Get full guest details for each duplicate name
|
||||||
'count': count,
|
result = []
|
||||||
'guests': guests
|
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
|
return result
|
||||||
|
|
||||||
|
|||||||
@ -104,12 +104,16 @@ def get_owners(db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/guests/duplicates/")
|
@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)
|
if by not in ["phone", "name"]:
|
||||||
return {"duplicates": duplicates}
|
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/")
|
@app.post("/guests/merge/")
|
||||||
|
|||||||
@ -80,8 +80,8 @@ export const updateGuestByPhone = async (phoneNumber, guestData) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Duplicate management
|
// Duplicate management
|
||||||
export const getDuplicates = async () => {
|
export const getDuplicates = async (by = 'phone') => {
|
||||||
const response = await api.get('/guests/duplicates/')
|
const response = await api.get(`/guests/duplicates/?by=${by}`)
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,6 +21,40 @@
|
|||||||
font-size: 1.5rem;
|
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 {
|
.btn-close {
|
||||||
background: #f3f4f6;
|
background: #f3f4f6;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@ -7,15 +7,16 @@ function DuplicateManager({ onUpdate, onClose }) {
|
|||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [selectedKeep, setSelectedKeep] = useState({})
|
const [selectedKeep, setSelectedKeep] = useState({})
|
||||||
const [merging, setMerging] = useState(false)
|
const [merging, setMerging] = useState(false)
|
||||||
|
const [duplicateBy, setDuplicateBy] = useState('phone') // 'phone' or 'name'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadDuplicates()
|
loadDuplicates()
|
||||||
}, [])
|
}, [duplicateBy])
|
||||||
|
|
||||||
const loadDuplicates = async () => {
|
const loadDuplicates = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
const response = await getDuplicates()
|
const response = await getDuplicates(duplicateBy)
|
||||||
setDuplicates(response.duplicates || [])
|
setDuplicates(response.duplicates || [])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading duplicates:', error)
|
console.error('Error loading duplicates:', error)
|
||||||
@ -25,8 +26,8 @@ function DuplicateManager({ onUpdate, onClose }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMerge = async (phoneNumber, guests) => {
|
const handleMerge = async (key, guests) => {
|
||||||
const keepId = selectedKeep[phoneNumber]
|
const keepId = selectedKeep[key]
|
||||||
if (!keepId) {
|
if (!keepId) {
|
||||||
alert('אנא בחר איזה אורח לשמור')
|
alert('אנא בחר איזה אורח לשמור')
|
||||||
return
|
return
|
||||||
@ -66,6 +67,13 @@ function DuplicateManager({ onUpdate, onClose }) {
|
|||||||
<h2>🔍 ניהול כפילויות</h2>
|
<h2>🔍 ניהול כפילויות</h2>
|
||||||
<button className="btn-close" onClick={onClose}>✕</button>
|
<button className="btn-close" onClick={onClose}>✕</button>
|
||||||
</div>
|
</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 className="loading">טוען כפילויות...</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -78,6 +86,13 @@ function DuplicateManager({ onUpdate, onClose }) {
|
|||||||
<h2>🔍 ניהול כפילויות</h2>
|
<h2>🔍 ניהול כפילויות</h2>
|
||||||
<button className="btn-close" onClick={onClose}>✕</button>
|
<button className="btn-close" onClick={onClose}>✕</button>
|
||||||
</div>
|
</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">
|
<div className="no-duplicates">
|
||||||
<p>✅ לא נמצאו כפילויות! כל האורחים ייחודיים.</p>
|
<p>✅ לא נמצאו כפילויות! כל האורחים ייחודיים.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -88,15 +103,23 @@ function DuplicateManager({ onUpdate, onClose }) {
|
|||||||
return (
|
return (
|
||||||
<div className="duplicate-manager" dir="rtl">
|
<div className="duplicate-manager" dir="rtl">
|
||||||
<div className="duplicate-header">
|
<div className="duplicate-header">
|
||||||
<h2>🔍 ניהול כפילויות ({duplicates.length} מספרי טלפון)</h2>
|
<h2>🔍 ניהול כפילויות ({duplicates.length} {duplicateBy === 'phone' ? 'מספרי טלפון' : 'שמות'})</h2>
|
||||||
<button className="btn-close" onClick={onClose}>✕</button>
|
<button className="btn-close" onClick={onClose}>✕</button>
|
||||||
</div>
|
</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">
|
<div className="duplicates-list">
|
||||||
{duplicates.map((dup, index) => (
|
{duplicates.map((dup, index) => (
|
||||||
<div key={index} className="duplicate-group">
|
<div key={index} className="duplicate-group">
|
||||||
<div className="duplicate-info">
|
<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>
|
<span className="count-badge">{dup.count} אורחים</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -104,21 +127,22 @@ function DuplicateManager({ onUpdate, onClose }) {
|
|||||||
{dup.guests.map((guest) => (
|
{dup.guests.map((guest) => (
|
||||||
<div
|
<div
|
||||||
key={guest.id}
|
key={guest.id}
|
||||||
className={`guest-card ${selectedKeep[dup.phone_number] === guest.id ? 'selected' : ''}`}
|
className={`guest-card ${selectedKeep[dup.key] === guest.id ? 'selected' : ''}`}
|
||||||
onClick={() => setSelectedKeep({...selectedKeep, [dup.phone_number]: guest.id})}
|
onClick={() => setSelectedKeep({...selectedKeep, [dup.key]: guest.id})}
|
||||||
>
|
>
|
||||||
<div className="radio-wrapper">
|
<div className="radio-wrapper">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
name={`keep-${dup.phone_number}`}
|
name={`keep-${dup.key}`}
|
||||||
checked={selectedKeep[dup.phone_number] === guest.id}
|
checked={selectedKeep[dup.key] === guest.id}
|
||||||
onChange={() => setSelectedKeep({...selectedKeep, [dup.phone_number]: guest.id})}
|
onChange={() => setSelectedKeep({...selectedKeep, [dup.key]: guest.id})}
|
||||||
/>
|
/>
|
||||||
<label>שמור אורח זה</label>
|
<label>שמור אורח זה</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="guest-details">
|
<div className="guest-details">
|
||||||
<h4>{guest.first_name} {guest.last_name}</h4>
|
<h4>{guest.first_name} {guest.last_name}</h4>
|
||||||
<p><strong>אימייל:</strong> {guest.email || '-'}</p>
|
<p><strong>אימייל:</strong> {guest.email || '-'}</p>
|
||||||
|
<p><strong>טלפון:</strong> {guest.phone_number || '-'}</p>
|
||||||
<p><strong>אישור:</strong> {guest.rsvp_status}</p>
|
<p><strong>אישור:</strong> {guest.rsvp_status}</p>
|
||||||
<p><strong>ארוחה:</strong> {guest.meal_preference || '-'}</p>
|
<p><strong>ארוחה:</strong> {guest.meal_preference || '-'}</p>
|
||||||
<p><strong>פלאס ואן:</strong> {guest.has_plus_one ? `כן${guest.plus_one_name ? ` (${guest.plus_one_name})` : ''}` : 'לא'}</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">
|
<div className="merge-actions">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary"
|
||||||
onClick={() => handleMerge(dup.phone_number, dup.guests)}
|
onClick={() => handleMerge(dup.key, dup.guests)}
|
||||||
disabled={!selectedKeep[dup.phone_number] || merging}
|
disabled={!selectedKeep[dup.key] || merging}
|
||||||
>
|
>
|
||||||
{merging ? 'ממזג...' : `🔗 מזג אורחים`}
|
{merging ? 'ממזג...' : `🔗 מזג אורחים`}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user