From 65a4b82fe537ddb8d72f21759226bc468440e2f7 Mon Sep 17 00:00:00 2001 From: dvirlabs Date: Fri, 3 Apr 2026 02:41:23 +0300 Subject: [PATCH] Add considering table --- frontend/src/components/GuestList.css | 211 ++++++++++++++++++++++++++ frontend/src/components/GuestList.jsx | 160 +++++++++++++++---- 2 files changed, 338 insertions(+), 33 deletions(-) diff --git a/frontend/src/components/GuestList.css b/frontend/src/components/GuestList.css index bba79a3..c42aed5 100644 --- a/frontend/src/components/GuestList.css +++ b/frontend/src/components/GuestList.css @@ -557,3 +557,214 @@ td { min-width: unset; } } + +/* ── Sortable column header ── */ +.sortable-th { + cursor: pointer; + user-select: none; + white-space: nowrap; +} + +.sortable-th:hover { + background: var(--color-background); + color: var(--color-primary); +} + +.sort-icon { + opacity: 0.6; + font-size: 0.8em; + margin-inline-start: 4px; +} + +/* ── Consideration panel ── */ +.consideration-panel { + background: var(--color-background-secondary); + border: 1px solid var(--color-warning, #e8a838); + border-radius: 8px; + margin-bottom: 1.5rem; + overflow: hidden; +} + +.consideration-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 1rem; + background: rgba(232, 168, 56, 0.1); + border-bottom: 1px solid var(--color-warning, #e8a838); +} + +.consideration-header h3 { + margin: 0; + font-size: 1rem; + color: var(--color-text); +} + +.consideration-count { + color: var(--color-warning, #e8a838); + font-weight: 700; +} + +.btn-consideration-toggle { + background: transparent; + border: 1px solid var(--color-warning, #e8a838); + border-radius: 4px; + padding: 0.25rem 0.75rem; + font-size: 0.8rem; + cursor: pointer; + color: var(--color-text); + transition: background 0.15s; +} + +.btn-consideration-toggle:hover { + background: rgba(232, 168, 56, 0.15); +} + +.consideration-list { + padding: 0.5rem; + display: flex; + flex-direction: column; + gap: 0.4rem; +} + +.consideration-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.5rem 0.75rem; + background: var(--color-background); + border-radius: 6px; + border: 1px solid var(--color-border); +} + +.consideration-item-info { + display: flex; + flex-direction: column; + gap: 0.1rem; +} + +.consideration-phone { + font-size: 0.8rem; + color: var(--color-text-secondary); +} + +.btn-remove-consideration { + background: transparent; + border: 1px solid var(--color-danger); + border-radius: 4px; + color: var(--color-danger); + padding: 0.2rem 0.6rem; + font-size: 0.8rem; + cursor: pointer; + white-space: nowrap; + transition: background 0.15s; +} + +.btn-remove-consideration:hover { + background: var(--color-danger); + color: #fff; +} + +.consideration-item-actions { + display: flex; + gap: 0.4rem; + flex-shrink: 0; +} + +.btn-consideration-invite, +.btn-consideration-decline { + padding: 0.25rem 0.75rem; + border-radius: 4px; + font-size: 0.82rem; + font-weight: 600; + cursor: pointer; + border: none; + white-space: nowrap; + transition: opacity 0.15s, box-shadow 0.15s; +} + +.btn-consideration-invite { + background: var(--color-success, #38a169); + color: #fff; +} + +.btn-consideration-invite:hover { + opacity: 0.85; + box-shadow: 0 2px 6px rgba(56, 161, 105, 0.4); +} + +.btn-consideration-decline { + background: var(--color-danger, #e53e3e); + color: #fff; +} + +.btn-consideration-decline:hover { + opacity: 0.85; + box-shadow: 0 2px 6px rgba(229, 62, 62, 0.4); +} + +/* ── Sticky action bar ── */ +.sticky-action-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + z-index: 1000; + background: var(--color-background-secondary); + border-top: 2px solid var(--color-primary); + box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.2); + padding: 0.75rem 1.5rem; + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; +} + +.sticky-selection-count { + font-weight: 600; + color: var(--color-text); + white-space: nowrap; +} + +.sticky-action-buttons { + display: flex; + gap: 0.5rem; + flex-wrap: wrap; +} + +.btn-consideration { + display: inline-flex; + align-items: center; + height: 38px; + padding: 0 1rem; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + white-space: nowrap; + transition: background 0.2s ease; + background: var(--color-warning, #e8a838); + color: #fff; +} + +.btn-consideration:hover { + background: #cf8f20; + box-shadow: 0 2px 8px rgba(232, 168, 56, 0.4); +} + +@media (max-width: 768px) { + .sticky-action-bar { + flex-direction: column; + align-items: stretch; + gap: 0.5rem; + } + + .sticky-action-buttons { + justify-content: stretch; + } + + .sticky-action-buttons > * { + flex: 1; + } +} diff --git a/frontend/src/components/GuestList.jsx b/frontend/src/components/GuestList.jsx index b57b88f..5fbcebe 100644 --- a/frontend/src/components/GuestList.jsx +++ b/frontend/src/components/GuestList.jsx @@ -49,7 +49,13 @@ const he = { selectGuestsFirst: 'בחר אורחים לשליחת הזמנה', deleteSelected: '🗑️ מחק נבחרים', confirmDeleteSelected: 'האם אתה בטוח שברצונך למחוק {count} אורחים?', - failedToDeleteSelected: 'נכשל במחיקת האורחים הנבחרים' + failedToDeleteSelected: 'נכשל במחיקת האורחים הנבחרים', + addToConsideration: '📋 הוסף לשיקול', + considerationList: 'רשימת שיקול', + removeFromConsideration: 'הסר', + sortByName: 'מיין לפי שם', + inviteGuest: '✅ מזמין', + notInviteGuest: '❌ לא מזמין' } function GuestList({ eventId, onBack, onShowMembers }) { @@ -72,6 +78,12 @@ function GuestList({ eventId, onBack, onShowMembers }) { const [itemsPerPage, setItemsPerPage] = useState(25) const [showWhatsAppModal, setShowWhatsAppModal] = useState(false) const [eventData, setEventData] = useState({}) + const [sortOrder, setSortOrder] = useState(() => { + const saved = localStorage.getItem(`guestSortOrder_${eventId}`) + return saved && ['asc', 'desc', 'none'].includes(saved) ? saved : 'none' + }) // 'none' | 'asc' | 'desc' + const [considerationIds, setConsiderationIds] = useState(new Set()) + const [showConsiderationPanel, setShowConsiderationPanel] = useState(true) useEffect(() => { loadGuests() @@ -171,6 +183,39 @@ function GuestList({ eventId, onBack, onShowMembers }) { } } + const handleAddToConsideration = () => { + setConsiderationIds(prev => new Set([...prev, ...selectedGuestIds])) + setSelectedGuestIds(new Set()) + setShowConsiderationPanel(true) + } + + const handleRemoveFromConsideration = (guestId) => { + setConsiderationIds(prev => { + const next = new Set(prev) + next.delete(guestId) + return next + }) + } + + const handleConsiderationDecision = async (guestId, invite) => { + if (invite) { + try { + await updateGuest(eventId, guestId, { rsvp_status: 'invited' }) + setGuests(prev => prev.map(g => g.id === guestId ? { ...g, rsvp_status: 'invited' } : g)) + } catch (err) { + console.error('Failed to update guest:', err) + } + } else { + try { + await deleteGuest(eventId, guestId) + setGuests(prev => prev.filter(g => g.id !== guestId)) + } catch (err) { + console.error('Failed to delete guest:', err) + } + } + handleRemoveFromConsideration(guestId) + } + const handleDeleteSelected = async () => { if (selectedGuestIds.size === 0) return if (!window.confirm(he.confirmDeleteSelected.replace('{count}', selectedGuestIds.size))) return @@ -201,10 +246,11 @@ function GuestList({ eventId, onBack, onShowMembers }) { } const toggleSelectAll = () => { - if (selectedGuestIds.size === filteredGuests.length) { + const paged = itemsPerPage === 'all' ? sortedGuests : sortedGuests.slice(0, itemsPerPage) + if (selectedGuestIds.size === paged.length && paged.length > 0) { setSelectedGuestIds(new Set()) } else { - setSelectedGuestIds(new Set(filteredGuests.map(g => g.id))) + setSelectedGuestIds(new Set(paged.map(g => g.id))) } } @@ -253,6 +299,16 @@ function GuestList({ eventId, onBack, onShowMembers }) { return true }) + const sortedGuests = sortOrder === 'none' + ? filteredGuests + : [...filteredGuests].sort((a, b) => { + const nameA = `${a.first_name || ''} ${a.last_name || ''}`.toLowerCase() + const nameB = `${b.first_name || ''} ${b.last_name || ''}`.toLowerCase() + return sortOrder === 'asc' + ? nameA.localeCompare(nameB, 'he') + : nameB.localeCompare(nameA, 'he') + }) + const stats = { total: guests.length, confirmed: guests.filter(g => g.rsvp_status === 'confirmed').length, @@ -344,7 +400,7 @@ function GuestList({ eventId, onBack, onShowMembers }) { } return ( -
+
0 ? { paddingBottom: '80px' } : {}}>
{/* ── Row 1: back + title ── */}
@@ -372,23 +428,6 @@ function GuestList({ eventId, onBack, onShowMembers }) {
- {selectedGuestIds.size > 0 && ( - <> - - - - )}
- {selectedGuestIds.size > 0 && ( -
- - {he.selectedCount.replace('{count}', selectedGuestIds.size)} - + + + {considerationIds.size > 0 && ( +
+
+

📋 {he.considerationList} ({considerationIds.size})

+ +
+ {showConsiderationPanel && ( +
+ {guests.filter(g => considerationIds.has(g.id)).map(guest => ( +
+
+ {guest.first_name} {guest.last_name} + {guest.phone_number && {guest.phone_number}} +
+
+ + +
+
+ ))} +
+ )}
)} - -
@@ -470,12 +538,18 @@ function GuestList({ eventId, onBack, onShowMembers }) { 0} + checked={selectedGuestIds.size === (itemsPerPage === 'all' ? sortedGuests : sortedGuests.slice(0, itemsPerPage)).length && (itemsPerPage === 'all' ? sortedGuests : sortedGuests.slice(0, itemsPerPage)).length > 0} onChange={toggleSelectAll} title={he.selectAll} /> - {he.name} + setSortOrder(o => { + const next = o === 'asc' ? 'desc' : o === 'desc' ? 'none' : 'asc' + localStorage.setItem(`guestSortOrder_${eventId}`, next) + return next + })} title={he.sortByName}> + {he.name} {sortOrder === 'asc' ? '↑' : sortOrder === 'desc' ? '↓' : '⇅'} + {he.phone} {he.email} {he.rsvpStatus} @@ -485,7 +559,7 @@ function GuestList({ eventId, onBack, onShowMembers }) { - {(itemsPerPage === 'all' ? filteredGuests : filteredGuests.slice(0, itemsPerPage)).map(guest => ( + {(itemsPerPage === 'all' ? sortedGuests : sortedGuests.slice(0, itemsPerPage)).map(guest => ( setShowWhatsAppModal(false)} selectedGuests={Array.from(selectedGuestIds).map(id => - filteredGuests.find(g => g.id === id) + guests.find(g => g.id === id) ).filter(Boolean)} eventData={eventData} onSend={handleSendWhatsApp} /> + + {/* Sticky action bar — always visible when guests are selected */} + {selectedGuestIds.size > 0 && ( +
+ + {he.selectedCount.replace('{count}', selectedGuestIds.size)} + +
+ + + +
+
+ )}
) }