admin/app/(dashboard)/settlements/page.tsx
alisaza 9552ae4c4c fix(settlements): adjust padding in completed status section and global styles
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.
2026-09-11 15:54:53 +03:30

330 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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