Updated the padding in the completed status section of the Settlements page for better spacing. Additionally, streamlined the consumer booking action bar styles by removing redundant padding properties.
330 lines
11 KiB
TypeScript
330 lines
11 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import dynamic from 'next/dynamic'
|
||
import { useQueryClient } from '@tanstack/react-query'
|
||
|
||
import { addToast } from '@/lib/toast'
|
||
import type { PaginationListColumnType } from '@/types'
|
||
import PaginatedList from '@/components/PaginatedList'
|
||
import PageNavbar from '@/components/layouts/PageNavbar'
|
||
import Button from '@/components/formElements/Button'
|
||
import FileCheckIcon from '@/components/icons/FileCheckIcon'
|
||
import Modal from '@/components/modals/Modal'
|
||
import { ListSkeleton } from '@/components/feedback/LoadingState'
|
||
import AdminTableActions from '@/components/ui/AdminTableActions'
|
||
import AdminTableViewButton from '@/components/ui/AdminTableViewButton'
|
||
import StatusChip from '@/components/ui/StatusChip'
|
||
import axiosInstance from '@/config/axios'
|
||
import { getSettlementStatus, type SettlementStatus, SETTLEMENT_STATUS_FILTER_ITEMS } from '@/constants/status'
|
||
import { formatCurrency, coerceToString } from '@/helpers'
|
||
import { unwrapApiPayload } from '@/helpers/listResponse'
|
||
import useAdminMutation from '@/hooks/useAdminMutation'
|
||
import { formatPersianDate } from '@/lib/formatters'
|
||
import { adminKeys } from '@/queries/admin/adminKeys'
|
||
import { API_ROUTES } from '@/services/config'
|
||
|
||
const CreateSettlementModal = dynamic(() => import('@/app/(dashboard)/settlements/_components/CreateSettlementModal'), { ssr: false })
|
||
const ManualPayoutModal = dynamic(() => import('@/components/admin/ManualPayoutModal'), { ssr: false })
|
||
|
||
interface SettlementItem {
|
||
id: string
|
||
amount: number | string
|
||
organizerEarningId: string
|
||
eventId: string
|
||
eventTitle: string
|
||
}
|
||
|
||
interface SettlementRow {
|
||
id: string
|
||
settlementCode: string
|
||
organizerId: string
|
||
bankAccountId: string
|
||
periodStart: string
|
||
periodEnd: string
|
||
totalAmount: number | string
|
||
status: SettlementStatus
|
||
processedAt?: string | null
|
||
failureReason?: string | null
|
||
manualTrackingCode?: string | null
|
||
manualReceiptUrl?: string | null
|
||
manualNote?: string | null
|
||
createdAt: string
|
||
updatedAt: string
|
||
items?: SettlementItem[]
|
||
[key: string]: unknown
|
||
}
|
||
|
||
const COMPLETABLE_STATUSES: SettlementStatus[] = ['pending', 'processing']
|
||
|
||
const columns: PaginationListColumnType[] = [
|
||
{
|
||
field: 'settlementCode',
|
||
label: 'کد تسویه',
|
||
filterable: false,
|
||
sortable: false,
|
||
type: 'text',
|
||
},
|
||
{
|
||
field: 'organizerId',
|
||
label: 'میزبان',
|
||
filterable: true,
|
||
sortable: false,
|
||
type: 'text',
|
||
hideInTable: true,
|
||
},
|
||
{
|
||
field: 'period',
|
||
label: 'بازه زمانی',
|
||
filterable: false,
|
||
sortable: false,
|
||
},
|
||
{
|
||
field: 'totalAmount',
|
||
label: 'مبلغ کل',
|
||
filterable: false,
|
||
sortable: true,
|
||
},
|
||
{
|
||
field: 'status',
|
||
label: 'وضعیت',
|
||
filterable: true,
|
||
sortable: true,
|
||
type: 'select',
|
||
filterItems: SETTLEMENT_STATUS_FILTER_ITEMS,
|
||
},
|
||
{
|
||
field: 'createdAt',
|
||
label: 'تاریخ ثبت',
|
||
filterable: false,
|
||
sortable: true,
|
||
type: 'date',
|
||
},
|
||
{
|
||
field: 'actions',
|
||
label: 'عملیات',
|
||
},
|
||
]
|
||
|
||
const formatDate = (value: unknown, withTime = false) =>
|
||
formatPersianDate(value, withTime ? { dateStyle: 'medium', timeStyle: 'short' } : { dateStyle: 'medium' })
|
||
|
||
const SettlementsPage = () => {
|
||
const queryClient = useQueryClient()
|
||
const { pendingId, runAction } = useAdminMutation({
|
||
errorMessage: 'تکمیل تسویه با خطا مواجه شد',
|
||
url: API_ROUTES.SETTLEMENTS.ADMIN_LIST,
|
||
})
|
||
const [detailTarget, setDetailTarget] = useState<SettlementRow | null>(null)
|
||
const [detailItems, setDetailItems] = useState<SettlementItem[]>([])
|
||
const [isLoadingDetail, setIsLoadingDetail] = useState(false)
|
||
const [isCreateOpen, setIsCreateOpen] = useState(false)
|
||
const [manualTarget, setManualTarget] = useState<SettlementRow | null>(null)
|
||
|
||
const openDetail = async (row: SettlementRow) => {
|
||
setDetailTarget(row)
|
||
setDetailItems([])
|
||
setIsLoadingDetail(true)
|
||
try {
|
||
const response = await axiosInstance.get(API_ROUTES.SETTLEMENTS.ADMIN_DETAIL(row.id))
|
||
const payload = unwrapApiPayload<{ items?: SettlementItem[] }>(response.data)
|
||
|
||
setDetailItems(Array.isArray(payload.items) ? payload.items : [])
|
||
} catch {
|
||
addToast({ title: 'بارگذاری جزئیات تسویه ناموفق بود', color: 'danger' })
|
||
} finally {
|
||
setIsLoadingDetail(false)
|
||
}
|
||
}
|
||
|
||
const closeDetail = () => {
|
||
setDetailTarget(null)
|
||
setDetailItems([])
|
||
}
|
||
|
||
return (
|
||
<section className="h-full w-full text-right">
|
||
<PageNavbar
|
||
endSlot={
|
||
<Button
|
||
size="sm"
|
||
onClick={() => {
|
||
setIsCreateOpen(true)
|
||
}}
|
||
>
|
||
ایجاد تسویه
|
||
</Button>
|
||
}
|
||
pageTitle="تسویهها"
|
||
/>
|
||
<div className="admin-page-container">
|
||
<PaginatedList
|
||
columns={columns}
|
||
url={API_ROUTES.SETTLEMENTS.ADMIN_LIST}
|
||
>
|
||
{{
|
||
period: (row) => {
|
||
const settlement = row as SettlementRow
|
||
|
||
return (
|
||
<span className="text-sm">
|
||
{formatDate(settlement.periodStart)} — {formatDate(settlement.periodEnd)}
|
||
</span>
|
||
)
|
||
},
|
||
totalAmount: (_row, cellValue) => formatCurrency(Number(cellValue ?? 0)),
|
||
createdAt: (_row, cellValue) => formatDate(cellValue, true),
|
||
status: (row, cellValue) => {
|
||
const settlement = row as SettlementRow
|
||
const { label, chipColor } = getSettlementStatus(coerceToString(cellValue))
|
||
|
||
return (
|
||
<StatusChip
|
||
chipColor={chipColor}
|
||
description={settlement.status === 'failed' ? settlement.failureReason : undefined}
|
||
label={label}
|
||
/>
|
||
)
|
||
},
|
||
actions: (row) => {
|
||
const settlement = row as SettlementRow
|
||
const isBusy = pendingId === settlement.id
|
||
const canComplete = COMPLETABLE_STATUSES.includes(settlement.status)
|
||
|
||
return (
|
||
<AdminTableActions>
|
||
<AdminTableViewButton
|
||
label="مشاهده جزئیات تسویه"
|
||
mode="open-detail"
|
||
onClick={() => openDetail(settlement)}
|
||
/>
|
||
{canComplete ? (
|
||
<Button
|
||
iconOnly
|
||
aria-label="تکمیل تسویه"
|
||
color="success"
|
||
disabled={isBusy}
|
||
isLoading={isBusy}
|
||
size="sm"
|
||
variant="flat"
|
||
onClick={() => {
|
||
setManualTarget(settlement)
|
||
}}
|
||
>
|
||
<FileCheckIcon className="size-4" />
|
||
</Button>
|
||
) : null}
|
||
</AdminTableActions>
|
||
)
|
||
},
|
||
}}
|
||
</PaginatedList>
|
||
</div>
|
||
|
||
<Modal
|
||
hideFooter
|
||
isLoading={isLoadingDetail}
|
||
isOpen={Boolean(detailTarget)}
|
||
rejectBtnText="بستن"
|
||
size="2xl"
|
||
title={detailTarget ? `اقلام تسویه «${detailTarget.settlementCode}»` : ''}
|
||
onOpenChange={(open) => {
|
||
if (!open) closeDetail()
|
||
}}
|
||
onReject={closeDetail}
|
||
>
|
||
{isLoadingDetail ? (
|
||
<ListSkeleton count={3} />
|
||
) : (
|
||
<div className="flex flex-col gap-3">
|
||
{detailTarget?.status === 'failed' && detailTarget?.failureReason ? (
|
||
<div className="rounded-md bg-fourth-100 px-3 py-2 text-sm text-fourth-900">
|
||
<span className="font-semibold">دلیل شکست: </span>
|
||
{detailTarget.failureReason}
|
||
</div>
|
||
) : null}
|
||
|
||
{detailTarget?.status === 'completed' ? (
|
||
<div className="flex flex-col gap-2 rounded-md bg-fifth-50 p-3 text-sm">
|
||
<p>
|
||
<span className="font-semibold">شماره پیگیری: </span>
|
||
<span dir="ltr">{detailTarget.manualTrackingCode ?? '—'}</span>
|
||
</p>
|
||
{detailTarget.manualNote ? (
|
||
<p>
|
||
<span className="font-semibold">یادداشت: </span>
|
||
{detailTarget.manualNote}
|
||
</p>
|
||
) : null}
|
||
{detailTarget.manualReceiptUrl ? (
|
||
<a
|
||
className="w-fit text-primary underline"
|
||
href={detailTarget.manualReceiptUrl}
|
||
rel="noreferrer"
|
||
target="_blank"
|
||
>
|
||
مشاهده رسید پرداخت
|
||
</a>
|
||
) : null}
|
||
</div>
|
||
) : null}
|
||
|
||
{detailItems.length === 0 ? (
|
||
<div className="py-6 text-center text-sm text-text-muted">هیچ قلمی برای این تسویه ثبت نشده است.</div>
|
||
) : (
|
||
<div className="flex flex-col gap-2">
|
||
<div className="grid grid-cols-[1fr_auto] gap-2 border-b border-divider pb-2 text-xs font-semibold text-text-muted">
|
||
<span>رویداد</span>
|
||
<span>مبلغ</span>
|
||
</div>
|
||
{detailItems.map((item) => (
|
||
<div
|
||
key={item.id}
|
||
className="grid grid-cols-[1fr_auto] items-center gap-2 border-b border-divider py-2 text-sm last:border-b-0"
|
||
>
|
||
<span>{item.eventTitle}</span>
|
||
<span>{formatCurrency(Number(item.amount ?? 0))}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</Modal>
|
||
|
||
{isCreateOpen ? (
|
||
<CreateSettlementModal
|
||
isOpen={isCreateOpen}
|
||
onClose={() => {
|
||
setIsCreateOpen(false)
|
||
}}
|
||
onCreated={() => {
|
||
setIsCreateOpen(false)
|
||
void queryClient.invalidateQueries({ queryKey: adminKeys.listByUrl(API_ROUTES.SETTLEMENTS.ADMIN_LIST) })
|
||
}}
|
||
/>
|
||
) : null}
|
||
|
||
{manualTarget ? (
|
||
<ManualPayoutModal
|
||
isOpen
|
||
isLoading={manualTarget.id === pendingId}
|
||
title={`ثبت پرداخت «${manualTarget.settlementCode}»`}
|
||
onClose={() => {
|
||
setManualTarget(null)
|
||
}}
|
||
onSubmit={async (payload) => {
|
||
return runAction(
|
||
manualTarget.id,
|
||
() => axiosInstance.patch(API_ROUTES.SETTLEMENTS.ADMIN_COMPLETE(manualTarget.id), payload),
|
||
'تسویه با رسید دستی تکمیل شد'
|
||
)
|
||
}}
|
||
/>
|
||
) : null}
|
||
</section>
|
||
)
|
||
}
|
||
|
||
export default SettlementsPage
|