Add considering table
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
dvirlabs 2026-04-03 02:41:23 +03:00
parent b8d02c43f8
commit 65a4b82fe5
2 changed files with 338 additions and 33 deletions

View File

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

View File

@ -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 (
<div className="guest-list-container">
<div className="guest-list-container" style={selectedGuestIds.size > 0 ? { paddingBottom: '80px' } : {}}>
<div className="guest-list-header">
{/* ── Row 1: back + title ── */}
<div className="guest-list-header-top">
@ -372,23 +428,6 @@ function GuestList({ eventId, onBack, onShowMembers }) {
</button>
</div>
<div className="btn-group btn-group-primary">
{selectedGuestIds.size > 0 && (
<>
<button
className="btn-delete-selected"
onClick={handleDeleteSelected}
>
{he.deleteSelected} ({selectedGuestIds.size})
</button>
<button
className="btn-whatsapp"
onClick={() => setShowWhatsAppModal(true)}
title={he.selectGuestsFirst}
>
{he.sendWhatsApp} ({selectedGuestIds.size})
</button>
</>
)}
<button className="btn-add-guest" onClick={() => {
setEditingGuest(null)
setShowGuestForm(true)
@ -420,15 +459,44 @@ function GuestList({ eventId, onBack, onShowMembers }) {
</div>
</div>
{selectedGuestIds.size > 0 && (
<div className="selection-bar">
<span className="selection-text">
{he.selectedCount.replace('{count}', selectedGuestIds.size)}
</span>
<SearchFilter eventId={eventId} onSearch={setSearchFilters} />
{considerationIds.size > 0 && (
<div className="consideration-panel">
<div className="consideration-header">
<h3>📋 {he.considerationList} <span className="consideration-count">({considerationIds.size})</span></h3>
<button className="btn-consideration-toggle" onClick={() => setShowConsiderationPanel(p => !p)}>
{showConsiderationPanel ? '▲ הסתר' : '▼ הצג'}
</button>
</div>
{showConsiderationPanel && (
<div className="consideration-list">
{guests.filter(g => considerationIds.has(g.id)).map(guest => (
<div key={guest.id} className="consideration-item">
<div className="consideration-item-info">
<strong>{guest.first_name} {guest.last_name}</strong>
{guest.phone_number && <span className="consideration-phone">{guest.phone_number}</span>}
</div>
<div className="consideration-item-actions">
<button
className="btn-consideration-invite"
onClick={() => handleConsiderationDecision(guest.id, true)}
>
{he.inviteGuest}
</button>
<button
className="btn-consideration-decline"
onClick={() => handleConsiderationDecision(guest.id, false)}
>
{he.notInviteGuest}
</button>
</div>
</div>
))}
</div>
)}
</div>
)}
<SearchFilter eventId={eventId} onSearch={setSearchFilters} />
<div className="pagination-controls">
<label htmlFor="items-per-page">הצג אורחים:</label>
@ -440,7 +508,7 @@ function GuestList({ eventId, onBack, onShowMembers }) {
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
<option value="all">הכל ({filteredGuests.length})</option>
<option value="all">הכל ({sortedGuests.length})</option>
</select>
</div>
@ -470,12 +538,18 @@ function GuestList({ eventId, onBack, onShowMembers }) {
<th className="checkbox-cell">
<input
type="checkbox"
checked={selectedGuestIds.size === (itemsPerPage === 'all' ? filteredGuests : filteredGuests.slice(0, itemsPerPage)).length && (itemsPerPage === 'all' ? filteredGuests : filteredGuests.slice(0, itemsPerPage)).length > 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}
/>
</th>
<th>{he.name}</th>
<th className="sortable-th" onClick={() => setSortOrder(o => {
const next = o === 'asc' ? 'desc' : o === 'desc' ? 'none' : 'asc'
localStorage.setItem(`guestSortOrder_${eventId}`, next)
return next
})} title={he.sortByName}>
{he.name} <span className="sort-icon">{sortOrder === 'asc' ? '↑' : sortOrder === 'desc' ? '↓' : '⇅'}</span>
</th>
<th>{he.phone}</th>
<th>{he.email}</th>
<th>{he.rsvpStatus}</th>
@ -485,7 +559,7 @@ function GuestList({ eventId, onBack, onShowMembers }) {
</tr>
</thead>
<tbody>
{(itemsPerPage === 'all' ? filteredGuests : filteredGuests.slice(0, itemsPerPage)).map(guest => (
{(itemsPerPage === 'all' ? sortedGuests : sortedGuests.slice(0, itemsPerPage)).map(guest => (
<tr key={guest.id} className={selectedGuestIds.has(guest.id) ? 'selected' : ''}>
<td className="checkbox-cell">
<input
@ -545,11 +619,31 @@ function GuestList({ eventId, onBack, onShowMembers }) {
isOpen={showWhatsAppModal}
onClose={() => 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 && (
<div className="sticky-action-bar">
<span className="sticky-selection-count">
{he.selectedCount.replace('{count}', selectedGuestIds.size)}
</span>
<div className="sticky-action-buttons">
<button className="btn-consideration" onClick={handleAddToConsideration}>
{he.addToConsideration}
</button>
<button className="btn-whatsapp" onClick={() => setShowWhatsAppModal(true)}>
💬 {he.sendWhatsApp}
</button>
<button className="btn-delete-selected" onClick={handleDeleteSelected}>
{he.deleteSelected}
</button>
</div>
</div>
)}
</div>
)
}