2717 lines
104 KiB
TypeScript
2717 lines
104 KiB
TypeScript
"use client";
|
||
|
||
import {
|
||
countryList,
|
||
createTours,
|
||
getAllAmenities,
|
||
hotelBadge,
|
||
hotelTransport,
|
||
regionList,
|
||
updateTours,
|
||
} from "@/pages/tours/lib/api";
|
||
import { TourformSchema } from "@/pages/tours/lib/form";
|
||
import { useTicketStore } from "@/pages/tours/lib/store";
|
||
import type { GetOneTours } from "@/pages/tours/lib/type";
|
||
import TicketsImagesModel from "@/pages/tours/ui/TicketsImagesModel";
|
||
import formatPrice from "@/shared/lib/formatPrice";
|
||
import { cn } from "@/shared/lib/utils";
|
||
import { Badge } from "@/shared/ui/badge";
|
||
import { Button } from "@/shared/ui/button";
|
||
import { Calendar } from "@/shared/ui/calendar";
|
||
import {
|
||
Command,
|
||
CommandGroup,
|
||
CommandInput,
|
||
CommandItem,
|
||
CommandList,
|
||
} from "@/shared/ui/command";
|
||
import {
|
||
Form,
|
||
FormControl,
|
||
FormField,
|
||
FormItem,
|
||
FormMessage,
|
||
} from "@/shared/ui/form";
|
||
import { Input } from "@/shared/ui/input";
|
||
import { Label } from "@/shared/ui/label";
|
||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||
import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
|
||
import { Textarea } from "@/shared/ui/textarea";
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { AnimatePresence, motion } from "framer-motion";
|
||
import {
|
||
Check,
|
||
ChevronDownIcon,
|
||
ChevronsUpDown,
|
||
Loader2,
|
||
MoveLeft,
|
||
SquareCheckBig,
|
||
XIcon,
|
||
} from "lucide-react";
|
||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||
import { useForm } from "react-hook-form";
|
||
import { useTranslation } from "react-i18next";
|
||
import { toast } from "sonner";
|
||
import z from "zod";
|
||
|
||
const StepOne = ({
|
||
setStep,
|
||
data,
|
||
id,
|
||
isEditMode,
|
||
}: {
|
||
setStep: Dispatch<SetStateAction<number>>;
|
||
data: GetOneTours | undefined;
|
||
id: string | undefined;
|
||
isEditMode: boolean;
|
||
}) => {
|
||
const [displayPrice, setDisplayPrice] = useState("");
|
||
const queryClient = useQueryClient();
|
||
// const [tarifdisplayPrice, setTarifDisplayPrice] = useState<string[]>([]);
|
||
const [transportPrices, setTransportPrices] = useState<string[]>([]);
|
||
|
||
const { t } = useTranslation();
|
||
const [openDate, setOpenDate] = useState(false);
|
||
const [openDateTravel, setOpenDateTravel] = useState(false);
|
||
|
||
const form = useForm<z.infer<typeof TourformSchema>>({
|
||
resolver: zodResolver(TourformSchema),
|
||
defaultValues: {
|
||
title: "",
|
||
title_ru: "",
|
||
hotel_services: [],
|
||
price: 0,
|
||
departure: "",
|
||
// tarif: [],
|
||
transport: [],
|
||
// departure_ru: "",
|
||
destination: "",
|
||
// destination_ru: "",
|
||
location_name: "",
|
||
location_name_ru: "",
|
||
departureDateTime: {
|
||
date: undefined,
|
||
},
|
||
amenities: [],
|
||
hotel_meals: [],
|
||
travelDateTime: {
|
||
date: undefined,
|
||
},
|
||
passenger_count: "1",
|
||
min_person: "1",
|
||
max_person: "1",
|
||
extra_service: [],
|
||
paid_extra_service: [],
|
||
languages: "",
|
||
duration: "1",
|
||
badges: [],
|
||
images: [],
|
||
hotel_info: "",
|
||
hotel_info_ru: "",
|
||
hotel_meals_info: "",
|
||
hotel_meals_info_ru: "",
|
||
ticket_itinerary: [],
|
||
},
|
||
});
|
||
|
||
const { addAmenity, setId, removeAmenity } = useTicketStore();
|
||
|
||
useEffect(() => {
|
||
if (!isEditMode || !data?.data) return;
|
||
|
||
const tour = data.data;
|
||
|
||
// 🔹 Jo'nash vaqti
|
||
let departureDateTime = undefined;
|
||
if (tour.departure_time) {
|
||
const d = new Date(tour.departure_time);
|
||
departureDateTime = {
|
||
date: d,
|
||
};
|
||
}
|
||
|
||
// 🔹 Qaytish vaqti
|
||
let travelDateTime = undefined;
|
||
if (tour.travel_time) {
|
||
const d = new Date(tour.travel_time);
|
||
travelDateTime = {
|
||
date: d,
|
||
};
|
||
}
|
||
|
||
// 🔹 TransportS
|
||
const transports =
|
||
tour.transports?.map((t) => ({
|
||
transport: t.transport.id,
|
||
price: t.price ?? 0,
|
||
})) ?? [];
|
||
|
||
setTransportPrices(transports.map((t) => formatPrice(t.price ?? 0)));
|
||
|
||
// 🔹 Tarif
|
||
// const tariffs =
|
||
// tour.tariff?.map((t) => ({
|
||
// tariff: t.tariff ?? 0,
|
||
// price: t.price ?? 0,
|
||
// })) ?? [];
|
||
// setTarifDisplayPrice(tariffs.map((t) => formatPrice(t.price ?? 0)));
|
||
|
||
form.reset({
|
||
title: tour.title_uz ?? "",
|
||
title_ru: tour.title_ru ?? "",
|
||
price: tour.price ?? 0,
|
||
passenger_count: String(tour.passenger_count) ?? "1",
|
||
min_person: String(tour.min_person) ?? "1",
|
||
max_person: String(tour.max_person) ?? "1",
|
||
departure: tour.departure?.id.toString() ?? "",
|
||
// departure_ru: tour.departure_ru ?? "",
|
||
destination: tour.destination?.id.toString() ?? "",
|
||
// destination_ru: tour.destination_ru ?? "",
|
||
location_name: tour.location_name_uz ?? "",
|
||
location_name_ru: tour.location_name_ru ?? "",
|
||
hotel_info: tour.hotel_info_uz ?? "",
|
||
hotel_info_ru: tour.hotel_info_ru ?? "",
|
||
hotel_meals_info: tour.hotel_meals_uz ?? "",
|
||
hotel_meals_info_ru: tour.hotel_meals_ru ?? "",
|
||
languages: tour.languages ?? "",
|
||
duration: String(tour.duration_days) ?? "1",
|
||
visa_required: tour.visa_required ? "yes" : "no",
|
||
badges: tour.badge ?? [],
|
||
departureDateTime,
|
||
travelDateTime,
|
||
amenities: tour.ticket_amenities.map((e) => e.id),
|
||
hotel_services:
|
||
tour.ticket_included_services?.map((service) => ({
|
||
id: service.id,
|
||
image: service.image ?? null,
|
||
title: service.title_uz ?? service.title ?? "",
|
||
title_ru: service.title_ru ?? "",
|
||
description: service.desc_uz ?? service.desc ?? "",
|
||
desc_ru: service.desc_ru ?? "",
|
||
})) ?? [],
|
||
hotel_meals:
|
||
tour.ticket_hotel_meals?.map((meal) => ({
|
||
id: meal.id,
|
||
image: meal.image ?? null,
|
||
title: meal.name_uz ?? meal.name ?? "",
|
||
title_ru: meal.name_ru ?? "",
|
||
description: meal.desc_uz ?? meal.desc ?? "",
|
||
desc_ru: meal.desc_ru ?? "",
|
||
})) ?? [],
|
||
transport: transports,
|
||
// tarif: tariffs,
|
||
ticket_itinerary:
|
||
tour.ticket_itinerary?.map((item) => ({
|
||
id: item.id,
|
||
title: item.title_uz ?? item.title ?? "",
|
||
title_ru: item.title_ru ?? "",
|
||
duration: item.duration ?? 1,
|
||
ticket_itinerary_image:
|
||
item.ticket_itinerary_image?.map((img) => ({
|
||
image: img.image,
|
||
})) ?? [],
|
||
ticket_itinerary_destinations:
|
||
item.ticket_itinerary_destinations?.map((dest) => ({
|
||
name: dest.name_uz ?? dest.name ?? "",
|
||
name_ru: dest.name_ru ?? "",
|
||
})) ?? [],
|
||
})) ?? [],
|
||
banner: tour.image_banner ?? null,
|
||
images: tour.ticket_images.map((img) => ({
|
||
id: img.id,
|
||
image: img.image,
|
||
})),
|
||
extra_service:
|
||
tour.extra_service?.map((s) => ({
|
||
name: s.name_uz ?? s.name ?? "",
|
||
name_ru: s.name_ru ?? "",
|
||
})) ?? [],
|
||
paid_extra_service:
|
||
tour.paid_extra_service?.map((s) => ({
|
||
name: s.name_uz ?? s.name ?? "",
|
||
name_ru: s.name_ru ?? "",
|
||
price: s.price ?? 0,
|
||
})) ?? [],
|
||
});
|
||
|
||
// Display price
|
||
setDisplayPrice(formatPrice(tour.price ?? 0));
|
||
|
||
// TicketStore uchun id
|
||
setId(tour.id);
|
||
setSelectedCountry(data.data.departure?.country?.id);
|
||
setSearchCity(data.data.departure?.name);
|
||
setSelectedCountryDes(data.data.destination?.country.id);
|
||
setSearchCityDes(data.data.destination?.name);
|
||
}, [isEditMode, data, form, setId]);
|
||
|
||
const { watch, setValue } = form;
|
||
const selectedDate = watch("departureDateTime.date");
|
||
const selectedDateTravel = watch("travelDateTime.date");
|
||
|
||
const { mutate: create, isPending } = useMutation({
|
||
mutationFn: (body: FormData) => {
|
||
return createTours({ body });
|
||
},
|
||
onSuccess: (res) => {
|
||
queryClient.refetchQueries({ queryKey: ["popular_tours"] });
|
||
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||
queryClient.refetchQueries({ queryKey: ["tours_detail"] });
|
||
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
||
setId(res.data.data.id);
|
||
setStep(2);
|
||
},
|
||
onError: () => {
|
||
toast.error(t("Xatolik yuz berdi"), {
|
||
richColors: true,
|
||
position: "top-center",
|
||
});
|
||
},
|
||
});
|
||
|
||
const { mutate: update, isPending: updatePending } = useMutation({
|
||
mutationFn: ({ body, id }: { id: number; body: FormData }) => {
|
||
return updateTours({ body, id });
|
||
},
|
||
onSuccess: (res) => {
|
||
queryClient.refetchQueries({ queryKey: ["popular_tours"] });
|
||
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||
queryClient.refetchQueries({ queryKey: ["tours_detail"] });
|
||
queryClient.refetchQueries({ queryKey: ["hotel_detail"] });
|
||
setId(res.data.data.id);
|
||
setStep(2);
|
||
},
|
||
onError: () => {
|
||
toast.error(t("Xatolik yuz berdi"), {
|
||
richColors: true,
|
||
position: "top-center",
|
||
});
|
||
},
|
||
});
|
||
|
||
function onSubmit(value: z.infer<typeof TourformSchema>) {
|
||
const formData = new FormData();
|
||
const tour = data ? data.data : null;
|
||
// Asosiy ma'lumotlar
|
||
formData.append("title", value.title);
|
||
formData.append("location_name", value.location_name);
|
||
formData.append("location_name_ru", value.location_name_ru);
|
||
formData.append(
|
||
"visa_required",
|
||
value.visa_required === "yes" ? "true" : "false",
|
||
);
|
||
formData.append("title_ru", value.title_ru);
|
||
formData.append("price", String(value.price));
|
||
formData.append("min_person", String(value.min_person));
|
||
formData.append("max_person", String(value.max_person));
|
||
formData.append("departure", String(value.departure));
|
||
// formData.append("departure_ru", String(value.departure_ru));
|
||
formData.append("destination", String(value.destination));
|
||
// formData.append("destination_ru", String(value.destination_ru));
|
||
formData.append(
|
||
"departure_time",
|
||
value.departureDateTime.date?.toISOString(),
|
||
);
|
||
formData.append(
|
||
"travel_time",
|
||
String(value.travelDateTime.date.toISOString()),
|
||
);
|
||
formData.append("passenger_count", String(value.passenger_count));
|
||
if (value.languages) {
|
||
formData.append("languages", value.languages);
|
||
}
|
||
if (value.hotel_info) {
|
||
formData.append("hotel_info", value.hotel_info);
|
||
}
|
||
|
||
if (value.hotel_info_ru) {
|
||
formData.append("hotel_info_ru", value.hotel_info_ru);
|
||
}
|
||
if (value.hotel_meals_info) {
|
||
formData.append("hotel_meals", value.hotel_meals_info);
|
||
}
|
||
if (value.hotel_meals_info_ru) {
|
||
formData.append("hotel_meals_ru", value.hotel_meals_info_ru);
|
||
}
|
||
formData.append("duration_days", String(value.duration));
|
||
formData.append("rating", tour ? String(tour.rating) : "0.0");
|
||
|
||
if (value.banner instanceof File) {
|
||
formData.append("image_banner", value.banner);
|
||
}
|
||
|
||
// Tarif va transport
|
||
// value.tarif?.forEach((e, i) => {
|
||
// formData.append(`tariff[${i}]tariff`, String(e.tariff));
|
||
// formData.append(`tariff[${i}]price`, String(e.price));
|
||
// });
|
||
|
||
value.transport?.forEach((e, i) => {
|
||
formData.append(`transports[${i}]transport`, String(e.transport));
|
||
formData.append(`transports[${i}]price`, String(e.price));
|
||
});
|
||
|
||
value.badges?.forEach((e) => {
|
||
formData.append(`badge`, String(e));
|
||
});
|
||
|
||
value.amenities?.forEach((e) => {
|
||
formData.append(`ticket_amenities`, String(e));
|
||
});
|
||
|
||
value.images.forEach((e) => {
|
||
if (e.id) {
|
||
formData.append(`ticket_images_ids`, String(e.id));
|
||
} else if (e.id === undefined) {
|
||
formData.append(`ticket_images`, e.image!);
|
||
}
|
||
});
|
||
|
||
// 🔹 Hotel Services - edit rejimida ID yuborish, yangi bo'lsa to'liq ma'lumot yuborish
|
||
value.hotel_services &&
|
||
value.hotel_services.forEach((e, i) => {
|
||
if (e.id && typeof e.image === "string") {
|
||
formData.append(`ticket_included_services_ids`, String(e.id));
|
||
} else {
|
||
// Yangi xizmat yoki o'zgartirilgan xizmat
|
||
if (e.image instanceof File) {
|
||
formData.append(`ticket_included_services[${i}]image`, e.image);
|
||
}
|
||
formData.append(`ticket_included_services[${i}]title`, e.title);
|
||
formData.append(`ticket_included_services[${i}]title_ru`, e.title_ru);
|
||
formData.append(`ticket_included_services[${i}]desc_ru`, e.desc_ru);
|
||
formData.append(`ticket_included_services[${i}]desc`, e.description);
|
||
}
|
||
});
|
||
|
||
// 🔹 Ticket Itinerary - edit rejimida ID yuborish
|
||
value.ticket_itinerary?.forEach((itinerary, i) => {
|
||
if (itinerary.id) {
|
||
// Mavjud itinerary (o'zgartirilmagan)
|
||
const hasNewImages = itinerary.ticket_itinerary_image?.some(
|
||
(img) =>
|
||
img instanceof File ||
|
||
(typeof img === "object" &&
|
||
"image" in img &&
|
||
img.image instanceof File),
|
||
);
|
||
|
||
if (!hasNewImages) {
|
||
formData.append(`ticket_itinerary_ids`, String(itinerary.id));
|
||
return; // Faqat ID yuborish, boshqa ma'lumot kerak emas
|
||
}
|
||
}
|
||
|
||
// Yangi itinerary yoki o'zgartirilgan itinerary
|
||
formData.append(`ticket_itinerary[${i}]title`, itinerary.title);
|
||
formData.append(`ticket_itinerary[${i}]title_ru`, itinerary.title_ru);
|
||
formData.append(
|
||
`ticket_itinerary[${i}]duration`,
|
||
String(itinerary.duration),
|
||
);
|
||
|
||
// Rasmlar
|
||
if (Array.isArray(itinerary.ticket_itinerary_image)) {
|
||
itinerary.ticket_itinerary_image.forEach((img, j) => {
|
||
let file: File | null = null;
|
||
|
||
if (img instanceof File) {
|
||
file = img;
|
||
} else if (
|
||
typeof img === "object" &&
|
||
"image" in img &&
|
||
img.image instanceof File
|
||
) {
|
||
file = img.image;
|
||
}
|
||
|
||
if (file) {
|
||
formData.append(
|
||
`ticket_itinerary[${i}]ticket_itinerary_image[${j}]image`,
|
||
file,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
// Destinations
|
||
if (Array.isArray(itinerary.ticket_itinerary_destinations)) {
|
||
itinerary.ticket_itinerary_destinations.forEach((dest, k) => {
|
||
formData.append(
|
||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${k}]name`,
|
||
dest.name,
|
||
);
|
||
formData.append(
|
||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${k}]name_ru`,
|
||
dest.name_ru,
|
||
);
|
||
});
|
||
}
|
||
});
|
||
if (value.hotel_meals) {
|
||
value.hotel_meals.forEach((e, i) => {
|
||
if (e.id && typeof e.image === "string") {
|
||
// Mavjud meal (o'zgartirilmagan)
|
||
formData.append(`ticket_hotel_meals_ids`, String(e.id));
|
||
} else {
|
||
// Yangi meal yoki o'zgartirilgan meal
|
||
if (e.image instanceof File) {
|
||
formData.append(`ticket_hotel_meals[${i}]image`, e.image);
|
||
}
|
||
formData.append(`ticket_hotel_meals[${i}]name`, e.title);
|
||
formData.append(`ticket_hotel_meals[${i}]name_ru`, e.title_ru);
|
||
formData.append(`ticket_hotel_meals[${i}]desc`, e.description);
|
||
formData.append(`ticket_hotel_meals[${i}]desc_ru`, e.desc_ru);
|
||
}
|
||
});
|
||
}
|
||
// Extra services
|
||
value.extra_service &&
|
||
value.extra_service.forEach((e, i) => {
|
||
formData.append(`extra_service[${i}]name`, e.name);
|
||
formData.append(`extra_service[${i}]name_ru`, e.name_ru);
|
||
});
|
||
|
||
value.paid_extra_service &&
|
||
value.paid_extra_service.forEach((e, i) => {
|
||
formData.append(`paid_extra_service[${i}]name`, e.name);
|
||
formData.append(`paid_extra_service[${i}]name_ru`, e.name_ru);
|
||
formData.append(`paid_extra_service[${i}]price`, String(e.price));
|
||
});
|
||
|
||
if (isEditMode && id) {
|
||
update({
|
||
body: formData,
|
||
id: Number(id),
|
||
});
|
||
} else {
|
||
create(formData);
|
||
}
|
||
}
|
||
|
||
const { data: badge } = useQuery({
|
||
queryKey: ["all_badge"],
|
||
queryFn: () => hotelBadge({ page: 1, page_size: 10 }),
|
||
});
|
||
|
||
const { data: amenitiesData } = useQuery({
|
||
queryKey: ["all_amenities"],
|
||
queryFn: () => getAllAmenities({ page: 1, page_size: 10 }),
|
||
});
|
||
|
||
// const { data: tariff } = useQuery({
|
||
// queryKey: ["all_tarif"],
|
||
// queryFn: () => hotelTarif({ page: 1, page_size: 10 }),
|
||
// });
|
||
|
||
const [searchCountry, setSearchCountry] = useState<string>("");
|
||
const [searchCity, setSearchCity] = useState<string>("");
|
||
const [openCountry, setOpenCountry] = useState<boolean>(false);
|
||
const [openCity, setOpenCity] = useState<boolean>(false);
|
||
const [selectedCountry, setSelectedCountry] = useState<number | null>(null);
|
||
|
||
const { data: countryData, isLoading: countryLoad } = useQuery({
|
||
queryKey: ["all_country", searchCountry],
|
||
queryFn: () => countryList({ page: 1, page_size: 10, name: searchCountry }),
|
||
select: (res) => res.data.data,
|
||
});
|
||
|
||
const { data: regionData, isLoading: regionLoad } = useQuery({
|
||
queryKey: ["all_region", selectedCountry, searchCity],
|
||
queryFn: () =>
|
||
regionList({
|
||
page: 1,
|
||
page_size: 10,
|
||
country: selectedCountry!,
|
||
name: searchCity,
|
||
}),
|
||
select: (res) => res.data.data,
|
||
enabled: !!selectedCountry,
|
||
});
|
||
//destions select country
|
||
const [searchCountryDes, setSearchCountryDes] = useState<string>("");
|
||
const [searchCityDes, setSearchCityDes] = useState<string>("");
|
||
const [openCountryDes, setOpenCountryDes] = useState<boolean>(false);
|
||
const [openCityDes, setOpenCityDes] = useState<boolean>(false);
|
||
const [selectedCountryDes, setSelectedCountryDes] = useState<number | null>(
|
||
null,
|
||
);
|
||
const { data: countryDataDes, isLoading: countryLoadDes } = useQuery({
|
||
queryKey: ["all_country_des", searchCountryDes],
|
||
queryFn: () =>
|
||
countryList({ page: 1, page_size: 10, name: searchCountryDes }),
|
||
select: (res) => res.data.data,
|
||
});
|
||
|
||
const { data: regionDataDes, isLoading: regionLoadDes } = useQuery({
|
||
queryKey: ["all_region_des", selectedCountryDes, searchCityDes],
|
||
queryFn: () =>
|
||
regionList({
|
||
page: 1,
|
||
page_size: 10,
|
||
country: selectedCountryDes!,
|
||
name: searchCityDes,
|
||
}),
|
||
select: (res) => res.data.data,
|
||
enabled: !!selectedCountryDes,
|
||
});
|
||
|
||
const handleCountrySelect = (id: number | null) => {
|
||
setSelectedCountry(id);
|
||
form.setValue("departure", "");
|
||
setOpenCountry(false);
|
||
};
|
||
|
||
const handleCountrySelectDes = (id: number | null) => {
|
||
setSelectedCountryDes(id);
|
||
form.setValue("destination", "");
|
||
setOpenCountryDes(false);
|
||
};
|
||
|
||
const { data: transport } = useQuery({
|
||
queryKey: ["all_transport"],
|
||
queryFn: () => hotelTransport({ page: 1, page_size: 10 }),
|
||
});
|
||
|
||
console.log(form.formState.errors);
|
||
|
||
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">
|
||
<FormField
|
||
control={form.control}
|
||
name="title"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Sarlavha")}</Label>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="Toshkent - Dubay"
|
||
{...field}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="title_ru"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Sarlavha")} (ru)</Label>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="Ташкент - Дубай"
|
||
{...field}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="price"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">
|
||
{t("Narx")} {t("(1 kishi uchun)")}
|
||
</Label>
|
||
<FormControl>
|
||
<Input
|
||
type="text"
|
||
inputMode="numeric"
|
||
placeholder="1 500 000"
|
||
value={displayPrice}
|
||
onChange={(e) => {
|
||
const raw = e.target.value.replace(/\D/g, "");
|
||
const num = Number(raw);
|
||
if (!isNaN(num)) {
|
||
form.setValue("price", num);
|
||
setDisplayPrice(raw ? formatPrice(num) : "");
|
||
}
|
||
}}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="min_person"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">
|
||
{t("hamrohlar soni (eng kamida)")}
|
||
</Label>
|
||
<FormControl>
|
||
<Input
|
||
type="number"
|
||
inputMode="numeric"
|
||
placeholder="1"
|
||
{...field}
|
||
onChange={(e) =>
|
||
form.setValue("min_person", e.target.value)
|
||
}
|
||
className="h-12 !text-md
|
||
[&::-webkit-inner-spin-button]:appearance-none
|
||
[&::-webkit-outer-spin-button]:appearance-none
|
||
[appearance:textfield]"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="max_person"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">
|
||
{t("hamrohlar soni (eng ko'pida)")}
|
||
</Label>
|
||
<FormControl>
|
||
<Input
|
||
type="number"
|
||
inputMode="numeric"
|
||
placeholder="1"
|
||
{...field}
|
||
onChange={(e) =>
|
||
form.setValue("max_person", e.target.value)
|
||
}
|
||
className="h-12 !text-md
|
||
[&::-webkit-inner-spin-button]:appearance-none
|
||
[&::-webkit-outer-spin-button]:appearance-none
|
||
[appearance:textfield]"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-start">
|
||
<FormField
|
||
name="departure"
|
||
control={form.control}
|
||
render={({ field }) => {
|
||
const selectedCity = regionData?.results.find(
|
||
(u) => String(u.id) === field.value,
|
||
);
|
||
|
||
return (
|
||
<FormItem className="flex flex-col gap-2">
|
||
<Label className="text-md">Ketish joyi</Label>
|
||
{!selectedCountry && (
|
||
<Popover open={openCountry} onOpenChange={setOpenCountry}>
|
||
<PopoverTrigger asChild>
|
||
<FormControl>
|
||
<Button
|
||
variant="outline"
|
||
className={cn(
|
||
"w-full h-12 justify-between transition-all duration-200",
|
||
!selectedCountry && "text-muted-foreground",
|
||
)}
|
||
>
|
||
{selectedCountry
|
||
? countryData?.results.find(
|
||
(c) => c.id === selectedCountry,
|
||
)?.name
|
||
: "Davlatni tanlang"}
|
||
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
|
||
</Button>
|
||
</FormControl>
|
||
</PopoverTrigger>
|
||
<AnimatePresence mode="wait">
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||
>
|
||
<PopoverContent className="!w-96 p-0">
|
||
<Command shouldFilter={false}>
|
||
<CommandInput
|
||
placeholder="Qidirish..."
|
||
value={searchCountry}
|
||
onValueChange={setSearchCountry}
|
||
/>
|
||
<CommandList>
|
||
{countryLoad ? (
|
||
<div className="py-6 text-center">
|
||
<Loader2 className="animate-spin mx-auto" />
|
||
</div>
|
||
) : (
|
||
<CommandGroup>
|
||
{countryData?.results.map((c, index) => (
|
||
<motion.div
|
||
key={c.id}
|
||
initial={{ opacity: 0, x: -20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{
|
||
duration: 0.2,
|
||
delay: index * 0.03,
|
||
ease: "easeOut",
|
||
}}
|
||
>
|
||
<CommandItem
|
||
onSelect={() => {
|
||
handleCountrySelect(c.id);
|
||
setOpenCountry(false);
|
||
setTimeout(
|
||
() => setOpenCity(true),
|
||
200,
|
||
);
|
||
}}
|
||
className="cursor-pointer transition-colors"
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-4 transition-opacity duration-200",
|
||
selectedCountry === c.id
|
||
? "opacity-100"
|
||
: "opacity-0",
|
||
)}
|
||
/>
|
||
{c.name}
|
||
</CommandItem>
|
||
</motion.div>
|
||
))}
|
||
</CommandGroup>
|
||
)}
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</Popover>
|
||
)}
|
||
|
||
{/* CITY DROPDOWN */}
|
||
{selectedCountry && (
|
||
<Popover open={openCity} onOpenChange={setOpenCity}>
|
||
<PopoverTrigger asChild>
|
||
<FormControl>
|
||
<Button
|
||
variant="outline"
|
||
className={cn(
|
||
"w-full h-12 justify-between transition-all duration-200",
|
||
!field.value && "text-muted-foreground",
|
||
)}
|
||
>
|
||
{selectedCity
|
||
? selectedCity.name
|
||
: "Shaharni tanlang"}
|
||
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
|
||
</Button>
|
||
</FormControl>
|
||
</PopoverTrigger>
|
||
<AnimatePresence>
|
||
<motion.div
|
||
initial={{ opacity: 0, height: 0, y: -10 }}
|
||
animate={{
|
||
opacity: 1,
|
||
height: "auto",
|
||
y: 0,
|
||
transition: {
|
||
height: { duration: 0.3 },
|
||
opacity: { duration: 0.3, delay: 0.1 },
|
||
y: { duration: 0.3 },
|
||
},
|
||
}}
|
||
exit={{
|
||
opacity: 0,
|
||
height: 0,
|
||
y: -10,
|
||
transition: {
|
||
height: { duration: 0.25 },
|
||
opacity: { duration: 0.15 },
|
||
},
|
||
}}
|
||
style={{ overflow: "hidden" }}
|
||
>
|
||
<PopoverContent className="!w-96 p-0">
|
||
<Command shouldFilter={false}>
|
||
<CommandInput
|
||
placeholder="Qidirish..."
|
||
value={searchCity}
|
||
onValueChange={setSearchCity}
|
||
/>
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ delay: 0.1 }}
|
||
>
|
||
<Button
|
||
className="w-fit ml-2 p-1 cursor-pointer hover:bg-accent transition-colors"
|
||
size={"icon"}
|
||
variant={"ghost"}
|
||
onClick={() => {
|
||
handleCountrySelect(null);
|
||
setOpenCity(false);
|
||
setTimeout(() => setOpenCountry(true), 200);
|
||
}}
|
||
>
|
||
<MoveLeft className="mr-1" />
|
||
<p>Orqaga</p>
|
||
</Button>
|
||
</motion.div>
|
||
<CommandList>
|
||
{regionLoad ? (
|
||
<div className="py-6 text-center">
|
||
<Loader2 className="animate-spin mx-auto" />
|
||
</div>
|
||
) : (
|
||
<CommandGroup>
|
||
{regionData?.results.map((r, index) => (
|
||
<motion.div
|
||
key={r.id}
|
||
initial={{ opacity: 0, x: -20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{
|
||
duration: 0.2,
|
||
delay: index * 0.03,
|
||
ease: "easeOut",
|
||
}}
|
||
>
|
||
<CommandItem
|
||
onSelect={() => {
|
||
field.onChange(String(r.id));
|
||
setOpenCity(false);
|
||
}}
|
||
className="cursor-pointer transition-colors"
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-4 transition-opacity duration-200",
|
||
field.value === String(r.id)
|
||
? "opacity-100"
|
||
: "opacity-0",
|
||
)}
|
||
/>
|
||
{r.name}
|
||
</CommandItem>
|
||
</motion.div>
|
||
))}
|
||
</CommandGroup>
|
||
)}
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</Popover>
|
||
)}
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
);
|
||
}}
|
||
/>
|
||
|
||
{/* <FormField
|
||
control={form.control}
|
||
name="departure_ru"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Ketish joyi")} (ru)</Label>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="Ташкент"
|
||
{...field}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/> */}
|
||
|
||
<FormField
|
||
name="destination"
|
||
control={form.control}
|
||
render={({ field }) => {
|
||
const selectedCity = regionDataDes?.results.find(
|
||
(u) => String(u.id) === field.value,
|
||
);
|
||
|
||
return (
|
||
<FormItem className="flex flex-col gap-2">
|
||
<Label className="text-md">Borish joyi</Label>
|
||
|
||
{/* COUNTRY DROPDOWN */}
|
||
{!selectedCountryDes && (
|
||
<Popover
|
||
open={openCountryDes}
|
||
onOpenChange={setOpenCountryDes}
|
||
>
|
||
<PopoverTrigger asChild>
|
||
<FormControl>
|
||
<Button
|
||
variant="outline"
|
||
className={cn(
|
||
"w-full h-12 justify-between transition-all duration-200",
|
||
!selectedCountryDes && "text-muted-foreground",
|
||
)}
|
||
>
|
||
{selectedCountryDes
|
||
? countryDataDes?.results.find(
|
||
(c) => c.id === selectedCountryDes,
|
||
)?.name
|
||
: "Davlatni tanlang"}
|
||
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
|
||
</Button>
|
||
</FormControl>
|
||
</PopoverTrigger>
|
||
<AnimatePresence mode="wait">
|
||
<motion.div
|
||
initial={{ opacity: 0, y: -10 }}
|
||
animate={{ opacity: 1, y: 0 }}
|
||
exit={{ opacity: 0, y: -10 }}
|
||
transition={{ duration: 0.3, ease: "easeOut" }}
|
||
>
|
||
<PopoverContent className="!w-96 p-0">
|
||
<Command shouldFilter={false}>
|
||
<CommandInput
|
||
placeholder="Qidirish..."
|
||
value={searchCountryDes}
|
||
onValueChange={setSearchCountryDes}
|
||
/>
|
||
<CommandList>
|
||
{countryLoadDes ? (
|
||
<div className="py-6 text-center">
|
||
<Loader2 className="animate-spin mx-auto" />
|
||
</div>
|
||
) : (
|
||
<CommandGroup>
|
||
{countryDataDes?.results.map((c, index) => (
|
||
<motion.div
|
||
key={c.id}
|
||
initial={{ opacity: 0, x: -20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{
|
||
duration: 0.2,
|
||
delay: index * 0.03,
|
||
ease: "easeOut",
|
||
}}
|
||
>
|
||
<CommandItem
|
||
onSelect={() => {
|
||
handleCountrySelectDes(c.id);
|
||
setOpenCountryDes(false);
|
||
setTimeout(
|
||
() => setOpenCityDes(true),
|
||
200,
|
||
);
|
||
}}
|
||
className="cursor-pointer transition-colors"
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-4 transition-opacity duration-200",
|
||
selectedCountryDes === c.id
|
||
? "opacity-100"
|
||
: "opacity-0",
|
||
)}
|
||
/>
|
||
{c.name}
|
||
</CommandItem>
|
||
</motion.div>
|
||
))}
|
||
</CommandGroup>
|
||
)}
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</Popover>
|
||
)}
|
||
|
||
{selectedCountryDes && (
|
||
<Popover open={openCityDes} onOpenChange={setOpenCityDes}>
|
||
<PopoverTrigger asChild>
|
||
<FormControl>
|
||
<Button
|
||
variant="outline"
|
||
className={cn(
|
||
"w-full h-12 justify-between transition-all duration-200",
|
||
!field.value && "text-muted-foreground",
|
||
)}
|
||
>
|
||
{selectedCity
|
||
? selectedCity.name
|
||
: "Shaharni tanlang"}
|
||
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
|
||
</Button>
|
||
</FormControl>
|
||
</PopoverTrigger>
|
||
<AnimatePresence>
|
||
<motion.div
|
||
initial={{ opacity: 0, height: 0, y: -10 }}
|
||
animate={{
|
||
opacity: 1,
|
||
height: "auto",
|
||
y: 0,
|
||
transition: {
|
||
height: { duration: 0.3 },
|
||
opacity: { duration: 0.3, delay: 0.1 },
|
||
y: { duration: 0.3 },
|
||
},
|
||
}}
|
||
exit={{
|
||
opacity: 0,
|
||
height: 0,
|
||
y: -10,
|
||
transition: {
|
||
height: { duration: 0.25 },
|
||
opacity: { duration: 0.15 },
|
||
},
|
||
}}
|
||
style={{ overflow: "hidden" }}
|
||
>
|
||
<PopoverContent className="!w-96 p-0">
|
||
<Command shouldFilter={false}>
|
||
<CommandInput
|
||
placeholder="Qidirish..."
|
||
value={searchCityDes}
|
||
onValueChange={setSearchCityDes}
|
||
/>
|
||
<motion.div
|
||
initial={{ opacity: 0 }}
|
||
animate={{ opacity: 1 }}
|
||
transition={{ delay: 0.1 }}
|
||
>
|
||
<Button
|
||
className="w-fit ml-2 p-1 cursor-pointer hover:bg-accent transition-colors"
|
||
size={"icon"}
|
||
variant={"ghost"}
|
||
onClick={() => {
|
||
handleCountrySelectDes(null);
|
||
setOpenCityDes(false);
|
||
setTimeout(
|
||
() => setOpenCountryDes(true),
|
||
200,
|
||
);
|
||
}}
|
||
>
|
||
<MoveLeft className="mr-1" />
|
||
<p>Orqaga</p>
|
||
</Button>
|
||
</motion.div>
|
||
<CommandList>
|
||
{regionLoadDes ? (
|
||
<div className="py-6 text-center">
|
||
<Loader2 className="animate-spin mx-auto" />
|
||
</div>
|
||
) : (
|
||
<CommandGroup>
|
||
{regionDataDes?.results.map((r, index) => (
|
||
<motion.div
|
||
key={r.id}
|
||
initial={{ opacity: 0, x: -20 }}
|
||
animate={{ opacity: 1, x: 0 }}
|
||
transition={{
|
||
duration: 0.2,
|
||
delay: index * 0.03,
|
||
ease: "easeOut",
|
||
}}
|
||
>
|
||
<CommandItem
|
||
onSelect={() => {
|
||
field.onChange(String(r.id));
|
||
setOpenCityDes(false);
|
||
}}
|
||
className="cursor-pointer transition-colors"
|
||
>
|
||
<Check
|
||
className={cn(
|
||
"mr-2 h-4 transition-opacity duration-200",
|
||
field.value === String(r.id)
|
||
? "opacity-100"
|
||
: "opacity-0",
|
||
)}
|
||
/>
|
||
{r.name}
|
||
</CommandItem>
|
||
</motion.div>
|
||
))}
|
||
</CommandGroup>
|
||
)}
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</Popover>
|
||
)}
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
);
|
||
}}
|
||
/>
|
||
|
||
{/* <FormField
|
||
control={form.control}
|
||
name="destination_ru"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Borish joyi")} (ru)</Label>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="Дубай"
|
||
{...field}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/> */}
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="location_name"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Manzil nomi")}</Label>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="Dubai"
|
||
{...field}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="location_name_ru"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Manzil nomi")} (ru)</Label>
|
||
<FormControl>
|
||
<Input
|
||
placeholder="Дубай"
|
||
{...field}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="passenger_count"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Yo'lovchilar soni")}</Label>
|
||
<FormControl>
|
||
<Input
|
||
type="number"
|
||
inputMode="numeric"
|
||
placeholder="1"
|
||
{...field}
|
||
onChange={(e) =>
|
||
form.setValue("passenger_count", e.target.value)
|
||
}
|
||
className="h-12 !text-md
|
||
[&::-webkit-inner-spin-button]:appearance-none
|
||
[&::-webkit-outer-spin-button]:appearance-none
|
||
[appearance:textfield]"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-2 max-lg:grid-cols-1 gap-4">
|
||
<FormField
|
||
control={form.control}
|
||
name="departureDateTime.date"
|
||
render={() => (
|
||
<FormItem>
|
||
<div>
|
||
<div className="flex flex-col gap-3">
|
||
<Label htmlFor="date-picker" className="px-1 text-md">
|
||
{t("Ketish sanasi")}
|
||
</Label>
|
||
<Popover open={openDate} onOpenChange={setOpenDate}>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
id="date-picker"
|
||
className="w-full justify-between font-normal h-12"
|
||
>
|
||
{selectedDate
|
||
? selectedDate.toLocaleDateString()
|
||
: t("Sana tanlang")}
|
||
<ChevronDownIcon />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent
|
||
className="w-auto overflow-hidden p-0"
|
||
align="start"
|
||
>
|
||
<Calendar
|
||
mode="single"
|
||
selected={selectedDate}
|
||
captionLayout="dropdown"
|
||
onSelect={(date) => {
|
||
setValue("departureDateTime.date", date!);
|
||
setOpenDate(false);
|
||
}}
|
||
toYear={new Date().getFullYear() + 100}
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
</div>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
{/* <FormField
|
||
control={form.control}
|
||
name="departureDateTime.time"
|
||
render={() => (
|
||
<FormItem>
|
||
<div className="flex flex-col gap-3">
|
||
<Label htmlFor="time-picker" className="px-1 text-md">
|
||
{t("Ketish vaqti")}
|
||
</Label>
|
||
<Input
|
||
type="time"
|
||
id="time-picker"
|
||
step="1"
|
||
value={watch("departureDateTime.time")}
|
||
onChange={(e) =>
|
||
setValue("departureDateTime.time", e.target.value)
|
||
}
|
||
className="bg-background !h-12 appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||
/>
|
||
</div>
|
||
</FormItem>
|
||
)}
|
||
/> */}
|
||
<FormField
|
||
control={form.control}
|
||
name="travelDateTime.date"
|
||
render={() => (
|
||
<FormItem>
|
||
<div className="grid grid-cols-1 gap-4 max-lg:grid-cols-1">
|
||
<div className="flex flex-col gap-3">
|
||
<Label htmlFor="date-picker" className="px-1 text-md">
|
||
{t("Qaytish sanasi")}
|
||
</Label>
|
||
<Popover
|
||
open={openDateTravel}
|
||
onOpenChange={setOpenDateTravel}
|
||
>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
id="date-picker"
|
||
className="w-full justify-between font-normal h-12"
|
||
>
|
||
{selectedDateTravel
|
||
? selectedDateTravel.toLocaleDateString()
|
||
: t("Sana tanlang")}
|
||
<ChevronDownIcon />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent
|
||
className="w-auto overflow-hidden p-0"
|
||
align="start"
|
||
>
|
||
<Calendar
|
||
mode="single"
|
||
selected={selectedDateTravel}
|
||
captionLayout="dropdown"
|
||
onSelect={(date) => {
|
||
setValue("travelDateTime.date", date!);
|
||
setOpenDateTravel(false);
|
||
}}
|
||
toYear={new Date().getFullYear() + 100}
|
||
/>
|
||
</PopoverContent>
|
||
</Popover>
|
||
</div>
|
||
{/* <div className="flex flex-col gap-3">
|
||
<Label htmlFor="time-picker" className="px-1 text-md">
|
||
{t("Qaytish vaqti")}
|
||
</Label>
|
||
<Input
|
||
type="time"
|
||
id="time-picker"
|
||
step="1"
|
||
value={watch("travelDateTime.time")}
|
||
onChange={(e) =>
|
||
setValue("travelDateTime.time", e.target.value)
|
||
}
|
||
className="bg-background !h-12 appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||
/>
|
||
</div> */}
|
||
</div>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 max-lg:grid-cols-1 gap-6">
|
||
<FormField
|
||
control={form.control}
|
||
name="duration"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Tur davomiyligi")}</Label>
|
||
<FormControl>
|
||
<Input
|
||
type="number"
|
||
inputMode="numeric"
|
||
placeholder="1"
|
||
{...field}
|
||
onChange={(e) => form.setValue("duration", e.target.value)}
|
||
className="h-12 !text-md
|
||
[&::-webkit-inner-spin-button]:appearance-none
|
||
[&::-webkit-outer-spin-button]:appearance-none
|
||
[appearance:textfield]"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
<FormField
|
||
control={form.control}
|
||
name="languages"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Tillar")}</Label>
|
||
<FormControl>
|
||
<Input
|
||
placeholder={t("Har bir tilni vergul (,) bilan ajrating")}
|
||
{...field}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
<div className="grid grid-cols-2 max-lg:grid-cols-1 gap-6 items-end">
|
||
<FormField
|
||
control={form.control}
|
||
name="badges"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Belgilar (Badge)")}</Label>
|
||
|
||
<div className="flex flex-wrap gap-2 mb-3">
|
||
{(form.watch("badges") ?? []).length > 0 &&
|
||
(form.watch("badges") ?? []).map((badgeId: number) => {
|
||
const badgeItem = badge?.data?.data?.results?.find(
|
||
(b: any) => b.id === badgeId,
|
||
);
|
||
|
||
return (
|
||
<Badge
|
||
key={badgeId}
|
||
variant="secondary"
|
||
className="px-3 py-1 text-sm flex items-center gap-2"
|
||
>
|
||
{badgeItem?.name || `Badge #${badgeId}`}
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current = form.getValues("badges") ?? [];
|
||
form.setValue(
|
||
"badges",
|
||
current.filter((b: number) => b !== badgeId),
|
||
);
|
||
}}
|
||
className="ml-1 text-muted-foreground hover:text-destructive"
|
||
>
|
||
<XIcon className="size-4" />
|
||
</button>
|
||
</Badge>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
className="w-full h-12 justify-between font-normal"
|
||
>
|
||
{t("Belgilarni tanlang")}
|
||
<ChevronDownIcon />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
|
||
<PopoverContent className="p-0 w-[250px]" align="start">
|
||
<Command>
|
||
<CommandList>
|
||
<CommandGroup heading={t("Mavjud belgilar")}>
|
||
{badge?.data?.data?.results?.map((item: any) => {
|
||
const currentBadges =
|
||
form.getValues("badges") ?? [];
|
||
const selected = currentBadges.includes(item.id);
|
||
|
||
return (
|
||
<CommandItem
|
||
key={item.id}
|
||
onSelect={() => {
|
||
if (selected) {
|
||
form.setValue(
|
||
"badges",
|
||
currentBadges.filter(
|
||
(b: number) => b !== item.id,
|
||
),
|
||
);
|
||
} else {
|
||
form.setValue("badges", [
|
||
...currentBadges,
|
||
item.id,
|
||
]);
|
||
}
|
||
}}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<SquareCheckBig
|
||
className={`size-4 ${
|
||
selected
|
||
? "text-green-500"
|
||
: "text-muted-foreground"
|
||
}`}
|
||
/>
|
||
{item.name}
|
||
</CommandItem>
|
||
);
|
||
})}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="amenities"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Qulayliklar")}</Label>
|
||
|
||
<div className="flex flex-wrap gap-2 mb-3">
|
||
{(form.watch("amenities") ?? []).length > 0 &&
|
||
(form.watch("amenities") ?? []).map((badgeId: number) => {
|
||
const badgeItem =
|
||
amenitiesData?.data?.data?.results?.find(
|
||
(b: any) => b.id === badgeId,
|
||
);
|
||
|
||
return (
|
||
<Badge
|
||
key={badgeId}
|
||
variant="secondary"
|
||
className="px-3 py-1 text-sm flex items-center gap-2"
|
||
>
|
||
{badgeItem?.name || `Qulaylilar #${badgeId}`}
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current = form.getValues("amenities") ?? [];
|
||
form.setValue(
|
||
"amenities",
|
||
current.filter((b: number) => b !== badgeId),
|
||
);
|
||
}}
|
||
className="ml-1 text-muted-foreground hover:text-destructive"
|
||
>
|
||
<XIcon className="size-4" />
|
||
</button>
|
||
</Badge>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
className="w-full h-12 justify-between font-normal"
|
||
>
|
||
{t("Qulaylik tanlang")}
|
||
<ChevronDownIcon />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
|
||
<PopoverContent className="p-0 w-[250px]" align="start">
|
||
<Command>
|
||
<CommandList>
|
||
<CommandGroup heading={t("Mavjud qulayliklar")}>
|
||
{amenitiesData?.data?.data?.results?.map(
|
||
(item: any) => {
|
||
const currentBadges =
|
||
form.getValues("amenities") ?? [];
|
||
const selected = currentBadges.includes(item.id);
|
||
|
||
return (
|
||
<CommandItem
|
||
key={item.id}
|
||
onSelect={() => {
|
||
const selected = currentBadges.includes(
|
||
item.id,
|
||
);
|
||
|
||
if (selected) {
|
||
removeAmenity(item.id);
|
||
form.setValue(
|
||
"amenities",
|
||
currentBadges.filter(
|
||
(b: number) => b !== item.id,
|
||
),
|
||
);
|
||
} else {
|
||
addAmenity(item.id);
|
||
form.setValue("amenities", [
|
||
...currentBadges,
|
||
item.id,
|
||
]);
|
||
}
|
||
}}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<SquareCheckBig
|
||
className={`size-4 ${
|
||
selected
|
||
? "text-green-500"
|
||
: "text-muted-foreground"
|
||
}`}
|
||
/>
|
||
{item.name}
|
||
</CommandItem>
|
||
);
|
||
},
|
||
)}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
{/* <FormField
|
||
control={form.control}
|
||
name="tarif"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Tariflar")}</Label>
|
||
<div className="flex flex-col gap-3 mb-3">
|
||
{form.watch("tarif")?.map((tr, idx) => {
|
||
const transportItem = tariff?.data?.data?.results?.find(
|
||
(item: any) => item.id === tr.tariff,
|
||
);
|
||
|
||
return (
|
||
<div
|
||
key={idx}
|
||
className="flex items-center justify-between border rounded-lg p-2"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<Badge
|
||
variant="secondary"
|
||
className="px-3 py-1 text-sm"
|
||
>
|
||
{transportItem?.name || `Tarif #${tr.tariff}`}
|
||
</Badge>
|
||
<Input
|
||
type="text"
|
||
inputMode="numeric"
|
||
placeholder="1 500 000"
|
||
value={tarifdisplayPrice[idx] || ""}
|
||
onChange={(e) => {
|
||
const raw = e.target.value.replace(/\D/g, "");
|
||
const num = Number(raw);
|
||
const currentTarifs =
|
||
form.getValues("tarif") || [];
|
||
const updatedTransport = currentTarifs.map(
|
||
(t, i) =>
|
||
i === idx ? { ...t, price: num } : t,
|
||
);
|
||
|
||
form.setValue("tarif", updatedTransport);
|
||
|
||
// Local holatda ham yangilaymiz:
|
||
const updatedPrices = [...tarifdisplayPrice];
|
||
updatedPrices[idx] = raw ? formatPrice(num) : "0";
|
||
setTarifDisplayPrice(updatedPrices);
|
||
}}
|
||
className="h-12 !text-md"
|
||
/>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current = form.getValues("tarif") || [];
|
||
form.setValue(
|
||
"tarif",
|
||
current.filter((_, i) => i !== idx),
|
||
);
|
||
const updatedPrices = tarifdisplayPrice.filter(
|
||
(_, i) => i !== idx,
|
||
);
|
||
setTarifDisplayPrice(updatedPrices);
|
||
}}
|
||
className="ml-2 text-muted-foreground hover:text-destructive"
|
||
>
|
||
<XIcon className="size-4" />
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
className="w-full h-12 justify-between font-normal"
|
||
>
|
||
{t("Tarifni tanlang")}
|
||
<ChevronDownIcon />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="p-0 w-[250px]" align="start">
|
||
<Command>
|
||
<CommandList>
|
||
<CommandGroup heading={t("Mavjud tariflar")}>
|
||
{tariff?.data?.data?.results?.map((item: any) => {
|
||
const currentTarifs = form.getValues("tarif") || [];
|
||
const selected = currentTarifs.some(
|
||
(t) => t.tariff === item.id,
|
||
);
|
||
|
||
return (
|
||
<CommandItem
|
||
key={item.id}
|
||
onSelect={() => {
|
||
const current = form.getValues("tarif") || [];
|
||
if (selected) {
|
||
form.setValue(
|
||
"tarif",
|
||
current.filter(
|
||
(t) => t.tariff !== item.id,
|
||
),
|
||
);
|
||
} else {
|
||
form.setValue("tarif", [
|
||
...current,
|
||
{ tariff: item.id, price: 0 },
|
||
]);
|
||
}
|
||
}}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<SquareCheckBig
|
||
className={`size-4 ${
|
||
selected
|
||
? "text-green-500"
|
||
: "text-muted-foreground"
|
||
}`}
|
||
/>
|
||
{item.name}
|
||
</CommandItem>
|
||
);
|
||
})}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/> */}
|
||
</div>
|
||
|
||
<div className="grid grid-cols-2 max-lg:grid-cols-1 gap-6 items-end">
|
||
<FormField
|
||
control={form.control}
|
||
name="transport"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Transportlar")}</Label>
|
||
|
||
<div className="flex flex-col gap-3 mb-3">
|
||
{form.watch("transport")?.map((tr, idx) => {
|
||
const transportItem = transport?.data?.data?.results?.find(
|
||
(item: any) => item.id === tr.transport,
|
||
);
|
||
return (
|
||
<div
|
||
key={idx}
|
||
className="flex items-center justify-between border rounded-lg p-2"
|
||
>
|
||
<div className="flex items-center gap-2">
|
||
<Badge
|
||
variant="secondary"
|
||
className="px-3 py-1 text-sm"
|
||
>
|
||
{transportItem?.name ||
|
||
`Transport #${tr.transport}`}
|
||
</Badge>
|
||
{/* <Input
|
||
type="text"
|
||
inputMode="numeric"
|
||
placeholder="1 500 000"
|
||
value={transportPrices[idx] || ""}
|
||
onChange={(e) => {
|
||
const raw = e.target.value.replace(/\D/g, "");
|
||
const num = Number(raw);
|
||
const currentTransports =
|
||
form.getValues("transport") || [];
|
||
const updatedTransport = currentTransports.map(
|
||
(t, i) =>
|
||
i === idx ? { ...t, price: num } : t,
|
||
);
|
||
|
||
form.setValue("transport", updatedTransport);
|
||
|
||
// Local holatda ham yangilaymiz:
|
||
const updatedPrices = [...transportPrices];
|
||
updatedPrices[idx] = raw ? formatPrice(num) : "";
|
||
setTransportPrices(updatedPrices);
|
||
}}
|
||
className="h-12 !text-md"
|
||
/> */}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current = form.getValues("transport") || [];
|
||
form.setValue(
|
||
"transport",
|
||
current.filter((_, i) => i !== idx),
|
||
);
|
||
const updatedPrices = transportPrices.filter(
|
||
(_, i) => i !== idx,
|
||
);
|
||
setTransportPrices(updatedPrices);
|
||
}}
|
||
className="ml-2 text-muted-foreground hover:text-destructive"
|
||
>
|
||
<XIcon className="size-4" />
|
||
</button>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
|
||
<Popover>
|
||
<PopoverTrigger asChild>
|
||
<Button
|
||
variant="outline"
|
||
className="w-full h-12 justify-between font-normal"
|
||
>
|
||
{t("Transport tanlang")}
|
||
<ChevronDownIcon />
|
||
</Button>
|
||
</PopoverTrigger>
|
||
|
||
<PopoverContent className="p-0 w-[250px]" align="start">
|
||
<Command>
|
||
<CommandList>
|
||
<CommandGroup heading={t("Mavjud transportlar")}>
|
||
{transport?.data?.data?.results?.map((item: any) => {
|
||
const currentTransports =
|
||
form.getValues("transport") || [];
|
||
const selected = currentTransports.some(
|
||
(t) => t.transport === item.id,
|
||
);
|
||
|
||
return (
|
||
<CommandItem
|
||
key={item.id}
|
||
onSelect={() => {
|
||
const current =
|
||
form.getValues("transport") || [];
|
||
if (selected) {
|
||
form.setValue(
|
||
"transport",
|
||
current.filter(
|
||
(t) => t.transport !== item.id,
|
||
),
|
||
);
|
||
} else {
|
||
form.setValue("transport", [
|
||
...current,
|
||
{ transport: item.id, price: 0 },
|
||
]);
|
||
}
|
||
}}
|
||
className="flex items-center gap-2"
|
||
>
|
||
<SquareCheckBig
|
||
className={`size-4 ${
|
||
selected
|
||
? "text-green-500"
|
||
: "text-muted-foreground"
|
||
}`}
|
||
/>
|
||
{item.name}
|
||
</CommandItem>
|
||
);
|
||
})}
|
||
</CommandGroup>
|
||
</CommandList>
|
||
</Command>
|
||
</PopoverContent>
|
||
</Popover>
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
</div>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="visa_required"
|
||
render={({ field }) => (
|
||
<FormItem className="space-y-3">
|
||
<Label>{t("Visa talab qilinadimi")}?</Label>
|
||
<FormControl>
|
||
<RadioGroup
|
||
onValueChange={field.onChange}
|
||
value={field.value}
|
||
className="flex gap-6"
|
||
>
|
||
<div className="flex items-center space-x-2">
|
||
<RadioGroupItem value="yes" id="visa_yes" />
|
||
<label htmlFor="visa_yes">{t("Ha")}</label>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<RadioGroupItem value="no" id="visa_no" />
|
||
<label htmlFor="visa_no">{t("Yo'q")}</label>
|
||
</div>
|
||
</RadioGroup>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<TicketsImagesModel
|
||
form={form}
|
||
name="banner"
|
||
multiple={false}
|
||
label={t("Banner")}
|
||
imageUrl={data?.data?.image_banner}
|
||
/>
|
||
|
||
<TicketsImagesModel
|
||
form={form}
|
||
name="images"
|
||
label={t("Qo'shimcha rasmlar")}
|
||
includeId={true}
|
||
imageUrl={
|
||
isEditMode && data?.data?.ticket_images
|
||
? data.data.ticket_images.map((img) => ({
|
||
id: img.id,
|
||
image: img.image,
|
||
}))
|
||
: undefined
|
||
}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="extra_service"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Bepul xizmatlar")}</Label>
|
||
|
||
<div className="flex flex-col gap-4">
|
||
{/* Ko'rsatilayotgan xizmatlar */}
|
||
<div className="flex flex-wrap gap-2">
|
||
{(form.watch("extra_service") ?? []).map((item, idx) => (
|
||
<Badge
|
||
key={idx}
|
||
variant="secondary"
|
||
className="px-3 py-1 text-sm flex items-center gap-2"
|
||
>
|
||
<span>{item.name}</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current = form.getValues("extra_service") ?? [];
|
||
form.setValue(
|
||
"extra_service",
|
||
current.filter((_, i) => i !== idx),
|
||
);
|
||
}}
|
||
className="ml-1 text-muted-foreground hover:text-destructive"
|
||
>
|
||
<XIcon className="size-4" />
|
||
</button>
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
|
||
{/* Yangi xizmat qo'shish */}
|
||
<div className="flex gap-3 items-end flex-wrap">
|
||
<Input
|
||
id="extra_service_name"
|
||
placeholder={t("Xizmat nomi")}
|
||
className="h-12 !text-md flex-1 min-w-[200px]"
|
||
/>
|
||
|
||
<Input
|
||
id="extra_service_name_ru"
|
||
placeholder={t("Xizmat nomi") + " (ru)"}
|
||
className="h-12 !text-md flex-1 min-w-[200px]"
|
||
/>
|
||
|
||
<Button
|
||
type="button"
|
||
onClick={() => {
|
||
const nameInput = document.getElementById(
|
||
"extra_service_name",
|
||
) as HTMLInputElement;
|
||
const nameRuInput = document.getElementById(
|
||
"extra_service_name_ru",
|
||
) as HTMLInputElement;
|
||
|
||
if (nameInput.value && nameRuInput.value) {
|
||
const current = form.getValues("extra_service") ?? [];
|
||
form.setValue("extra_service", [
|
||
...current,
|
||
{
|
||
name: nameInput.value,
|
||
name_ru: nameRuInput.value,
|
||
},
|
||
]);
|
||
nameInput.value = "";
|
||
nameRuInput.value = "";
|
||
}
|
||
}}
|
||
className="h-12"
|
||
>
|
||
{t("Qo'shish")}
|
||
</Button>
|
||
</div>
|
||
|
||
<FormMessage />
|
||
</div>
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="paid_extra_service"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Pullik xizmatlar")}</Label>
|
||
|
||
<div className="flex flex-col gap-4">
|
||
{/* Ro'yxat */}
|
||
<div className="flex flex-wrap gap-2">
|
||
{(form.watch("paid_extra_service") || []).map((item, idx) => (
|
||
<Badge
|
||
key={idx}
|
||
variant="secondary"
|
||
className="px-3 py-1 text-sm flex items-center gap-2"
|
||
>
|
||
<span>
|
||
{item.name} —{" "}
|
||
<strong>{formatPrice(item.price, true)}</strong>
|
||
</span>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current =
|
||
form.getValues("paid_extra_service") ?? [];
|
||
form.setValue(
|
||
"paid_extra_service",
|
||
current.filter((_, i) => i !== idx),
|
||
);
|
||
}}
|
||
className="ml-1 text-muted-foreground hover:text-destructive"
|
||
>
|
||
<XIcon className="size-4" />
|
||
</button>
|
||
</Badge>
|
||
))}
|
||
</div>
|
||
|
||
{/* Qo'shish formasi */}
|
||
<div className="flex gap-3 items-end flex-wrap">
|
||
<Input
|
||
id="paid_service_name"
|
||
placeholder={t("Xizmat nomi")}
|
||
className="h-12 !text-md flex-1 min-w-[200px]"
|
||
/>
|
||
|
||
<Input
|
||
id="paid_service_name_ru"
|
||
placeholder={t("Xizmat nomi") + " (ru)"}
|
||
className="h-12 !text-md flex-1 min-w-[200px]"
|
||
/>
|
||
|
||
<Input
|
||
id="paid_service_price"
|
||
type="text"
|
||
inputMode="numeric"
|
||
placeholder="1 500 000"
|
||
className="h-12 !text-md w-[150px]"
|
||
onInput={(e) => {
|
||
const input = e.target as HTMLInputElement;
|
||
const raw = input.value.replace(/\D/g, "");
|
||
input.value = raw
|
||
? Number(raw).toLocaleString("ru-RU")
|
||
: "";
|
||
}}
|
||
/>
|
||
|
||
{/* Qo'shish tugmasi */}
|
||
<Button
|
||
type="button"
|
||
onClick={() => {
|
||
const nameInput = document.getElementById(
|
||
"paid_service_name",
|
||
) as HTMLInputElement;
|
||
const nameRuInput = document.getElementById(
|
||
"paid_service_name_ru",
|
||
) as HTMLInputElement;
|
||
const priceInput = document.getElementById(
|
||
"paid_service_price",
|
||
) as HTMLInputElement;
|
||
|
||
const raw = priceInput.value.replace(/\D/g, "");
|
||
const num = Number(raw);
|
||
|
||
if (
|
||
nameInput.value.trim() &&
|
||
nameRuInput.value.trim() &&
|
||
!isNaN(num)
|
||
) {
|
||
const current =
|
||
form.getValues("paid_extra_service") ?? [];
|
||
form.setValue("paid_extra_service", [
|
||
...current,
|
||
{
|
||
name: nameInput.value.trim(),
|
||
name_ru: nameRuInput.value.trim(),
|
||
price: num, // 🟢 0 ham bo‘lishi mumkin
|
||
},
|
||
]);
|
||
|
||
// inputlarni tozalaymiz
|
||
nameInput.value = "";
|
||
nameRuInput.value = "";
|
||
priceInput.value = "";
|
||
}
|
||
}}
|
||
className="h-12"
|
||
>
|
||
{t("Qo'shish")}
|
||
</Button>
|
||
</div>
|
||
|
||
<FormMessage />
|
||
</div>
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="hotel_info"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Mehmonxona haqida")}</Label>
|
||
<FormControl>
|
||
<Textarea
|
||
placeholder={t("Mehmonxona haqida")}
|
||
{...field}
|
||
className="min-h-48 max-h-60 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="hotel_info_ru"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Mehmonxona haqida")} (ru)</Label>
|
||
<FormControl>
|
||
<Textarea
|
||
placeholder={t("Mehmonxona haqida") + " (ru)"}
|
||
{...field}
|
||
className="min-h-48 max-h-60 !text-md"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="hotel_services"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Mehmonxona xizmatlari")}</Label>
|
||
|
||
<div className="flex flex-col gap-4">
|
||
<div className="flex flex-wrap gap-4">
|
||
{form.watch("hotel_services")?.map((service, idx) => (
|
||
<div
|
||
key={idx}
|
||
className="relative w-48 border rounded-xl overflow-hidden shadow-sm"
|
||
>
|
||
<img
|
||
src={
|
||
service.image instanceof File
|
||
? URL.createObjectURL(service.image)
|
||
: service.image
|
||
}
|
||
alt={service.title}
|
||
className="object-cover w-full h-32"
|
||
/>
|
||
<div className="p-2 text-center">
|
||
<p className="font-semibold">{service.title}</p>
|
||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||
{service.description}
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current =
|
||
form.getValues("hotel_services") ?? [];
|
||
form.setValue(
|
||
"hotel_services",
|
||
current.filter((_, i) => i !== idx),
|
||
);
|
||
}}
|
||
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 hover:bg-white shadow"
|
||
>
|
||
<XIcon className="size-4 text-destructive" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-3 border rounded-xl p-4 bg-muted/10">
|
||
<Label className="text-md font-semibold">
|
||
{t("Yangi xizmat qo'shish")}
|
||
</Label>
|
||
|
||
<Input
|
||
type="file"
|
||
id="hotel_service_image"
|
||
accept="image/*"
|
||
className="h-12"
|
||
/>
|
||
|
||
<Input
|
||
id="hotel_service_title"
|
||
placeholder={t("Xizmat nomi")}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Input
|
||
id="hotel_service_title_ru"
|
||
placeholder={t("Xizmat nomi") + " (ru)"}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Textarea
|
||
id="hotel_service_desc"
|
||
placeholder={t("Xizmat tavsifi")}
|
||
className="min-h-24 !text-md"
|
||
/>
|
||
|
||
<Textarea
|
||
id="hotel_service_desc_ru"
|
||
placeholder={t("Xizmat tavsifi") + " (ru)"}
|
||
className="min-h-24 !text-md"
|
||
/>
|
||
|
||
<Button
|
||
type="button"
|
||
onClick={() => {
|
||
const imgInput = document.getElementById(
|
||
"hotel_service_image",
|
||
) as HTMLInputElement;
|
||
const titleInput = document.getElementById(
|
||
"hotel_service_title",
|
||
) as HTMLInputElement;
|
||
const titleRuInput = document.getElementById(
|
||
"hotel_service_title_ru",
|
||
) as HTMLInputElement;
|
||
const descInput = document.getElementById(
|
||
"hotel_service_desc",
|
||
) as HTMLTextAreaElement;
|
||
const descRuInput = document.getElementById(
|
||
"hotel_service_desc_ru",
|
||
) as HTMLTextAreaElement;
|
||
|
||
const file = imgInput.files?.[0];
|
||
if (file && titleInput.value && descInput.value) {
|
||
const current = form.getValues("hotel_services") || [];
|
||
form.setValue("hotel_services", [
|
||
...current,
|
||
{
|
||
image: file,
|
||
title: titleInput.value,
|
||
title_ru: titleRuInput.value,
|
||
description: descInput.value,
|
||
desc_ru: descRuInput.value,
|
||
},
|
||
]);
|
||
|
||
imgInput.value = "";
|
||
titleInput.value = "";
|
||
titleRuInput.value = "";
|
||
descInput.value = "";
|
||
descRuInput.value = "";
|
||
}
|
||
}}
|
||
className="h-12"
|
||
>
|
||
{t("Qo'shish")}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="hotel_meals_info"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">
|
||
{t("Mehmonxona taomlari haqida")}
|
||
</Label>
|
||
<FormControl>
|
||
<Textarea
|
||
placeholder={t("Mehmonxona taomlari haqida")}
|
||
{...field}
|
||
className="min-h-48 max-h-60"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="hotel_meals_info_ru"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<Label className="text-md">
|
||
{t("Mehmonxona taomlari haqida")} (ru)
|
||
</Label>
|
||
<FormControl>
|
||
<Textarea
|
||
placeholder={t("Mehmonxona taomlari haqida") + " (ru)"}
|
||
{...field}
|
||
className="min-h-48 max-h-60"
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="hotel_meals"
|
||
render={() => (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Mehmonxona taomlari")}</Label>
|
||
|
||
<div className="flex flex-col gap-4">
|
||
<div className="flex flex-wrap gap-4">
|
||
{form.watch("hotel_meals")?.map((service, idx) => (
|
||
<div
|
||
key={idx}
|
||
className="relative w-48 border rounded-xl overflow-hidden shadow-sm"
|
||
>
|
||
<img
|
||
src={
|
||
service.image instanceof File
|
||
? URL.createObjectURL(service.image)
|
||
: service.image
|
||
}
|
||
alt={service.title}
|
||
className="object-cover w-full h-32"
|
||
/>
|
||
<div className="p-2 text-center">
|
||
<p className="font-semibold">{service.title}</p>
|
||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||
{service.description}
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current = form.getValues("hotel_meals") || [];
|
||
form.setValue(
|
||
"hotel_meals",
|
||
current.filter((_, i) => i !== idx),
|
||
);
|
||
}}
|
||
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 hover:bg-white shadow"
|
||
>
|
||
<XIcon className="size-4 text-destructive" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<div className="flex flex-col gap-3 border rounded-xl p-4 bg-muted/10">
|
||
<Label className="text-md font-semibold">
|
||
{t("Mehmonxona taomlari ro'yxati")}
|
||
</Label>
|
||
|
||
<Input
|
||
type="file"
|
||
id="hotel_meals_image"
|
||
accept="image/*"
|
||
className="h-12"
|
||
/>
|
||
|
||
<Input
|
||
id="hotel_meals_title"
|
||
placeholder={t("Taom nomi")}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Input
|
||
id="hotel_meals_title_ru"
|
||
placeholder={t("Taom nomi") + " (ru)"}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Textarea
|
||
id="hotel_meals_desc"
|
||
placeholder={t("Taom tavsifi")}
|
||
className="min-h-24 !text-md"
|
||
/>
|
||
|
||
<Textarea
|
||
id="hotel_meals_desc_ru"
|
||
placeholder={t("Taom tavsifi") + " (ru)"}
|
||
className="min-h-24 !text-md"
|
||
/>
|
||
|
||
<Button
|
||
type="button"
|
||
onClick={() => {
|
||
const imgInput = document.getElementById(
|
||
"hotel_meals_image",
|
||
) as HTMLInputElement;
|
||
const titleInput = document.getElementById(
|
||
"hotel_meals_title",
|
||
) as HTMLInputElement;
|
||
const titleRuInput = document.getElementById(
|
||
"hotel_meals_title_ru",
|
||
) as HTMLInputElement;
|
||
const descInput = document.getElementById(
|
||
"hotel_meals_desc",
|
||
) as HTMLTextAreaElement;
|
||
const descRuInput = document.getElementById(
|
||
"hotel_meals_desc_ru",
|
||
) as HTMLTextAreaElement;
|
||
|
||
const file = imgInput.files?.[0];
|
||
if (file && titleInput.value && descInput.value) {
|
||
const current = form.getValues("hotel_meals") ?? [];
|
||
form.setValue("hotel_meals", [
|
||
...current,
|
||
{
|
||
image: file,
|
||
title: titleInput.value,
|
||
title_ru: titleRuInput.value,
|
||
description: descInput.value,
|
||
desc_ru: descRuInput.value,
|
||
},
|
||
]);
|
||
imgInput.value = "";
|
||
titleInput.value = "";
|
||
titleRuInput.value = "";
|
||
descInput.value = "";
|
||
descRuInput.value = "";
|
||
}
|
||
}}
|
||
className="h-12"
|
||
>
|
||
{t("Qo'shish")}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="ticket_itinerary"
|
||
render={() => {
|
||
// Helper function to get image source
|
||
const getImageSrc = (imageItem: any): string => {
|
||
// Agar object bo'lsa va image property bo'lsa
|
||
if (
|
||
imageItem &&
|
||
typeof imageItem === "object" &&
|
||
"image" in imageItem
|
||
) {
|
||
const img = imageItem.image;
|
||
// File bo'lsa
|
||
if (img instanceof File) {
|
||
return URL.createObjectURL(img);
|
||
}
|
||
// String (URL) bo'lsa
|
||
if (typeof img === "string") {
|
||
return img;
|
||
}
|
||
}
|
||
// Agar to'g'ridan-to'g'ri File bo'lsa
|
||
if (imageItem instanceof File) {
|
||
return URL.createObjectURL(imageItem);
|
||
}
|
||
// Agar string bo'lsa
|
||
if (typeof imageItem === "string") {
|
||
return imageItem;
|
||
}
|
||
// Default fallback
|
||
return "";
|
||
};
|
||
|
||
return (
|
||
<FormItem>
|
||
<Label className="text-md">{t("Yo'nalishlar")}</Label>
|
||
|
||
<div className="flex flex-col gap-4">
|
||
{/* mavjud yo'nalishlar */}
|
||
<div className="flex flex-wrap gap-4">
|
||
{form.watch("ticket_itinerary")?.map((item, idx) => (
|
||
<div
|
||
key={idx}
|
||
className="relative w-48 border rounded-xl overflow-hidden shadow-sm"
|
||
>
|
||
<img
|
||
src={getImageSrc(item.ticket_itinerary_image[0])}
|
||
alt={item.title}
|
||
className="object-cover w-full h-32"
|
||
/>
|
||
<div className="p-2 text-center">
|
||
<p className="font-semibold">{item.title}</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{item.ticket_itinerary_destinations[0]?.name}
|
||
</p>
|
||
<p className="text-sm text-muted-foreground">
|
||
{item.duration} {t("kun")}
|
||
</p>
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={() => {
|
||
const current =
|
||
form.getValues("ticket_itinerary") || [];
|
||
form.setValue(
|
||
"ticket_itinerary",
|
||
current.filter((_, i) => i !== idx),
|
||
);
|
||
}}
|
||
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 hover:bg-white shadow"
|
||
>
|
||
<XIcon className="size-4 text-destructive" />
|
||
</button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
{/* yangi yo'nalish qo'shish formasi */}
|
||
<div className="flex flex-col gap-3 border rounded-xl p-4 bg-muted/10">
|
||
<Label className="text-md font-semibold">
|
||
{t("Yo'nalish qo'shish")}
|
||
</Label>
|
||
|
||
<Input
|
||
type="file"
|
||
id="ticket_itinerary_image"
|
||
accept="image/*"
|
||
className="h-12"
|
||
/>
|
||
|
||
<Input
|
||
id="ticket_itinerary_title"
|
||
placeholder={t("Sarlavha")}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Input
|
||
id="ticket_itinerary_title_ru"
|
||
placeholder={t("Sarlavha") + " (ru)"}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Input
|
||
id="ticket_itinerary_duration"
|
||
type="number"
|
||
min={1}
|
||
placeholder={t("Davomiylik (kun)")}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Input
|
||
id="ticket_itinerary_destination"
|
||
placeholder={t("Manzil")}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Input
|
||
id="ticket_itinerary_destination_ru"
|
||
placeholder={t("Manzil") + " (ru)"}
|
||
className="h-12 !text-md"
|
||
/>
|
||
|
||
<Button
|
||
type="button"
|
||
onClick={() => {
|
||
const imgInput = document.getElementById(
|
||
"ticket_itinerary_image",
|
||
) as HTMLInputElement;
|
||
const titleInput = document.getElementById(
|
||
"ticket_itinerary_title",
|
||
) as HTMLInputElement;
|
||
const titleRuInput = document.getElementById(
|
||
"ticket_itinerary_title_ru",
|
||
) as HTMLInputElement;
|
||
const durationInput = document.getElementById(
|
||
"ticket_itinerary_duration",
|
||
) as HTMLInputElement;
|
||
const destInput = document.getElementById(
|
||
"ticket_itinerary_destination",
|
||
) as HTMLInputElement;
|
||
const destRuInput = document.getElementById(
|
||
"ticket_itinerary_destination_ru",
|
||
) as HTMLInputElement;
|
||
|
||
const file = imgInput.files?.[0];
|
||
const title = titleInput.value.trim();
|
||
const titleRu = titleRuInput.value.trim();
|
||
const duration = Number(durationInput.value);
|
||
const dest = destInput.value.trim();
|
||
const destRu = destRuInput.value.trim();
|
||
|
||
if (
|
||
file &&
|
||
title &&
|
||
titleRu &&
|
||
duration &&
|
||
dest &&
|
||
destRu
|
||
) {
|
||
const current =
|
||
form.getValues("ticket_itinerary") || [];
|
||
|
||
form.setValue("ticket_itinerary", [
|
||
...current,
|
||
{
|
||
ticket_itinerary_image: [{ image: file }],
|
||
title,
|
||
title_ru: titleRu,
|
||
duration,
|
||
ticket_itinerary_destinations: [
|
||
{ name: dest, name_ru: destRu },
|
||
],
|
||
},
|
||
]);
|
||
|
||
imgInput.value = "";
|
||
titleInput.value = "";
|
||
titleRuInput.value = "";
|
||
durationInput.value = "";
|
||
destInput.value = "";
|
||
destRuInput.value = "";
|
||
}
|
||
}}
|
||
className="h-12"
|
||
>
|
||
{t("Qo'shish")}
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<FormMessage />
|
||
</FormItem>
|
||
);
|
||
}}
|
||
/>
|
||
|
||
<div className="w-full flex justify-end">
|
||
<Button
|
||
type="submit"
|
||
className="mt-6 px-8 py-3 bg-blue-600 text-white rounded-md hover:bg-blue-600 cursor-pointer"
|
||
>
|
||
{isPending || updatePending ? (
|
||
<Loader2 className="animate-spin" />
|
||
) : (
|
||
t("Saqlash")
|
||
)}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Form>
|
||
);
|
||
};
|
||
|
||
export default StepOne;
|