- Replaced `LIST_PUBLIC_CATEGORIES` with `LIST_ADMIN_CATEGORIES_FLAT` in `ArticleFormModal.tsx`. - Removed the "Add Event" button from the dashboard page. - Simplified the `EventsPage` by removing the button and adjusting the layout. - Cleaned up the `AdminEventEditPage` by removing unnecessary props. - Deleted unused layout and page files related to event creation. - Updated `AdminAuthContent` to use `useAdminCitiesQuery` instead of `useDiscoveryCitiesQuery`. - Refactored `EventGuestListAccessPanel` to remove the `accessMode` prop and adjust API calls accordingly. - Removed several unused components and tests related to event creation, enhancing project maintainability.
237 lines
6.6 KiB
TypeScript
237 lines
6.6 KiB
TypeScript
'use client'
|
|
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
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 [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
|
|
columns={columns}
|
|
url={API_ROUTES.EVENTS.ADMIN_LIST}
|
|
>
|
|
{{
|
|
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
|