admin/components/events/create/steps/Step5GuestList.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

162 lines
4.8 KiB
TypeScript

'use client'
import { useEffect, useMemo, useState } from 'react'
import Modal from '@/components/modals/Modal'
import Input from '@/components/formElements/Input'
import SearchAltIcon from '@/components/icons/SearchAltIcon'
import { coerceToString, formatPersonName } from '@/helpers'
import { addToast } from '@/lib/toast'
import { fetchPreviousAttendees, setPreviousAttendeeGuests, type PreviousAttendee } from '@/services/events'
import { texts } from '@/texts'
interface Step5GuestListProps {
isOpen: boolean
onOpenChange: (open: boolean) => void
eventId: string
attendees: PreviousAttendee[]
onFinish: () => void
}
export default function Step5GuestList({ isOpen, onOpenChange, eventId, attendees, onFinish }: Step5GuestListProps) {
const [search, setSearch] = useState('')
const [selectedIds, setSelectedIds] = useState<string[]>([])
const [isLinking, setIsLinking] = useState(false)
useEffect(() => {
if (!isOpen) return
setSearch('')
setSelectedIds([])
}, [isOpen])
const filteredAttendees = useMemo(() => {
const query = search.trim().toLowerCase()
if (!query) return attendees
return attendees.filter((attendee) => {
const name = formatPersonName(attendee.firstName, attendee.lastName, '').toLowerCase()
return name.includes(query)
})
}, [attendees, search])
const allFilteredSelected = filteredAttendees.length > 0 && filteredAttendees.every((attendee) => selectedIds.includes(attendee.userId))
const toggleAttendee = (userId: string, checked: boolean) => {
setSelectedIds((prev) => (checked ? [...prev, userId] : prev.filter((id) => id !== userId)))
}
const toggleSelectAll = (checked: boolean) => {
const filteredIds = filteredAttendees.map((attendee) => attendee.userId)
if (checked) {
setSelectedIds((prev) => Array.from(new Set([...prev, ...filteredIds])))
return
}
setSelectedIds((prev) => prev.filter((id) => !filteredIds.includes(id)))
}
const handleSendInvitations = async () => {
if (selectedIds.length === 0) {
onOpenChange(false)
onFinish()
return
}
setIsLinking(true)
try {
await setPreviousAttendeeGuests(eventId, selectedIds)
addToast({ title: texts.events.guestsSaved, color: 'success' })
onOpenChange(false)
onFinish()
} catch {
addToast({ title: texts.events.guestsSaveFailed, color: 'danger' })
} finally {
setIsLinking(false)
}
}
return (
<Modal
acceptBtnDisabled={selectedIds.length === 0}
acceptBtnText={texts.events.sendInvitation}
bodyClassName="max-h-[min(60vh,28rem)]"
isLoading={isLinking}
isOpen={isOpen}
rejectBtnText={texts.common.cancel}
size="lg"
title={texts.events.invitePreviousGuestsTitle}
onAccept={() => void handleSendInvitations()}
onClose={onFinish}
onOpenChange={onOpenChange}
>
<div className="flex flex-col gap-4">
<p className="text-sm leading-6 text-secondary-30">{texts.events.invitePreviousGuestsDescription}</p>
<Input
isClearable
ariaLabel={texts.events.guestSearchAria}
generalType="input"
iconStart={<SearchAltIcon className="size-5 text-secondary-30" />}
name="guestSearch"
placeholder={texts.events.guestSearchPlaceholder}
value={search}
onClear={() => {
setSearch('')
}}
onValueChange={(next) => {
setSearch(coerceToString(next))
}}
/>
<Input
generalType="checkbox"
label={texts.events.selectAllGuests}
name="selectAllGuests"
value={allFilteredSelected}
onValueChange={(checked) => {
toggleSelectAll(Boolean(checked))
}}
/>
<div className="flex flex-col gap-2">
{filteredAttendees.length === 0 ? (
<p className="rounded-xl bg-default-100 p-4 text-sm text-secondary-30">{texts.common.emptyChoice}</p>
) : (
filteredAttendees.map((attendee) => (
<div
key={attendee.userId}
className="rounded-xl border border-default-200 bg-white p-3"
>
<Input
generalType="checkbox"
label={formatPersonName(attendee.firstName, attendee.lastName, texts.events.guest)}
name={`guest-${attendee.userId}`}
value={selectedIds.includes(attendee.userId)}
onValueChange={(checked) => {
toggleAttendee(attendee.userId, Boolean(checked))
}}
/>
</div>
))
)}
</div>
</div>
</Modal>
)
}
export async function loadPreviousAttendeesForStep5(): Promise<PreviousAttendee[]> {
try {
return await fetchPreviousAttendees()
} catch {
return []
}
}