'use client'; import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import Link from 'next/link'; import { useRouter } from 'next/navigation'; import * as React from 'react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select } from '@/components/ui/select'; import { Textarea } from '@/components/ui/textarea'; import { industrialApi, PARK_STATUS_LABELS, REGION_LABELS, type CreateIndustrialParkPayload, type IndustrialParkStatus, type VietnamRegion, } from '@/lib/khu-cong-nghiep-api'; const STATUS_OPTIONS: IndustrialParkStatus[] = [ 'PLANNING', 'UNDER_CONSTRUCTION', 'OPERATIONAL', 'FULL', ]; const REGION_OPTIONS: VietnamRegion[] = ['NORTH', 'CENTRAL', 'SOUTH']; const optionalString = z .string() .optional() .transform((v) => (v && v.trim() !== '' ? v.trim() : undefined)); const requiredString = (msg: string) => z.string().min(1, msg); const nonNegativeNumber = (msg: string) => z .string() .min(1, msg) .refine((v) => !isNaN(Number(v)) && Number(v) >= 0, 'Phải là số không âm'); const optionalNonNegativeNumber = z .string() .optional() .refine( (v) => v === undefined || v === '' || (!isNaN(Number(v)) && Number(v) >= 0), 'Phải là số không âm', ); const parkFormSchema = z.object({ name: requiredString('Vui lòng nhập tên KCN'), nameEn: optionalString, slug: requiredString('Vui lòng nhập slug'), developer: requiredString('Vui lòng nhập chủ đầu tư'), operator: optionalString, status: z.enum(['PLANNING', 'UNDER_CONSTRUCTION', 'OPERATIONAL', 'FULL']), address: requiredString('Vui lòng nhập địa chỉ'), district: requiredString('Vui lòng nhập quận/huyện'), province: requiredString('Vui lòng nhập tỉnh/thành'), region: z.enum(['NORTH', 'CENTRAL', 'SOUTH']), latitude: z .string() .min(1, 'Vui lòng nhập vĩ độ') .refine((v) => !isNaN(Number(v)) && Number(v) >= -90 && Number(v) <= 90, 'Vĩ độ từ -90 đến 90'), longitude: z .string() .min(1, 'Vui lòng nhập kinh độ') .refine( (v) => !isNaN(Number(v)) && Number(v) >= -180 && Number(v) <= 180, 'Kinh độ từ -180 đến 180', ), totalAreaHa: nonNegativeNumber('Vui lòng nhập tổng diện tích'), leasableAreaHa: nonNegativeNumber('Vui lòng nhập diện tích cho thuê'), landRentUsdM2Year: optionalNonNegativeNumber, rbfRentUsdM2Month: optionalNonNegativeNumber, rbwRentUsdM2Month: optionalNonNegativeNumber, managementFeeUsd: optionalNonNegativeNumber, targetIndustries: optionalString, infrastructure: optionalString, establishedYear: optionalNonNegativeNumber, tenantCount: optionalNonNegativeNumber, description: optionalString, descriptionEn: optionalString, }); type ParkFormValues = z.input; function parseInfrastructure(text: string | undefined): Record | undefined { if (!text) return undefined; const lines = text.split('\n').map((l) => l.trim()).filter(Boolean); if (lines.length === 0) return undefined; const out: Record = {}; for (const line of lines) { const idx = line.indexOf(':'); if (idx > 0) { const key = line.slice(0, idx).trim(); const value = line.slice(idx + 1).trim(); if (key) out[key] = value; } else { out[line] = true; } } return Object.keys(out).length > 0 ? out : undefined; } function toNumOrUndef(v: string | undefined): number | undefined { if (v === undefined || v === '') return undefined; const n = Number(v); return isNaN(n) ? undefined : n; } export default function CreateIndustrialParkPage() { const router = useRouter(); const queryClient = useQueryClient(); const [error, setError] = React.useState(null); const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(parkFormSchema), mode: 'onTouched', defaultValues: { status: 'PLANNING', region: 'NORTH', }, }); const mutation = useMutation({ mutationFn: (payload: CreateIndustrialParkPayload) => industrialApi.createPark(payload), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['admin-industrial-parks'] }); router.push('/industrial-parks'); }, onError: (err: unknown) => { setError(err instanceof Error ? err.message : 'Có lỗi xảy ra'); }, }); const onSubmit = (data: ParkFormValues) => { setError(null); const totalArea = Number(data.totalAreaHa); const leasableArea = Number(data.leasableAreaHa); const industries = data.targetIndustries ? data.targetIndustries .split(',') .map((s) => s.trim()) .filter(Boolean) : []; const payload: CreateIndustrialParkPayload = { name: data.name!, slug: data.slug!, developer: data.developer!, status: data.status, address: data.address!, district: data.district!, province: data.province!, region: data.region, latitude: Number(data.latitude), longitude: Number(data.longitude), totalAreaHa: totalArea, leasableAreaHa: leasableArea, occupancyRate: 0, remainingAreaHa: leasableArea, targetIndustries: industries, }; if (data.nameEn) payload.nameEn = data.nameEn; if (data.operator) payload.operator = data.operator; const landRent = toNumOrUndef(data.landRentUsdM2Year); if (landRent != null) payload.landRentUsdM2Year = landRent; const rbfRent = toNumOrUndef(data.rbfRentUsdM2Month); if (rbfRent != null) payload.rbfRentUsdM2Month = rbfRent; const rbwRent = toNumOrUndef(data.rbwRentUsdM2Month); if (rbwRent != null) payload.rbwRentUsdM2Month = rbwRent; const mgmtFee = toNumOrUndef(data.managementFeeUsd); if (mgmtFee != null) payload.managementFeeUsd = mgmtFee; const year = toNumOrUndef(data.establishedYear); if (year != null) payload.establishedYear = year; const tenantCount = toNumOrUndef(data.tenantCount); if (tenantCount != null) payload.tenantCount = tenantCount; const infra = parseInfrastructure(data.infrastructure); if (infra) payload.infrastructure = infra; if (data.description) payload.description = data.description; if (data.descriptionEn) payload.descriptionEn = data.descriptionEn; mutation.mutate(payload); }; return (
← Danh sách KCN

Thêm KCN

{error && (
{error}
)}
{/* Thông tin cơ bản */} Thông tin cơ bản
{errors.name && (

{errors.name.message}

)}
{errors.slug && (

{errors.slug.message}

)}
{errors.developer && (

{errors.developer.message}

)}