'use client';
import { zodResolver } from '@hookform/resolvers/zod';
import { ArrowLeft } from 'lucide-react';
import dynamic from 'next/dynamic';
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 { duAnApi, type CreateProjectPayload } from '@/lib/du-an-api';
const LocationPicker = dynamic(
() => import('@/components/map/location-picker').then((m) => m.LocationPicker),
{
ssr: false,
loading: () => (
Đang tải bản đồ…
),
},
);
const SLUG_REGEX = /^[a-z0-9-]+$/;
const projectSchema = z.object({
name: z.string().min(1, 'Bắt buộc'),
slug: z
.string()
.min(1, 'Bắt buộc')
.regex(SLUG_REGEX, 'Chỉ cho phép chữ thường, số và dấu -'),
developer: z.string().min(1, 'Bắt buộc'),
developerLogo: z
.string()
.optional()
.refine(
(v) => !v || /^https?:\/\//.test(v),
'URL không hợp lệ',
),
status: z.enum(['PLANNING', 'UNDER_CONSTRUCTION', 'HANDOVER', 'COMPLETED']),
totalUnits: z
.string()
.min(1, 'Bắt buộc')
.refine((v) => /^\d+$/.test(v) && Number(v) > 0, 'Phải là số nguyên > 0'),
address: z.string().min(1, 'Bắt buộc'),
ward: z.string().min(1, 'Bắt buộc'),
district: z.string().min(1, 'Bắt buộc'),
city: z.string().min(1, 'Bắt buộc'),
latitude: z
.string()
.min(1, 'Bắt buộc')
.refine((v) => {
const n = Number(v);
return Number.isFinite(n) && n >= -90 && n <= 90;
}, 'Từ -90 đến 90'),
longitude: z
.string()
.min(1, 'Bắt buộc')
.refine((v) => {
const n = Number(v);
return Number.isFinite(n) && n >= -180 && n <= 180;
}, 'Từ -180 đến 180'),
description: z.string().optional(),
masterPlanUrl: z
.string()
.optional()
.refine(
(v) => !v || /^https?:\/\//.test(v),
'URL không hợp lệ',
),
minPrice: z
.string()
.optional()
.refine((v) => !v || /^\d+$/.test(v), 'Chỉ nhập số nguyên'),
maxPrice: z
.string()
.optional()
.refine((v) => !v || /^\d+$/.test(v), 'Chỉ nhập số nguyên'),
totalArea: z
.string()
.optional()
.refine((v) => !v || (Number.isFinite(Number(v)) && Number(v) > 0), 'Phải lớn hơn 0'),
buildingCount: z
.string()
.optional()
.refine((v) => !v || (/^\d+$/.test(v) && Number(v) > 0), 'Phải là số nguyên > 0'),
floorCount: z
.string()
.optional()
.refine((v) => !v || (/^\d+$/.test(v) && Number(v) > 0), 'Phải là số nguyên > 0'),
startDate: z.string().optional(),
completionDate: z.string().optional(),
tags: z.string().optional(),
suitableFor: z.string().optional(),
whyThisLocation: z
.string()
.max(2000, 'Tối đa 2000 ký tự')
.optional(),
});
type ProjectFormData = z.infer;
function FormSection({
title,
children,
}: {
title: string;
children: React.ReactNode;
}) {
return (
{title}
{children}
);
}
export default function CreateProjectPage() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [error, setError] = React.useState(null);
const [success, setSuccess] = React.useState(false);
const {
register,
handleSubmit,
setValue,
watch,
formState: { errors },
} = useForm({
resolver: zodResolver(projectSchema),
mode: 'onTouched',
defaultValues: {
status: 'PLANNING',
},
});
const latStr = watch('latitude');
const lngStr = watch('longitude');
const latNum = Number.parseFloat(latStr ?? '');
const lngNum = Number.parseFloat(lngStr ?? '');
const handlePickLocation = React.useCallback(
(
coords: { lat: number; lng: number },
resolved?: { address?: string; ward?: string; district?: string; city?: string },
) => {
setValue('latitude', coords.lat.toFixed(6), { shouldValidate: true });
setValue('longitude', coords.lng.toFixed(6), { shouldValidate: true });
// Only autofill address fields when currently empty — don't clobber what
// the admin typed intentionally.
if (resolved?.address && !watch('address')) {
setValue('address', resolved.address, { shouldValidate: true });
}
if (resolved?.ward && !watch('ward')) {
setValue('ward', resolved.ward, { shouldValidate: true });
}
if (resolved?.district && !watch('district')) {
setValue('district', resolved.district, { shouldValidate: true });
}
if (resolved?.city && !watch('city')) {
setValue('city', resolved.city, { shouldValidate: true });
}
},
[setValue, watch],
);
const onSubmit = async (data: ProjectFormData) => {
setIsSubmitting(true);
setError(null);
try {
const payload: CreateProjectPayload = {
name: data.name,
slug: data.slug,
developer: data.developer,
status: data.status,
totalUnits: Number(data.totalUnits),
address: data.address,
ward: data.ward,
district: data.district,
city: data.city,
latitude: Number(data.latitude),
longitude: Number(data.longitude),
};
if (data.developerLogo) payload.developerLogo = data.developerLogo;
if (data.description) payload.description = data.description;
if (data.masterPlanUrl) payload.masterPlanUrl = data.masterPlanUrl;
if (data.minPrice) payload.minPrice = data.minPrice;
if (data.maxPrice) payload.maxPrice = data.maxPrice;
if (data.totalArea) payload.totalArea = Number(data.totalArea);
if (data.buildingCount) payload.buildingCount = Number(data.buildingCount);
if (data.floorCount) payload.floorCount = Number(data.floorCount);
if (data.startDate) payload.startDate = data.startDate;
if (data.completionDate) payload.completionDate = data.completionDate;
if (data.tags) {
const tags = data.tags
.split(',')
.map((s) => s.trim())
.filter(Boolean);
if (tags.length > 0) payload.tags = tags;
}
if (data.suitableFor) {
const suitableFor = data.suitableFor
.split(',')
.map((s) => s.trim())
.filter(Boolean);
if (suitableFor.length > 0) payload.suitableFor = suitableFor;
}
if (data.whyThisLocation) payload.whyThisLocation = data.whyThisLocation;
await duAnApi.create(payload);
setSuccess(true);
setTimeout(() => router.push('/projects'), 600);
} catch (err) {
setError(err instanceof Error ? err.message : 'Có lỗi xảy ra');
} finally {
setIsSubmitting(false);
}
};
return (
Danh sách dự án
Thêm dự án mới
{success && (
Đã tạo dự án
)}
{error && (
{error}
)}
);
}