api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-10-25 18:42:01 +05:00
parent 1a08775451
commit 05b752daf2
84 changed files with 11179 additions and 3724 deletions

View File

@@ -1,3 +1,18 @@
"use client";
import {
createHotel,
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,
@@ -15,45 +30,64 @@ import {
SelectValue,
} from "@/shared/ui/select";
import { zodResolver } from "@hookform/resolvers/zod";
import type { Dispatch, SetStateAction } from "react";
import { Controller, useForm } from "react-hook-form";
import { useMutation } 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 bolishi kerak.",
message: "Sarlavha kamida 2 ta belgidan iborat bo'lishi kerak",
}),
rating: z.number(),
rating: z.number().min(1).max(5),
mealPlan: z.string().min(1, { message: "Taom rejasi tanlanishi majburiy" }),
hotelType: z
.string()
.min(1, { message: "Mehmonxona turi tanlanishi majburiy" }),
.array(z.string())
.min(1, { message: "Kamida 1 ta mehmonxona turi tanlang" }),
hotelFeatures: z
.array(z.string())
.min(1, { message: "Kamida 1 ta xususiyat tanlanishi kerak" }),
.min(1, { message: "Kamida 1 ta xususiyat tanlang" }),
hotelFeaturesType: z
.array(z.string())
.min(1, { message: "Kamida 1 ta tur tanlang" }),
});
const StepTwo = ({
setStep,
data,
isEditMode,
}: {
setStep: Dispatch<SetStateAction<number>>;
data: GetOneTours | undefined;
isEditMode: boolean;
}) => {
const { amenities, id: ticketId } = useTicketStore();
const navigator = useNavigate();
const { t } = useTranslation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
title: "",
rating: 3.0,
mealPlan: "",
hotelType: "",
hotelType: [],
hotelFeatures: [],
hotelFeaturesType: [],
},
});
function onSubmit() {
navigator("tours");
}
useEffect(() => {
if (isEditMode && data?.data) {
const tourData = data.data;
form.setValue("title", tourData.hotel_name);
form.setValue("rating", Number(tourData.hotel_rating));
form.setValue("mealPlan", tourData.hotel_meals);
}
}, [isEditMode, data, form]);
const mealPlans = [
"Breakfast Only",
@@ -61,24 +95,221 @@ const StepTwo = ({
"Full Board",
"All Inclusive",
];
const hotelTypes = ["Hotel", "Resort", "Guest House", "Apartment"];
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");
useEffect(() => {
const loadAll = async () => {
try {
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);
} catch (err) {
console.error(err);
}
};
loadAll();
}, []);
useEffect(() => {
const loadAll = async () => {
try {
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);
} catch (err) {
console.error(err);
}
};
loadAll();
}, []);
useEffect(() => {
if (selectedHotelFeatures.length === 0) {
setAllHotelFeatureType([]);
setFeatureTypeMapping({});
return;
}
const loadAll = async () => {
try {
const selectedFeatureIds = selectedHotelFeatures
.map((featureId) => Number(featureId))
.filter((id) => !isNaN(id));
if (selectedFeatureIds.length === 0) return;
let allResults: HotelFeaturesType[] = [];
const newMapping: Record<string, string[]> = {};
for (const featureId of selectedFeatureIds) {
let page = 1;
let hasNext = true;
const featureTypes: string[] = [];
while (hasNext) {
const res = await hotelFeatureType({
page,
page_size: 50,
feature_type: featureId,
});
const data = res.data.data;
allResults = [...allResults, ...data.results];
data.results.forEach((item: HotelFeaturesType) => {
featureTypes.push(String(item.id));
});
hasNext = !!data.links.next;
page++;
}
newMapping[String(featureId)] = featureTypes;
}
const uniqueResults = allResults.filter(
(item, index, self) =>
index === self.findIndex((t) => t.id === item.id),
);
setAllHotelFeatureType(uniqueResults);
setFeatureTypeMapping(newMapping);
} catch (err) {
console.error(err);
}
};
loadAll();
}, [selectedHotelFeatures]);
const { mutate, isPending } = useMutation({
mutationFn: (body: FormData) => createHotel({ body }),
onSuccess: () => {
navigator("/tours");
toast.success(t("Muvaffaqiyatli saqlandi"), {
richColors: true,
position: "top-center",
});
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
richColors: true,
position: "top-center",
});
},
});
const removeHotelType = (typeId: string) => {
const current = form.getValues("hotelType");
form.setValue(
"hotelType",
current.filter((val) => val !== typeId),
);
};
const onSubmit = (data: z.infer<typeof formSchema>) => {
const formData = new FormData();
formData.append("ticket", ticketId ? ticketId?.toString() : "");
formData.append("name", data.title);
formData.append("rating", String(data.rating));
formData.append(
"meal_plan",
data.mealPlan === "Breakfast Only"
? "breakfast"
: data.mealPlan === "All Inclusive"
? "all_inclusive"
: data.mealPlan === "Half Board"
? "half_board"
: data.mealPlan === "Full Board"
? "full_board"
: "all_inclusive",
);
data.hotelType.forEach((typeId) => {
formData.append("hotel_type", typeId);
});
data.hotelFeaturesType.forEach((typeId) => {
formData.append("hotel_features", typeId);
});
amenities.forEach((e, i) => {
formData.append(`hotel_amenities[${i}]name`, e.name);
formData.append(`hotel_amenities[${i}]name_ru`, e.name_ru);
formData.append(`hotel_amenities[${i}]icon_name`, e.icon_name);
});
mutate(formData);
};
const removeHotelFeature = (featureId: string) => {
const currentFeatures = form.getValues("hotelFeatures");
const currentFeatureTypes = form.getValues("hotelFeaturesType");
const typesToRemove = featureTypeMapping[featureId] || [];
form.setValue(
"hotelFeatures",
currentFeatures.filter((val) => val !== featureId),
);
form.setValue(
"hotelFeaturesType",
currentFeatureTypes.filter((val) => !typesToRemove.includes(val)),
);
};
const removeFeatureType = (typeId: string) => {
const currentValues = form.getValues("hotelFeaturesType");
form.setValue(
"hotelFeaturesType",
currentValues.filter((val) => val !== typeId),
);
};
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-start">
<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 className="text-md">Mehmonxona nomi</Label>
<Label>{t("Mehmonxona nomi")}</Label>
<FormControl>
<Input
placeholder="Toshkent - Dubay"
{...field}
className="h-12 !text-md"
className="h-12"
/>
</FormControl>
<FormMessage />
@@ -86,25 +317,36 @@ const StepTwo = ({
)}
/>
{/* Mehmonxona rating */}
{/* Rating */}
<FormField
control={form.control}
name="rating"
render={({ field }) => (
<FormItem>
<Label className="text-md">Mehmonxona raytingi</Label>
<Label>{t("Mehmonxona reytingi")}</Label>
<FormControl>
<Input
type="text"
placeholder="3.0"
{...field}
className="h-12 !text-md"
value={field.value}
className="h-12"
onChange={(e) => {
const val = e.target.value;
if (/^\d*\.?\d*$/.test(val)) {
// 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 />
@@ -116,31 +358,22 @@ const StepTwo = ({
<FormField
control={form.control}
name="mealPlan"
render={() => (
render={({ field }) => (
<FormItem>
<Label className="text-md">Meal Plan</Label>
<Label>{t("Taom rejasi")}</Label>
<FormControl>
<Controller
control={form.control}
name="mealPlan"
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger className="!h-12 w-full">
<SelectValue placeholder="Taom rejasini tanlang" />
</SelectTrigger>
<SelectContent>
{mealPlans.map((plan) => (
<SelectItem key={plan} value={plan}>
{plan}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
<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>
@@ -151,134 +384,232 @@ const StepTwo = ({
<FormField
control={form.control}
name="hotelType"
render={() => (
render={({ field }) => (
<FormItem>
<Label className="text-md">Mehmonxona turi</Label>
<Label>{t("Mehmonxona turlari")}</Label>
<FormControl>
<Controller
control={form.control}
name="hotelType"
render={({ field }) => (
<Select
value={field.value}
onValueChange={field.onChange}
>
<SelectTrigger className="!h-12 w-full">
<SelectValue placeholder="Mehmonxona turini tanlang" />
</SelectTrigger>
<SelectContent>
{hotelTypes.map((type) => (
<SelectItem key={type} value={type}>
{type}
<div className="space-y-2">
{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.includes(value)) {
field.onChange([...field.value, value]);
}
}}
>
<SelectTrigger className="!h-12 w-full">
<SelectValue
placeholder={
field.value.length > 0
? t("Yana tanlang...")
: t("Tanlang")
}
/>
</SelectTrigger>
<SelectContent>
{allHotelTypes
.filter(
(type) => !field.value.includes(String(type.id)),
)
.map((type) => (
<SelectItem key={type.id} value={String(type.id)}>
{type.name}
</SelectItem>
))}
</SelectContent>
</Select>
)}
/>
</SelectContent>
</Select>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* <FormField
{/* Hotel Features */}
<FormField
control={form.control}
name="hotelFeatures"
render={() => (
render={({ field }) => (
<FormItem>
<Label className="text-md">Mehmonxona qulayliklar</Label>
<Label>{t("Mehmonxona xususiyatlari")}</Label>
<FormControl>
<div className="space-y-2">
{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,
);
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>
)}
<div className="flex flex-col gap-4">
<div className="flex flex-wrap gap-2">
{form.watch("amenities").map((item, idx) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Icon =
(LucideIcons as any)[item.icon_name] || XIcon;
return (
<Badge
key={idx}
variant="secondary"
className="px-3 py-1 text-sm flex items-center gap-2"
>
<Icon className="size-4" />
<span>{item.name}</span>
<button
type="button"
onClick={() => {
const current = form.getValues("amenities");
form.setValue(
"amenities",
current.filter((_, i: number) => i !== idx),
);
}}
className="ml-1 text-muted-foreground hover:text-destructive"
>
<XIcon className="size-4" />
</button>
</Badge>
);
})}
</div>
<div className="flex gap-3 items-center">
<IconSelect
setSelectedIcon={setSelectedIcon}
selectedIcon={selectedIcon}
/>
<Input
id="amenity_name"
placeholder="Qulaylik nomi (masalan: Wi-Fi)"
className="h-12 !text-md flex-1"
/>
<Button
type="button"
onClick={() => {
const nameInput = document.getElementById(
"amenity_name",
) as HTMLInputElement;
if (selectedIcon && nameInput.value) {
const current = form.getValues("amenities");
form.setValue("amenities", [
...current,
{
icon_name: selectedIcon,
name: nameInput.value,
},
]);
nameInput.value = "";
setSelectedIcon("");
<Select
value=""
onValueChange={(value) => {
if (!field.value.includes(value)) {
field.onChange([...field.value, value]);
}
}}
className="h-12"
>
Qoshish
</Button>
<SelectTrigger className="!h-12 w-full">
<SelectValue
placeholder={
field.value.length > 0
? t("Yana tanlang...")
: t("Tanlang")
}
/>
</SelectTrigger>
<SelectContent>
{allHotelFeature
.filter(
(type) => !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>
<FormMessage />
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/> */}
</div>
<div className="flex justify-between">
<button
type="button"
onClick={() => setStep(1)}
className="mt-6 px-6 py-3 bg-gray-600 text-white rounded-md"
>
Ortga
</button>
<button
type="submit"
className="mt-6 px-6 py-3 bg-blue-600 text-white rounded-md"
>
Saqlash
</button>
/>
{/* 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.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.includes(value)) {
field.onChange([...field.value, value]);
}
}}
>
<SelectTrigger className="!h-12 w-full">
<SelectValue
placeholder={
selectedHotelFeatures.length === 0
? t("Avval xususiyat tanlang")
: 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.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 ? t("Yuklanmoqda...") : t("Saqlash")}
</button>
</form>
</Form>
);