admin/app/(dashboard)/settlements/_components/CreateSettlementModal.tsx
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

205 lines
7.0 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 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