Files
simple-admin/src/pages/tours/ui/StepTwo.tsx
Samandar Turgunboyev 4e9b2f3bd8 bug fix
2025-11-01 16:18:36 +05:00

650 lines
21 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 {
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 toldirish
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 boyicha)
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;