Wire manage-events/new with host picker and create flow against the new admin create API, including entry points from the events list and user detail.
244 lines
6.8 KiB
TypeScript
244 lines
6.8 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
import { useRouter } from 'next/navigation'
|
|
|
|
import type { PaginationListColumnType } from '@/types'
|
|
import PaginatedList from '@/components/PaginatedList'
|
|
import PageNavbar from '@/components/layouts/PageNavbar'
|
|
import AdminTableViewButton from '@/components/ui/AdminTableViewButton'
|
|
import StatusChip from '@/components/ui/StatusChip'
|
|
import axiosInstance from '@/config/axios'
|
|
import { APP_ROUTES } from '@/constants/routes'
|
|
import { EVENT_STATUS_FILTER_ITEMS, getBooleanStatus, getEventStatus } from '@/constants/status'
|
|
import { formatCurrency, formatPersonName, coerceToString } from '@/helpers'
|
|
import { formatPersianDate } from '@/lib/formatters'
|
|
import { API_ROUTES } from '@/services/config'
|
|
|
|
interface NamedEntity {
|
|
id: number | string
|
|
name: string
|
|
}
|
|
|
|
interface EventOrganizer {
|
|
id: string
|
|
firstName: string | null
|
|
lastName: string | null
|
|
}
|
|
|
|
interface EventRow {
|
|
id: string
|
|
title: string
|
|
status: string
|
|
isDiscoverable: boolean
|
|
isFree: boolean
|
|
price: number
|
|
category?: NamedEntity
|
|
province?: NamedEntity
|
|
city?: NamedEntity
|
|
organizer?: EventOrganizer
|
|
[key: string]: unknown
|
|
}
|
|
|
|
interface FilterOption {
|
|
code: string
|
|
name: string
|
|
}
|
|
|
|
const IS_FREE_FILTER_ITEMS: FilterOption[] = [
|
|
{ code: 'true', name: 'بله' },
|
|
{ code: 'false', name: 'خیر' },
|
|
]
|
|
|
|
const IS_DISCOVERABLE_FILTER_ITEMS: FilterOption[] = [
|
|
{ code: 'true', name: 'بله' },
|
|
{ code: 'false', name: 'خیر' },
|
|
]
|
|
|
|
const toSelectFilterItems = (items: NamedEntity[]): FilterOption[] => items.map((item) => ({ code: String(item.id), name: item.name }))
|
|
|
|
const extractApiList = <T,>(raw: unknown): T[] => {
|
|
if (Array.isArray(raw)) return raw as T[]
|
|
|
|
if (typeof raw === 'object' && raw !== null && 'data' in raw) {
|
|
const data = raw.data
|
|
|
|
if (Array.isArray(data)) return data as T[]
|
|
}
|
|
|
|
return []
|
|
}
|
|
|
|
const getOrganizer = (row: EventRow): EventOrganizer => {
|
|
if (row.organizer && typeof row.organizer === 'object') {
|
|
return row.organizer
|
|
}
|
|
|
|
return { id: '—', firstName: null, lastName: null }
|
|
}
|
|
|
|
const EventsPage = () => {
|
|
const router = useRouter()
|
|
const [categoryFilterItems, setCategoryFilterItems] = useState<FilterOption[]>([])
|
|
const [cityFilterItems, setCityFilterItems] = useState<FilterOption[]>([])
|
|
|
|
useEffect(() => {
|
|
const loadFilterOptions = async () => {
|
|
try {
|
|
const [categoriesRes, citiesRes] = await Promise.all([
|
|
axiosInstance.get(API_ROUTES.EVENT_CATEGORIES.ADMIN_FLAT),
|
|
axiosInstance.get(API_ROUTES.GEOGRAPHY.ALL_CITIES),
|
|
])
|
|
|
|
setCategoryFilterItems(toSelectFilterItems(extractApiList<NamedEntity>(categoriesRes.data)))
|
|
setCityFilterItems(toSelectFilterItems(extractApiList<NamedEntity>(citiesRes.data)))
|
|
} catch {
|
|
setCategoryFilterItems([])
|
|
setCityFilterItems([])
|
|
}
|
|
}
|
|
|
|
void loadFilterOptions()
|
|
}, [])
|
|
|
|
const columns = useMemo<PaginationListColumnType[]>(
|
|
() => [
|
|
{
|
|
field: 'title',
|
|
label: 'عنوان',
|
|
filterable: true,
|
|
type: 'text',
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'status',
|
|
label: 'وضعیت',
|
|
filterable: true,
|
|
type: 'select',
|
|
sortable: false,
|
|
filterItems: EVENT_STATUS_FILTER_ITEMS,
|
|
},
|
|
{
|
|
field: 'isDiscoverable',
|
|
label: 'قابلجستجو',
|
|
filterable: true,
|
|
type: 'select',
|
|
sortable: false,
|
|
filterItems: IS_DISCOVERABLE_FILTER_ITEMS,
|
|
},
|
|
{
|
|
field: 'categoryId',
|
|
label: 'دستهبندی',
|
|
filterable: true,
|
|
type: 'select',
|
|
sortable: false,
|
|
filterItems: categoryFilterItems,
|
|
},
|
|
{
|
|
field: 'organizerId',
|
|
label: 'برگزارکننده',
|
|
filterable: true,
|
|
type: 'text',
|
|
sortable: false,
|
|
},
|
|
// Province-level filtering is disabled here too — see cities/page.tsx's
|
|
// note for why (city-only admin surface). Only the city filter below
|
|
// is active.
|
|
{
|
|
field: 'cityId',
|
|
label: 'شهر',
|
|
filterable: true,
|
|
type: 'select',
|
|
sortable: false,
|
|
filterItems: cityFilterItems,
|
|
},
|
|
{
|
|
field: 'startsAt',
|
|
label: 'تاریخ شروع',
|
|
filterable: true,
|
|
sortable: true,
|
|
type: 'dateFromTo',
|
|
},
|
|
{
|
|
field: 'isFree',
|
|
label: 'رایگان',
|
|
filterable: true,
|
|
type: 'select',
|
|
sortable: false,
|
|
filterItems: IS_FREE_FILTER_ITEMS,
|
|
},
|
|
{
|
|
field: 'price',
|
|
label: 'قیمت',
|
|
filterable: true,
|
|
type: 'inputFromTo',
|
|
sortable: false,
|
|
},
|
|
{
|
|
field: 'actions',
|
|
label: 'عملیات',
|
|
},
|
|
],
|
|
[categoryFilterItems, cityFilterItems]
|
|
)
|
|
|
|
return (
|
|
<section className="h-full w-full text-right">
|
|
<PageNavbar pageTitle="رویدادها" />
|
|
<div className="admin-page-container">
|
|
<PaginatedList
|
|
hasDynamicButton
|
|
columns={columns}
|
|
dynamicButtonText="رویداد جدید"
|
|
url={API_ROUTES.EVENTS.ADMIN_LIST}
|
|
onDynamicButtonClick={() => {
|
|
router.push(APP_ROUTES.MANAGE_EVENT_NEW)
|
|
}}
|
|
>
|
|
{{
|
|
status: (_row, cellValue) => <StatusChip {...getEventStatus(coerceToString(cellValue))} />,
|
|
isDiscoverable: (_row, cellValue) => <StatusChip {...getBooleanStatus(typeof cellValue === 'boolean' ? cellValue : null)} />,
|
|
categoryId: (row) => {
|
|
const event = row as EventRow
|
|
|
|
return event.category?.name ?? '—'
|
|
},
|
|
organizerId: (row) => {
|
|
const organizer = getOrganizer(row as EventRow)
|
|
|
|
return formatPersonName(organizer.firstName, organizer.lastName)
|
|
},
|
|
cityId: (row) => {
|
|
const event = row as EventRow
|
|
|
|
return event.city?.name ?? '—'
|
|
},
|
|
startsAt: (_row, cellValue) => formatPersianDate(cellValue),
|
|
isFree: (_row, cellValue) => <StatusChip {...getBooleanStatus(typeof cellValue === 'boolean' ? cellValue : null)} />,
|
|
price: (row, cellValue) => {
|
|
const event = row as EventRow
|
|
|
|
if (event.isFree) return '—'
|
|
|
|
const amount = typeof cellValue === 'number' ? cellValue : Number(cellValue)
|
|
|
|
if (!Number.isFinite(amount)) return '—'
|
|
|
|
return formatCurrency(amount)
|
|
},
|
|
actions: (row) => (
|
|
<AdminTableViewButton
|
|
label="مشاهده رویداد"
|
|
mode="navigate"
|
|
to={APP_ROUTES.MANAGE_EVENT_DETAIL(String((row as EventRow).id))}
|
|
/>
|
|
),
|
|
}}
|
|
</PaginatedList>
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
export default EventsPage
|