650 lines
21 KiB
TypeScript
650 lines
21 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
createHotel,
|
||
editHotel,
|
||
getHotel,
|
||
hotelFeature,
|
||
hotelFeatureType,
|
||
hotelType,
|
||
} from "@/pages/tours/lib/api";
|
||
import { useTicketStore } from "@/pages/tours/lib/store";
|
||
import type {
|
||
GetOneTours,
|
||
HotelFeatures,
|
||
HotelFeaturesType,
|
||
Type,
|
||
} from "@/pages/tours/lib/type";
|
||
import {
|
||
Form,
|
||
FormControl,
|
||
FormField,
|
||
FormItem,
|
||
FormMessage,
|
||
} from "@/shared/ui/form";
|
||
import { Input } from "@/shared/ui/input";
|
||
import { Label } from "@/shared/ui/label";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/shared/ui/select";
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||
import { X } from "lucide-react";
|
||
import { useEffect, useState } from "react";
|
||
import { useForm } from "react-hook-form";
|
||
import { useTranslation } from "react-i18next";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { toast } from "sonner";
|
||
import z from "zod";
|
||
|
||
const formSchema = z.object({
|
||
title: z.string().min(2, {
|
||
message: "Sarlavha kamida 2 ta belgidan iborat bo'lishi kerak",
|
||
}),
|
||
rating: z.string().min(1).max(5),
|
||
mealPlan: z.string().min(1, { message: "Taom rejasi tanlanishi majburiy" }),
|
||
hotelType: z.array(z.string()).optional(),
|
||
hotelFeatures: z.array(z.string()).optional(),
|
||
hotelFeaturesType: z.array(z.string()).optional(),
|
||
});
|
||
|
||
const StepTwo = ({
|
||
data,
|
||
isEditMode,
|
||
}: {
|
||
data: GetOneTours | undefined;
|
||
isEditMode: boolean;
|
||
}) => {
|
||
const { amenities, id: ticketId } = useTicketStore();
|
||
const navigate = useNavigate();
|
||
const { t } = useTranslation();
|
||
|
||
// 🧩 Query - Hotel detail
|
||
const { data: hotelDetail } = useQuery({
|
||
queryKey: ["hotel_detail", data?.data.id],
|
||
queryFn: () => getHotel(data?.data.id!),
|
||
select: (res) => res.data.data.results,
|
||
enabled: !!data?.data.id,
|
||
});
|
||
|
||
// 🧩 React Hook Form
|
||
const form = useForm<z.infer<typeof formSchema>>({
|
||
resolver: zodResolver(formSchema),
|
||
defaultValues: {
|
||
title: "",
|
||
rating: "3.0",
|
||
mealPlan: "",
|
||
hotelType: [],
|
||
hotelFeatures: [],
|
||
hotelFeaturesType: [],
|
||
},
|
||
});
|
||
|
||
// 🧩 Edit holati uchun formni to‘ldirish
|
||
useEffect(() => {
|
||
if (isEditMode && hotelDetail?.[0]) {
|
||
const hotel = hotelDetail[0];
|
||
|
||
form.setValue("title", hotel.name);
|
||
form.setValue("rating", String(hotel.rating));
|
||
|
||
const mealPlan =
|
||
hotel.meal_plan === "breakfast"
|
||
? "Breakfast Only"
|
||
: hotel.meal_plan === "all_inclusive"
|
||
? "All Inclusive"
|
||
: hotel.meal_plan === "half_board"
|
||
? "Half Board"
|
||
: hotel.meal_plan === "full_board"
|
||
? "Full Board"
|
||
: "All Inclusive";
|
||
|
||
form.setValue("mealPlan", mealPlan);
|
||
|
||
form.setValue(
|
||
"hotelType",
|
||
hotel.hotel_type?.map((t) => String(t.id)) ?? [],
|
||
);
|
||
form.setValue(
|
||
"hotelFeatures",
|
||
hotel.hotel_features?.map((f) => String(f.feature_type.id)) ?? [],
|
||
);
|
||
form.setValue("hotelFeaturesType", [
|
||
...new Set(hotel.hotel_features?.map((f) => String(f.id)) ?? []),
|
||
]);
|
||
}
|
||
}, [isEditMode, hotelDetail, form, data]);
|
||
|
||
// 🧩 Select ma'lumotlari
|
||
const [allHotelTypes, setAllHotelTypes] = useState<Type[]>([]);
|
||
const [allHotelFeature, setAllHotelFeature] = useState<HotelFeatures[]>([]);
|
||
const [allHotelFeatureType, setAllHotelFeatureType] = useState<
|
||
HotelFeaturesType[]
|
||
>([]);
|
||
const [featureTypeMapping, setFeatureTypeMapping] = useState<
|
||
Record<string, string[]>
|
||
>({});
|
||
|
||
const selectedHotelFeatures = form.watch("hotelFeatures");
|
||
|
||
// 🔹 Hotel Types yuklash
|
||
useEffect(() => {
|
||
const loadHotelTypes = async () => {
|
||
let page = 1;
|
||
let results: Type[] = [];
|
||
let hasNext = true;
|
||
|
||
while (hasNext) {
|
||
const res = await hotelType({ page, page_size: 50 });
|
||
const data = res.data.data;
|
||
results = [...results, ...data.results];
|
||
hasNext = !!data.links.next;
|
||
page++;
|
||
}
|
||
|
||
setAllHotelTypes(results);
|
||
};
|
||
loadHotelTypes();
|
||
}, []);
|
||
|
||
// 🔹 Hotel Features yuklash
|
||
useEffect(() => {
|
||
const loadHotelFeatures = async () => {
|
||
let page = 1;
|
||
let results: HotelFeatures[] = [];
|
||
let hasNext = true;
|
||
|
||
while (hasNext) {
|
||
const res = await hotelFeature({ page, page_size: 50 });
|
||
const data = res.data.data;
|
||
results = [...results, ...data.results];
|
||
hasNext = !!data.links.next;
|
||
page++;
|
||
}
|
||
|
||
setAllHotelFeature(results);
|
||
};
|
||
loadHotelFeatures();
|
||
}, []);
|
||
|
||
// 🔹 Feature type'larni yuklash (tanlangan feature bo‘yicha)
|
||
useEffect(() => {
|
||
if (selectedHotelFeatures && selectedHotelFeatures.length === 0) {
|
||
setAllHotelFeatureType([]);
|
||
setFeatureTypeMapping({});
|
||
return;
|
||
}
|
||
|
||
const loadFeatureTypes = async () => {
|
||
const selectedIds =
|
||
selectedHotelFeatures &&
|
||
selectedHotelFeatures.map(Number).filter(Boolean);
|
||
let allResults: HotelFeaturesType[] = [];
|
||
const mapping: Record<string, string[]> = {};
|
||
|
||
for (const id of selectedIds!) {
|
||
let page = 1;
|
||
let hasNext = true;
|
||
const featureTypes: string[] = [];
|
||
|
||
while (hasNext) {
|
||
const res = await hotelFeatureType({
|
||
page,
|
||
page_size: 50,
|
||
feature_type: id,
|
||
});
|
||
const data = res.data.data;
|
||
allResults = [...allResults, ...data.results];
|
||
data.results.forEach((ft: HotelFeaturesType) =>
|
||
featureTypes.push(String(ft.id)),
|
||
);
|
||
hasNext = !!data.links.next;
|
||
page++;
|
||
}
|
||
mapping[String(id)] = featureTypes;
|
||
}
|
||
|
||
const uniqueResults = allResults.filter(
|
||
(v, i, a) => a.findIndex((t) => t.id === v.id) === i,
|
||
);
|
||
|
||
setAllHotelFeatureType(uniqueResults);
|
||
setFeatureTypeMapping(mapping);
|
||
};
|
||
|
||
loadFeatureTypes();
|
||
}, [selectedHotelFeatures]);
|
||
|
||
const { mutate, isPending } = useMutation({
|
||
mutationFn: (body: FormData) => createHotel({ body }),
|
||
onSuccess: () => {
|
||
toast.success(t("Muvaffaqiyatli saqlandi"));
|
||
navigate("/tours");
|
||
},
|
||
onError: () =>
|
||
toast.error(t("Xatolik yuz berdi"), {
|
||
richColors: true,
|
||
position: "top-center",
|
||
}),
|
||
});
|
||
|
||
const { mutate: edit, isPending: editPending } = useMutation({
|
||
mutationFn: ({ body, id }: { id: number; body: FormData }) =>
|
||
editHotel({ body, id }),
|
||
onSuccess: () => {
|
||
toast.success(t("Muvaffaqiyatli saqlandi"));
|
||
navigate("/tours");
|
||
},
|
||
onError: () =>
|
||
toast.error(t("Xatolik yuz berdi"), {
|
||
richColors: true,
|
||
position: "top-center",
|
||
}),
|
||
});
|
||
|
||
const removeHotelType = (id: string) =>
|
||
form.setValue(
|
||
"hotelType",
|
||
(form.getValues("hotelType") ?? []).filter((v) => v !== id),
|
||
);
|
||
|
||
const removeHotelFeature = (id: string) => {
|
||
const current = form.getValues("hotelFeatures") ?? [];
|
||
const types = form.getValues("hotelFeaturesType") ?? [];
|
||
const toRemove = featureTypeMapping[id] || [];
|
||
|
||
form.setValue(
|
||
"hotelFeatures",
|
||
current.filter((v) => v !== id),
|
||
);
|
||
form.setValue(
|
||
"hotelFeaturesType",
|
||
types.filter((v) => !toRemove.includes(v)),
|
||
);
|
||
};
|
||
|
||
const removeFeatureType = (id: string) =>
|
||
form.setValue(
|
||
"hotelFeaturesType",
|
||
(form.getValues("hotelFeaturesType") ?? []).filter((v) => v !== id),
|
||
);
|
||
|
||
// 🧩 Submit
|
||
const onSubmit = (data: z.infer<typeof formSchema>) => {
|
||
const formData = new FormData();
|
||
|
||
formData.append("ticket", ticketId ? String(ticketId) : "");
|
||
formData.append("name", data.title);
|
||
formData.append("rating", data.rating);
|
||
amenities.forEach((e, i) => {
|
||
formData.append(`hotel_amenities[${i}]`, String(e));
|
||
});
|
||
|
||
const mealPlan =
|
||
data.mealPlan === "Breakfast Only"
|
||
? "breakfast"
|
||
: data.mealPlan === "Half Board"
|
||
? "half_board"
|
||
: data.mealPlan === "Full Board"
|
||
? "full_board"
|
||
: "all_inclusive";
|
||
formData.append("meal_plan", mealPlan);
|
||
|
||
data.hotelType &&
|
||
data.hotelType.forEach((id) => formData.append("hotel_type", id));
|
||
data.hotelFeatures &&
|
||
data.hotelFeatures.forEach((id) => formData.append("hotel_features", id));
|
||
|
||
if (isEditMode && hotelDetail) {
|
||
edit({
|
||
body: formData,
|
||
id: Number(hotelDetail[0].id),
|
||
});
|
||
} else {
|
||
mutate(formData);
|
||
}
|
||
};
|
||
|
||
const mealPlans = [
|
||
"Breakfast Only",
|
||
"Half Board",
|
||
"Full Board",
|
||
"All Inclusive",
|
||
];
|
||
|
||
return (
|
||
<Form {...form}>
|
||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-end justify-end">
|
||
{/* Mehmonxona nomi */}
|
||
<FormField
|
||
control={form.control}
|
||
name="title"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label>{t("Mehmonxona nomi")}</Label>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="Toshkent - Dubay"
|
||
{...field}
|
||
className="h-12"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
{/* Rating */}
|
||
<FormField
|
||
control={form.control}
|
||
name="rating"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label>{t("Mehmonxona reytingi")}</Label>
|
||
<FormControl>
|
||
<Input
|
||
type="text"
|
||
placeholder="3.0"
|
||
value={field.value}
|
||
className="h-12"
|
||
onChange={(e) => {
|
||
const val = e.target.value;
|
||
// Faqat raqam va nuqta kiritishga ruxsat berish
|
||
if (/^\d*\.?\d*$/.test(val) || val === "") {
|
||
field.onChange(val);
|
||
}
|
||
}}
|
||
onBlur={(e) => {
|
||
const val = e.target.value;
|
||
if (val && !isNaN(parseFloat(val))) {
|
||
// Agar 1 xonali bo'lsa, .0 qo'shish
|
||
const num = parseFloat(val);
|
||
if (val.indexOf(".") === -1) {
|
||
field.onChange(num.toFixed(1));
|
||
}
|
||
}
|
||
}}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
{/* Meal Plan */}
|
||
<FormField
|
||
control={form.control}
|
||
name="mealPlan"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label>{t("Taom rejasi")}</Label>
|
||
<FormControl>
|
||
<Select value={field.value} onValueChange={field.onChange}>
|
||
<SelectTrigger className="!h-12 w-full">
|
||
<SelectValue placeholder={t("Tanlang")} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{mealPlans.map((plan) => (
|
||
<SelectItem key={plan} value={plan}>
|
||
{t(plan)}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
{/* Hotel Type */}
|
||
<FormField
|
||
control={form.control}
|
||
name="hotelType"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label>{t("Mehmonxona turlari")}</Label>
|
||
<FormControl>
|
||
<div className="space-y-2">
|
||
{field.value && field.value.length > 0 && (
|
||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||
{field.value.map((selectedValue) => {
|
||
const selectedItem = allHotelTypes.find(
|
||
(item) => String(item.id) === selectedValue,
|
||
);
|
||
return (
|
||
<div
|
||
key={selectedValue}
|
||
className="flex items-center gap-1 bg-purple-600 text-white px-3 py-1 rounded-md text-sm"
|
||
>
|
||
<span>{selectedItem?.name}</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeHotelType(selectedValue)}
|
||
className="hover:bg-purple-700 rounded-full p-0.5"
|
||
>
|
||
<X size={14} />
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
<Select
|
||
value=""
|
||
onValueChange={(value) => {
|
||
if (field.value && !field.value.includes(value)) {
|
||
field.onChange([...field.value, value]);
|
||
}
|
||
}}
|
||
>
|
||
<SelectTrigger className="!h-12 w-full">
|
||
<SelectValue
|
||
placeholder={
|
||
field.value && field.value.length > 0
|
||
? t("Yana tanlang...")
|
||
: t("Tanlang")
|
||
}
|
||
/>
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{allHotelTypes
|
||
.filter(
|
||
(type) =>
|
||
field.value &&
|
||
!field.value.includes(String(type.id)),
|
||
)
|
||
.map((type) => (
|
||
<SelectItem key={type.id} value={String(type.id)}>
|
||
{type.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
{/* Hotel Features */}
|
||
<FormField
|
||
control={form.control}
|
||
name="hotelFeatures"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label>{t("Mehmonxona xususiyatlari")}</Label>
|
||
<FormControl>
|
||
<div className="space-y-2">
|
||
{field.value && field.value.length > 0 && (
|
||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||
{field.value.map((selectedValue) => {
|
||
const selectedItem = allHotelFeature.find(
|
||
(item) => String(item.id) === selectedValue,
|
||
);
|
||
console.log(allHotelFeature);
|
||
|
||
return (
|
||
<div
|
||
key={selectedValue}
|
||
className="flex items-center gap-1 bg-blue-600 text-white px-3 py-1 rounded-md text-sm"
|
||
>
|
||
<span>
|
||
{selectedItem?.hotel_feature_type_name}
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() =>
|
||
removeHotelFeature(selectedValue)
|
||
}
|
||
className="hover:bg-blue-700 rounded-full p-0.5"
|
||
>
|
||
<X size={14} />
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
<Select
|
||
value=""
|
||
onValueChange={(value) => {
|
||
if (field.value && !field.value.includes(value)) {
|
||
field.onChange([...field.value, value]);
|
||
}
|
||
}}
|
||
>
|
||
<SelectTrigger className="!h-12 w-full">
|
||
<SelectValue
|
||
placeholder={
|
||
field.value && field.value.length > 0
|
||
? t("Yana tanlang...")
|
||
: t("Tanlang")
|
||
}
|
||
/>
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{allHotelFeature
|
||
.filter(
|
||
(type) =>
|
||
field.value &&
|
||
!field.value.includes(String(type.id)),
|
||
)
|
||
.map((type) => (
|
||
<SelectItem key={type.id} value={String(type.id)}>
|
||
{type.hotel_feature_type_name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
{/* Hotel Feature Type */}
|
||
<FormField
|
||
control={form.control}
|
||
name="hotelFeaturesType"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label>{t("Xususiyat turlari")}</Label>
|
||
<FormControl>
|
||
<div className="space-y-2">
|
||
{field.value && field.value.length > 0 && (
|
||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||
{field.value.map((selectedValue) => {
|
||
const selectedItem = allHotelFeatureType.find(
|
||
(item) => String(item.id) === selectedValue,
|
||
);
|
||
return (
|
||
<div
|
||
key={selectedValue}
|
||
className="flex items-center gap-1 bg-green-600 text-white px-3 py-1 rounded-md text-sm"
|
||
>
|
||
<span>{selectedItem?.feature_name}</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => removeFeatureType(selectedValue)}
|
||
className="hover:bg-green-700 rounded-full p-0.5"
|
||
>
|
||
<X size={14} />
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
)}
|
||
|
||
<Select
|
||
value=""
|
||
onValueChange={(value) => {
|
||
if (field.value && !field.value.includes(value)) {
|
||
field.onChange([...field.value, value]);
|
||
}
|
||
}}
|
||
>
|
||
<SelectTrigger className="!h-12 w-full">
|
||
<SelectValue
|
||
placeholder={
|
||
selectedHotelFeatures &&
|
||
selectedHotelFeatures.length === 0
|
||
? t("Avval xususiyat tanlang")
|
||
: field.value && field.value.length > 0
|
||
? t("Yana tanlang...")
|
||
: t("Tanlang")
|
||
}
|
||
/>
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{allHotelFeatureType.length === 0 ? (
|
||
<div className="p-2 text-sm text-gray-500">
|
||
{t("Avval mehmonxona xususiyatini tanlang")}
|
||
</div>
|
||
) : (
|
||
allHotelFeatureType
|
||
.filter(
|
||
(type) =>
|
||
field.value &&
|
||
!field.value.includes(String(type.id)),
|
||
)
|
||
.map((type) => (
|
||
<SelectItem key={type.id} value={String(type.id)}>
|
||
{type.feature_name}
|
||
</SelectItem>
|
||
))
|
||
)}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<button
|
||
type="submit"
|
||
disabled={isPending}
|
||
className="mt-6 px-6 py-3 bg-blue-600 text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||
>
|
||
{isPending || editPending ? t("Yuklanmoqda...") : t("Saqlash")}
|
||
</button>
|
||
</form>
|
||
</Form>
|
||
);
|
||
};
|
||
|
||
export default StepTwo;
|