Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
205 lines
7.0 KiB
TypeScript
205 lines
7.0 KiB
TypeScript
'use client'
|
||
|
||
import { useState } from 'react'
|
||
import DatePicker from 'react-multi-date-picker'
|
||
import persian from 'react-date-object/calendars/persian'
|
||
import persian_fa from 'react-date-object/locales/persian_fa'
|
||
import 'react-multi-date-picker/styles/layouts/mobile.css'
|
||
|
||
import { addToast } from '@/lib/toast'
|
||
import Input from '@/components/formElements/Input'
|
||
import Modal from '@/components/modals/Modal'
|
||
import axiosInstance from '@/config/axios'
|
||
import { convertToISOFormat, coerceToString } from '@/helpers'
|
||
import { API_ROUTES } from '@/services/config'
|
||
import { extractServerErrorDetail } from '@/services/errorHandler'
|
||
|
||
interface CreateSettlementModalProps {
|
||
isOpen: boolean
|
||
onClose: () => void
|
||
onCreated: () => void
|
||
}
|
||
|
||
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
|
||
|
||
type FieldErrors = Partial<Record<'organizerId' | 'bankAccountId' | 'periodStart' | 'periodEnd', string>>
|
||
|
||
function toDatePickerValue(value: string): Date | null {
|
||
if (!value) return null
|
||
const date = new Date(value)
|
||
|
||
return Number.isFinite(date.getTime()) ? date : null
|
||
}
|
||
|
||
const CreateSettlementModal = ({ isOpen, onClose, onCreated }: CreateSettlementModalProps) => {
|
||
const [organizerId, setOrganizerId] = useState('')
|
||
const [bankAccountId, setBankAccountId] = useState('')
|
||
const [periodStart, setPeriodStart] = useState('')
|
||
const [periodEnd, setPeriodEnd] = useState('')
|
||
const [errors, setErrors] = useState<FieldErrors>({})
|
||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||
|
||
const resetForm = () => {
|
||
setOrganizerId('')
|
||
setBankAccountId('')
|
||
setPeriodStart('')
|
||
setPeriodEnd('')
|
||
setErrors({})
|
||
}
|
||
|
||
const handleClose = () => {
|
||
if (isSubmitting) return
|
||
|
||
resetForm()
|
||
onClose()
|
||
}
|
||
|
||
const validate = (): FieldErrors => {
|
||
const nextErrors: FieldErrors = {}
|
||
|
||
if (!organizerId.trim()) {
|
||
nextErrors.organizerId = 'شناسه میزبان الزامی است'
|
||
} else if (!UUID_PATTERN.test(organizerId.trim())) {
|
||
nextErrors.organizerId = 'شناسه میزبان باید یک UUID معتبر باشد'
|
||
}
|
||
|
||
if (!bankAccountId.trim()) {
|
||
nextErrors.bankAccountId = 'شناسه حساب بانکی الزامی است'
|
||
} else if (!UUID_PATTERN.test(bankAccountId.trim())) {
|
||
nextErrors.bankAccountId = 'شناسه حساب بانکی باید یک UUID معتبر باشد'
|
||
}
|
||
|
||
if (!periodStart) {
|
||
nextErrors.periodStart = 'ابتدای بازه الزامی است'
|
||
}
|
||
|
||
if (!periodEnd) {
|
||
nextErrors.periodEnd = 'انتهای بازه الزامی است'
|
||
}
|
||
|
||
if (periodStart && periodEnd && periodEnd < periodStart) {
|
||
nextErrors.periodEnd = 'انتهای بازه باید بعد از ابتدای بازه باشد'
|
||
}
|
||
|
||
return nextErrors
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
const nextErrors = validate()
|
||
|
||
setErrors(nextErrors)
|
||
if (Object.keys(nextErrors).length > 0) return
|
||
|
||
try {
|
||
setIsSubmitting(true)
|
||
await axiosInstance.post(API_ROUTES.SETTLEMENTS.ADMIN_CREATE, {
|
||
organizerId: organizerId.trim(),
|
||
bankAccountId: bankAccountId.trim(),
|
||
periodStart,
|
||
periodEnd,
|
||
})
|
||
addToast({ title: 'تسویه با موفقیت ایجاد شد', color: 'success' })
|
||
resetForm()
|
||
onCreated()
|
||
} catch (err) {
|
||
const detail = extractServerErrorDetail((err as { response?: { data?: unknown } })?.response?.data)
|
||
|
||
addToast({
|
||
title: 'ایجاد تسویه ناموفق بود',
|
||
description: detail ?? 'ممکن است حساب بانکی معتبر نباشد یا درآمد قابل تسویهای در این بازه وجود نداشته باشد.',
|
||
color: 'danger',
|
||
})
|
||
} finally {
|
||
setIsSubmitting(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Modal
|
||
acceptBtnText="ایجاد تسویه"
|
||
isLoading={isSubmitting}
|
||
isOpen={isOpen}
|
||
rejectBtnText="انصراف"
|
||
size="lg"
|
||
title="ایجاد تسویه جدید"
|
||
onAccept={handleSubmit}
|
||
onOpenChange={(open) => {
|
||
if (!open) handleClose()
|
||
}}
|
||
onReject={handleClose}
|
||
>
|
||
<div className="flex flex-col gap-4">
|
||
<div className="flex flex-col gap-1">
|
||
<Input
|
||
generalType="input"
|
||
label="شناسه میزبان (organizerId)"
|
||
name="organizerId"
|
||
placeholder="مثال: 3c1b2e2a-0000-0000-0000-000000000000"
|
||
value={organizerId}
|
||
onValueChange={(next) => {
|
||
setOrganizerId(coerceToString(next))
|
||
}}
|
||
/>
|
||
{errors.organizerId && <span className="text-tiny text-fourth-900">{errors.organizerId}</span>}
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-1">
|
||
<Input
|
||
generalType="input"
|
||
label="شناسه حساب بانکی میزبان (bankAccountId)"
|
||
name="bankAccountId"
|
||
placeholder="مثال: 7fa1c9d4-0000-0000-0000-000000000000"
|
||
value={bankAccountId}
|
||
onValueChange={(next) => {
|
||
setBankAccountId(coerceToString(next))
|
||
}}
|
||
/>
|
||
{errors.bankAccountId && <span className="text-tiny text-fourth-900">{errors.bankAccountId}</span>}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-sm text-text-muted">ابتدای بازه</span>
|
||
<DatePicker
|
||
portal
|
||
calendar={persian}
|
||
calendarPosition="bottom-right"
|
||
className={errors.periodStart ? 'date-picker-input date-picker-input--invalid' : 'date-picker-input'}
|
||
containerStyle={{ width: '100%' }}
|
||
format="YYYY/MM/DD"
|
||
locale={persian_fa}
|
||
placeholder="انتخاب تاریخ"
|
||
value={toDatePickerValue(periodStart)}
|
||
onChange={(date) => {
|
||
setPeriodStart(date ? convertToISOFormat(date).split('T')[0] : '')
|
||
}}
|
||
/>
|
||
{errors.periodStart && <span className="text-tiny text-fourth-900">{errors.periodStart}</span>}
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-1">
|
||
<span className="text-sm text-text-muted">انتهای بازه</span>
|
||
<DatePicker
|
||
portal
|
||
calendar={persian}
|
||
calendarPosition="bottom-right"
|
||
className={errors.periodEnd ? 'date-picker-input date-picker-input--invalid' : 'date-picker-input'}
|
||
containerStyle={{ width: '100%' }}
|
||
format="YYYY/MM/DD"
|
||
locale={persian_fa}
|
||
placeholder="انتخاب تاریخ"
|
||
value={toDatePickerValue(periodEnd)}
|
||
onChange={(date) => {
|
||
setPeriodEnd(date ? convertToISOFormat(date).split('T')[0] : '')
|
||
}}
|
||
/>
|
||
{errors.periodEnd && <span className="text-tiny text-fourth-900">{errors.periodEnd}</span>}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Modal>
|
||
)
|
||
}
|
||
|
||
export default CreateSettlementModal
|