api ulandi
This commit is contained in:
@@ -160,7 +160,7 @@ const BadgeTable = ({
|
||||
useEffect(() => {
|
||||
if (badgeDetail) {
|
||||
form.setValue("color", badgeDetail.data.data.color);
|
||||
form.setValue("name", badgeDetail.data.data.name);
|
||||
form.setValue("name", badgeDetail.data.data.name_uz);
|
||||
form.setValue("name_ru", badgeDetail.data.data.name_ru);
|
||||
}
|
||||
}, [editId, badgeDetail]);
|
||||
|
||||
@@ -43,7 +43,12 @@ const CreateEditTour = () => {
|
||||
</div>
|
||||
</div>
|
||||
{step === 1 && (
|
||||
<StepOne setStep={setStep} data={data} isEditMode={isEditMode} />
|
||||
<StepOne
|
||||
setStep={setStep}
|
||||
data={data}
|
||||
isEditMode={isEditMode}
|
||||
id={id}
|
||||
/>
|
||||
)}
|
||||
{step === 2 && <StepTwo data={data} isEditMode={isEditMode} />}
|
||||
</div>
|
||||
|
||||
@@ -174,7 +174,7 @@ const FeaturesTable = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (badgeDetail) {
|
||||
form.setValue("name", badgeDetail.data.data.hotel_feature_type_name);
|
||||
form.setValue("name", badgeDetail.data.data.hotel_feature_type_name_uz);
|
||||
form.setValue(
|
||||
"name_ru",
|
||||
badgeDetail.data.data.hotel_feature_type_name_ru,
|
||||
|
||||
@@ -167,7 +167,7 @@ const FeaturesTableType = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (badgeDetail) {
|
||||
form.setValue("name", badgeDetail.data.data.feature_name);
|
||||
form.setValue("name", badgeDetail.data.data.feature_name_uz);
|
||||
form.setValue("name_ru", badgeDetail.data.data.feature_name_ru);
|
||||
}
|
||||
}, [editId, badgeDetail]);
|
||||
|
||||
@@ -155,7 +155,7 @@ const MealTable = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (typeDetail) {
|
||||
form.setValue("name", typeDetail.data.data.name);
|
||||
form.setValue("name", typeDetail.data.data.name_uz);
|
||||
form.setValue("name_ru", typeDetail.data.data.name_ru);
|
||||
}
|
||||
}, [editId, typeDetail]);
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
hotelBadge,
|
||||
hotelTarif,
|
||||
hotelTransport,
|
||||
updateTours,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import { TourformSchema } from "@/pages/tours/lib/form";
|
||||
import { useTicketStore } from "@/pages/tours/lib/store";
|
||||
@@ -46,10 +47,12 @@ 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("");
|
||||
@@ -88,6 +91,8 @@ const StepOne = ({
|
||||
passenger_count: 1,
|
||||
min_person: 1,
|
||||
max_person: 1,
|
||||
extra_service: [],
|
||||
paid_extra_service: [],
|
||||
languages: "",
|
||||
duration: 1,
|
||||
badges: [],
|
||||
@@ -103,119 +108,152 @@ const StepOne = ({
|
||||
const { addAmenity, setId } = useTicketStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditMode && data?.data) {
|
||||
const tourData = data.data;
|
||||
if (!isEditMode || !data?.data) return;
|
||||
|
||||
form.setValue("title", tourData.title);
|
||||
form.setValue("title_ru", formatPrice(tourData.title));
|
||||
form.setValue("price", tourData.price);
|
||||
setDisplayPrice(formatPrice(tourData.price));
|
||||
form.setValue("passenger_count", tourData.passenger_count || 1);
|
||||
form.setValue("min_person", tourData.min_person || 1);
|
||||
form.setValue("max_person", tourData.max_person || 1);
|
||||
form.setValue("departure", tourData.departure || "");
|
||||
form.setValue("departure_ru", tourData.departure || "");
|
||||
form.setValue("destination", tourData.destination || "");
|
||||
form.setValue("destination_ru", tourData.destination || "");
|
||||
form.setValue("location_name", tourData.location_name || "");
|
||||
form.setValue("location_name_ru", tourData.location_name || "");
|
||||
form.setValue("hotel_info", tourData.hotel_info || "");
|
||||
form.setValue("hotel_info_ru", tourData.hotel_info || "");
|
||||
form.setValue("hotel_meals_info", tourData.hotel_meals || "");
|
||||
form.setValue("hotel_meals_info_ru", tourData.hotel_meals || "");
|
||||
form.setValue("languages", tourData.languages || "");
|
||||
form.setValue("duration", tourData.duration_days || 1);
|
||||
form.setValue("visa_required", tourData.visa_required ? "yes" : "no");
|
||||
form.setValue("badges", tourData.badge || []);
|
||||
const tour = data.data;
|
||||
|
||||
// DateTime fields
|
||||
if (tourData.departure_time) {
|
||||
const departureDate = new Date(tourData.departure_time);
|
||||
form.setValue("departureDateTime", {
|
||||
date: departureDate,
|
||||
time: departureDate.toTimeString().slice(0, 8), // HH:MM:SS
|
||||
});
|
||||
}
|
||||
// 🔹 Oddiy text maydonlar
|
||||
form.setValue("title", tour.title_uz ?? "");
|
||||
form.setValue("title_ru", tour.title_ru ?? "");
|
||||
form.setValue("price", tour.price ?? 0);
|
||||
setDisplayPrice(formatPrice(tour.price ?? 0));
|
||||
|
||||
if (tourData.travel_time) {
|
||||
const travelDate = new Date(tourData.travel_time);
|
||||
form.setValue("travelDateTime", {
|
||||
date: travelDate,
|
||||
time: travelDate.toTimeString().slice(0, 8),
|
||||
});
|
||||
}
|
||||
form.setValue("passenger_count", tour.passenger_count ?? 1);
|
||||
form.setValue("min_person", tour.min_person ?? 1);
|
||||
form.setValue("max_person", tour.max_person ?? 1);
|
||||
|
||||
// Amenities
|
||||
if (tourData.ticket_amenities && tourData.ticket_amenities.length > 0) {
|
||||
const amenities = tourData.ticket_amenities.map((item) => ({
|
||||
name: item.name,
|
||||
name_ru: item.name_ru,
|
||||
icon_name: item.icon_name,
|
||||
}));
|
||||
form.setValue("amenities", amenities);
|
||||
}
|
||||
form.setValue("departure", tour.departure_uz ?? "");
|
||||
form.setValue("departure_ru", tour.departure_ru ?? "");
|
||||
form.setValue("destination", tour.destination_uz ?? "");
|
||||
form.setValue("destination_ru", tour.destination_ru ?? "");
|
||||
form.setValue("location_name", tour.location_name_uz ?? "");
|
||||
form.setValue("location_name_ru", tour.location_name_ru ?? "");
|
||||
|
||||
if (
|
||||
tourData.ticket_included_services &&
|
||||
tourData.ticket_included_services.length > 0
|
||||
) {
|
||||
const services = tourData.ticket_included_services.map((item) => ({
|
||||
image: item.image,
|
||||
title: item.title,
|
||||
title_ru: item.title_ru,
|
||||
description: item.desc_uz || item.desc,
|
||||
desc_ru: item.desc || item.desc,
|
||||
}));
|
||||
form.setValue("hotel_services", services);
|
||||
}
|
||||
form.setValue("hotel_info", tour.hotel_info_uz ?? "");
|
||||
form.setValue("hotel_info_ru", tour.hotel_info_ru ?? "");
|
||||
form.setValue("hotel_meals_info", tour.hotel_meals_uz ?? "");
|
||||
form.setValue("hotel_meals_info_ru", tour.hotel_meals_ru ?? "");
|
||||
|
||||
if (
|
||||
tourData.ticket_hotel_meals &&
|
||||
tourData.ticket_hotel_meals.length > 0
|
||||
) {
|
||||
const meals = tourData.ticket_hotel_meals.map((item) => ({
|
||||
image: item.image,
|
||||
title: item.name,
|
||||
title_ru: item.name_ru,
|
||||
description: item.desc,
|
||||
desc_ru: item.desc_ru,
|
||||
}));
|
||||
form.setValue("hotel_meals", meals);
|
||||
}
|
||||
form.setValue("languages", tour.languages ?? "");
|
||||
form.setValue("duration", tour.duration_days ?? 1);
|
||||
form.setValue("visa_required", tour.visa_required ? "yes" : "no");
|
||||
form.setValue("badges", tour.badge ?? []);
|
||||
|
||||
// Transport
|
||||
if (tourData.transports && tourData.transports.length > 0) {
|
||||
const transports = tourData.transports.map((item, index) => ({
|
||||
transport: index + 1, // Agar transport ID bo'lsa, uni ishlatish kerak
|
||||
price: item.price,
|
||||
}));
|
||||
// const tariff = tourData.tar => ({
|
||||
// transport: index + 1,
|
||||
// price: item.price,
|
||||
// }));
|
||||
form.setValue("transport", transports);
|
||||
// form.setValue("tarif", );
|
||||
}
|
||||
|
||||
// Ticket itinerary
|
||||
if (tourData.ticket_itinerary && tourData.ticket_itinerary.length > 0) {
|
||||
const itinerary = tourData.ticket_itinerary.map((item) => ({
|
||||
ticket_itinerary_image: [], // Image fayllarni alohida handle qilish kerak
|
||||
title: item.title,
|
||||
title_ru: item.title_ru,
|
||||
duration: item.duration,
|
||||
ticket_itinerary_destinations: [], // Agar destinations bo'lsa qo'shish kerak
|
||||
}));
|
||||
form.setValue("ticket_itinerary", itinerary);
|
||||
}
|
||||
|
||||
form.setValue("banner", tourData.image_banner);
|
||||
if (tourData.ticket_images && tourData.ticket_images.length > 0) {
|
||||
const images = tourData.ticket_images.map((img) => img.image); // faqat linklarni olamiz
|
||||
form.setValue("images", images);
|
||||
}
|
||||
// 🔹 Jo‘nash vaqti
|
||||
if (tour.departure_time) {
|
||||
const d = new Date(tour.departure_time);
|
||||
form.setValue("departureDateTime", {
|
||||
date: d,
|
||||
time: d.toTimeString().slice(0, 8),
|
||||
});
|
||||
}
|
||||
}, [isEditMode, data, form]);
|
||||
|
||||
// 🔹 Qaytish vaqti
|
||||
if (tour.travel_time) {
|
||||
const d = new Date(tour.travel_time);
|
||||
form.setValue("travelDateTime", {
|
||||
date: d,
|
||||
time: d.toTimeString().slice(0, 8),
|
||||
});
|
||||
}
|
||||
|
||||
// 🔹 Qulayliklar (amenities)
|
||||
form.setValue(
|
||||
"amenities",
|
||||
tour.ticket_amenities?.map((a) => ({
|
||||
name: a.name ?? "",
|
||||
name_ru: a.name_ru ?? "",
|
||||
icon_name: a.icon_name ?? "",
|
||||
})) ?? [],
|
||||
);
|
||||
|
||||
// 🔹 Xizmatlar (hotel_services)
|
||||
form.setValue(
|
||||
"hotel_services",
|
||||
tour.ticket_included_services?.map((s) => ({
|
||||
image: s.image ?? null,
|
||||
title: s.title_uz ?? "",
|
||||
title_ru: s.title_ru ?? "",
|
||||
description: s.desc_uz ?? "",
|
||||
desc_ru: s.desc_ru ?? "",
|
||||
})) ?? [],
|
||||
);
|
||||
|
||||
// 🔹 Taomlar (hotel_meals)
|
||||
form.setValue(
|
||||
"hotel_meals",
|
||||
tour.ticket_hotel_meals?.map((m) => ({
|
||||
image: m.image ?? null,
|
||||
title: m.name ?? "",
|
||||
title_ru: m.name_ru ?? "",
|
||||
description: m.desc ?? "",
|
||||
desc_ru: m.desc_ru ?? "",
|
||||
})) ?? [],
|
||||
);
|
||||
|
||||
// 🔹 Transport
|
||||
const transports =
|
||||
tour.transports?.map((t, i) => ({
|
||||
transport: i + 1,
|
||||
price: t.price ?? 0,
|
||||
})) ?? [];
|
||||
form.setValue("transport", transports);
|
||||
setTransportPrices(transports.map((t) => formatPrice(t.price ?? 0))); // 👈 YANGI QO‘SHILGAN
|
||||
|
||||
// 🔹 Tarif
|
||||
const tariffs =
|
||||
tour.tariff?.map((t) => ({
|
||||
tariff: t.tariff ?? 0,
|
||||
price: t.price ?? 0,
|
||||
})) ?? [];
|
||||
form.setValue("tarif", tariffs);
|
||||
setTarifDisplayPrice(tariffs.map((t) => formatPrice(t.price ?? 0)));
|
||||
|
||||
// 🔹 Yo‘nalishlar (ticket_itinerary)
|
||||
form.setValue(
|
||||
"ticket_itinerary",
|
||||
tour.ticket_itinerary?.map((item) => ({
|
||||
ticket_itinerary_image:
|
||||
item.ticket_itinerary_image?.map((img) => ({
|
||||
image: img.image,
|
||||
})) ?? [],
|
||||
title: item.title ?? "",
|
||||
title_ru: item.title_ru ?? "",
|
||||
duration: item.duration ?? 1,
|
||||
ticket_itinerary_destinations:
|
||||
item.ticket_itinerary_destinations?.map((d) => ({
|
||||
name: d.name ?? "",
|
||||
name_ru: d.name_ru ?? "",
|
||||
})) ?? [],
|
||||
})) ?? [],
|
||||
);
|
||||
|
||||
// 🔹 Banner va rasmlar
|
||||
form.setValue("banner", tour.image_banner ?? null);
|
||||
form.setValue("images", tour.ticket_images?.map((img) => img.image) ?? []);
|
||||
|
||||
// 🔹 Bepul xizmatlar (extra_service)
|
||||
form.setValue(
|
||||
"extra_service",
|
||||
tour.extra_service?.map((s) => ({
|
||||
name: s.name_uz ?? s.name ?? "",
|
||||
name_ru: s.name_ru ?? "",
|
||||
})) ?? [],
|
||||
);
|
||||
|
||||
// 🔹 Pullik xizmatlar (paid_extra_service)
|
||||
form.setValue(
|
||||
"paid_extra_service",
|
||||
tour.paid_extra_service?.map((s) => ({
|
||||
name: s.name_uz ?? s.name ?? "",
|
||||
name_ru: s.name_ru ?? "",
|
||||
price: s.price ?? 0,
|
||||
})) ?? [],
|
||||
);
|
||||
|
||||
// 🔹 TicketStore uchun id
|
||||
setId(tour.id);
|
||||
}, [isEditMode, data, form, setId]);
|
||||
|
||||
const { watch, setValue } = form;
|
||||
const selectedDate = watch("departureDateTime.date");
|
||||
@@ -238,6 +276,22 @@ const StepOne = ({
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: update } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: number; body: FormData }) => {
|
||||
return updateTours({ body, id });
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
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();
|
||||
|
||||
@@ -273,7 +327,9 @@ const StepOne = ({
|
||||
formData.append("hotel_meals_ru", value.hotel_meals_info_ru);
|
||||
formData.append("duration_days", String(value.duration));
|
||||
formData.append("rating", String("0.0"));
|
||||
formData.append("image_banner", value.banner);
|
||||
if (value.banner instanceof File) {
|
||||
formData.append("image_banner", value.banner);
|
||||
}
|
||||
value.tarif.forEach((e, i) => {
|
||||
formData.append(`tariff[${i}]tariff`, String(e.tariff));
|
||||
formData.append(`tariff[${i}]price`, String(e.price));
|
||||
@@ -285,7 +341,11 @@ const StepOne = ({
|
||||
value.badges?.forEach((e, i) => {
|
||||
formData.append(`badge[${i}]`, String(e));
|
||||
});
|
||||
value.images.forEach((e) => formData.append("ticket_images", e));
|
||||
value.images.forEach((e) => {
|
||||
if (e instanceof File) {
|
||||
formData.append("ticket_images", e);
|
||||
}
|
||||
});
|
||||
value.amenities.forEach((e, i) => {
|
||||
formData.append(`ticket_amenities[${i}]name`, e.name);
|
||||
formData.append(`ticket_amenities[${i}]name_ru`, e.name_ru);
|
||||
@@ -297,43 +357,67 @@ const StepOne = ({
|
||||
});
|
||||
});
|
||||
value.hotel_services.forEach((e, i) => {
|
||||
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);
|
||||
if (e 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);
|
||||
}
|
||||
});
|
||||
value.ticket_itinerary.forEach((e, i) => {
|
||||
formData.append(`ticket_itinerary[${i}]title`, e.title);
|
||||
formData.append(`ticket_itinerary[${i}]title_ru`, e.title_ru);
|
||||
formData.append(`ticket_itinerary[${i}]duration`, String(e.duration));
|
||||
e.ticket_itinerary_image.forEach((e, f) => {
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_image[${f}]image`,
|
||||
e.image,
|
||||
);
|
||||
});
|
||||
e.ticket_itinerary_destinations.forEach((e, f) => {
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${f}]name`,
|
||||
String(e.name),
|
||||
);
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${f}]name_ru`,
|
||||
String(e.name_ru),
|
||||
);
|
||||
e.ticket_itinerary_image.forEach((l, f) => {
|
||||
if (e instanceof File) {
|
||||
formData.append(`ticket_itinerary[${i}]title`, e.title);
|
||||
formData.append(`ticket_itinerary[${i}]title_ru`, e.title_ru);
|
||||
formData.append(`ticket_itinerary[${i}]duration`, String(e.duration));
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_image[${f}]image`,
|
||||
l.image,
|
||||
);
|
||||
e.ticket_itinerary_destinations.forEach((e, f) => {
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${f}]name`,
|
||||
String(e.name),
|
||||
);
|
||||
formData.append(
|
||||
`ticket_itinerary[${i}]ticket_itinerary_destinations[${f}]name_ru`,
|
||||
String(e.name_ru),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
value.hotel_meals.forEach((e, i) => {
|
||||
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);
|
||||
if (e 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);
|
||||
}
|
||||
});
|
||||
create(formData);
|
||||
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.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);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(form.formState.errors);
|
||||
|
||||
const { data: badge } = useQuery({
|
||||
queryKey: ["all_badge"],
|
||||
queryFn: () => hotelBadge({ page: 1, page_size: 10 }),
|
||||
@@ -392,7 +476,9 @@ const StepOne = ({
|
||||
name="price"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Label className="text-md">{t("Narx")} (1 kishi uchun)</Label>
|
||||
<Label className="text-md">
|
||||
{t("Narx")} {t("(1 kishi uchun)")}
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
@@ -1157,11 +1243,11 @@ const StepOne = ({
|
||||
name="visa_required"
|
||||
render={({ field }) => (
|
||||
<FormItem className="space-y-3">
|
||||
<Label>{t("Visa talab qilinadimi?")}</Label>
|
||||
<Label>{t("Visa talab qilinadimi")}?</Label>
|
||||
<FormControl>
|
||||
<RadioGroup
|
||||
onValueChange={field.onChange}
|
||||
defaultValue={field.value}
|
||||
value={field.value}
|
||||
className="flex gap-6"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -1170,7 +1256,7 @@ const StepOne = ({
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="no" id="visa_no" />
|
||||
<label htmlFor="visa_no">{t("Yo‘q")}</label>
|
||||
<label htmlFor="visa_no">{t("Yo'q")}</label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
@@ -1287,6 +1373,206 @@ const StepOne = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
<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 (UZ)")}
|
||||
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)} so‘m</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 (UZ)")}
|
||||
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]"
|
||||
/>
|
||||
|
||||
{/* Narx maydoni */}
|
||||
<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"
|
||||
@@ -1313,7 +1599,7 @@ const StepOne = ({
|
||||
<Label className="text-md">{t("Mehmonxona haqida")} (ru)</Label>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t("Mehmonxona haqida (ru)")}
|
||||
placeholder={t("Mehmonxona haqida") + " (ru)"}
|
||||
{...field}
|
||||
className="min-h-48 max-h-60 !text-md"
|
||||
/>
|
||||
@@ -1389,7 +1675,7 @@ const StepOne = ({
|
||||
|
||||
<Input
|
||||
id="hotel_service_title_ru"
|
||||
placeholder={t("Xizmat nomi (ru)")}
|
||||
placeholder={t("Xizmat nomi") + " (ru)"}
|
||||
className="h-12 !text-md"
|
||||
/>
|
||||
|
||||
@@ -1401,7 +1687,7 @@ const StepOne = ({
|
||||
|
||||
<Textarea
|
||||
id="hotel_service_desc_ru"
|
||||
placeholder={t("Xizmat tavsifi (ru)")}
|
||||
placeholder={t("Xizmat tavsifi") + " (ru)"}
|
||||
className="min-h-24 !text-md"
|
||||
/>
|
||||
|
||||
@@ -1487,7 +1773,7 @@ const StepOne = ({
|
||||
</Label>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder={t("Mehmonxona taomlari haqida (ru)")}
|
||||
placeholder={t("Mehmonxona taomlari haqida") + " (ru)"}
|
||||
{...field}
|
||||
className="min-h-48 max-h-60"
|
||||
/>
|
||||
@@ -1563,7 +1849,7 @@ const StepOne = ({
|
||||
|
||||
<Input
|
||||
id="hotel_meals_title_ru"
|
||||
placeholder={t("Taom nomi (ru)")}
|
||||
placeholder={t("Taom nomi") + " (ru)"}
|
||||
className="h-12 !text-md"
|
||||
/>
|
||||
|
||||
@@ -1575,7 +1861,7 @@ const StepOne = ({
|
||||
|
||||
<Textarea
|
||||
id="hotel_meals_desc_ru"
|
||||
placeholder={t("Taom tavsifi (ru)")}
|
||||
placeholder={t("Taom tavsifi") + " (ru)"}
|
||||
className="min-h-24 !text-md"
|
||||
/>
|
||||
|
||||
@@ -1662,7 +1948,7 @@ const StepOne = ({
|
||||
{item.ticket_itinerary_destinations[0]?.name}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{item.duration} kun
|
||||
{item.duration} {t("kun")}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -1704,7 +1990,7 @@ const StepOne = ({
|
||||
|
||||
<Input
|
||||
id="ticket_itinerary_title_ru"
|
||||
placeholder={t("Sarlavha (RU)")}
|
||||
placeholder={t("Sarlavha") + " (ru)"}
|
||||
className="h-12 !text-md"
|
||||
/>
|
||||
|
||||
@@ -1724,7 +2010,7 @@ const StepOne = ({
|
||||
|
||||
<Input
|
||||
id="ticket_itinerary_destination_ru"
|
||||
placeholder={t("Manzil (RU)")}
|
||||
placeholder={t("Manzil") + " (ru)"}
|
||||
className="h-12 !text-md"
|
||||
/>
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
import {
|
||||
createHotel,
|
||||
editHotel,
|
||||
getHotel,
|
||||
hotelFeature,
|
||||
hotelFeatureType,
|
||||
hotelType,
|
||||
@@ -30,7 +32,7 @@ import {
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
@@ -64,9 +66,18 @@ const StepTwo = ({
|
||||
isEditMode: boolean;
|
||||
}) => {
|
||||
const { amenities, id: ticketId } = useTicketStore();
|
||||
const navigator = useNavigate();
|
||||
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: {
|
||||
@@ -79,78 +90,94 @@ const StepTwo = ({
|
||||
},
|
||||
});
|
||||
|
||||
// 🧩 Edit holati uchun formni to‘ldirish
|
||||
useEffect(() => {
|
||||
if (isEditMode && data?.data) {
|
||||
const tourData = data.data;
|
||||
if (isEditMode && hotelDetail?.[0]) {
|
||||
const hotel = hotelDetail[0];
|
||||
|
||||
form.setValue("title", tourData.hotel_name);
|
||||
form.setValue("rating", tourData.hotel_rating);
|
||||
form.setValue("mealPlan", tourData.hotel_meals);
|
||||
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, data, form]);
|
||||
}, [isEditMode, hotelDetail, form, data]);
|
||||
|
||||
const mealPlans = [
|
||||
"Breakfast Only",
|
||||
"Half Board",
|
||||
"Full Board",
|
||||
"All Inclusive",
|
||||
];
|
||||
// 🧩 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 loadAll = async () => {
|
||||
try {
|
||||
let page = 1;
|
||||
let results: Type[] = [];
|
||||
let hasNext = true;
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
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);
|
||||
};
|
||||
loadAll();
|
||||
loadHotelTypes();
|
||||
}, []);
|
||||
|
||||
// 🔹 Hotel Features yuklash
|
||||
useEffect(() => {
|
||||
const loadAll = async () => {
|
||||
try {
|
||||
let page = 1;
|
||||
let results: HotelFeatures[] = [];
|
||||
let hasNext = true;
|
||||
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);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
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);
|
||||
};
|
||||
loadAll();
|
||||
loadHotelFeatures();
|
||||
}, []);
|
||||
|
||||
// 🔹 Feature type'larni yuklash (tanlangan feature bo‘yicha)
|
||||
useEffect(() => {
|
||||
if (selectedHotelFeatures.length === 0) {
|
||||
setAllHotelFeatureType([]);
|
||||
@@ -158,107 +185,118 @@ const StepTwo = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const loadAll = async () => {
|
||||
try {
|
||||
const selectedFeatureIds = selectedHotelFeatures
|
||||
.map((featureId) => Number(featureId))
|
||||
.filter((id) => !isNaN(id));
|
||||
const loadFeatureTypes = async () => {
|
||||
const selectedIds = selectedHotelFeatures.map(Number).filter(Boolean);
|
||||
let allResults: HotelFeaturesType[] = [];
|
||||
const mapping: Record<string, string[]> = {};
|
||||
|
||||
if (selectedFeatureIds.length === 0) return;
|
||||
for (const id of selectedIds) {
|
||||
let page = 1;
|
||||
let hasNext = true;
|
||||
const featureTypes: string[] = [];
|
||||
|
||||
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;
|
||||
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++;
|
||||
}
|
||||
|
||||
const uniqueResults = allResults.filter(
|
||||
(item, index, self) =>
|
||||
index === self.findIndex((t) => t.id === item.id),
|
||||
);
|
||||
|
||||
setAllHotelFeatureType(uniqueResults);
|
||||
setFeatureTypeMapping(newMapping);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
mapping[String(id)] = featureTypes;
|
||||
}
|
||||
|
||||
const uniqueResults = allResults.filter(
|
||||
(v, i, a) => a.findIndex((t) => t.id === v.id) === i,
|
||||
);
|
||||
|
||||
setAllHotelFeatureType(uniqueResults);
|
||||
setFeatureTypeMapping(mapping);
|
||||
};
|
||||
|
||||
loadAll();
|
||||
loadFeatureTypes();
|
||||
}, [selectedHotelFeatures]);
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: FormData) => createHotel({ body }),
|
||||
onSuccess: () => {
|
||||
navigator("/tours");
|
||||
toast.success(t("Muvaffaqiyatli saqlandi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
toast.success(t("Muvaffaqiyatli saqlandi"));
|
||||
navigate("/tours");
|
||||
},
|
||||
onError: () => {
|
||||
onError: () =>
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const removeHotelType = (typeId: string) => {
|
||||
const current = form.getValues("hotelType");
|
||||
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",
|
||||
current.filter((val) => val !== typeId),
|
||||
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 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",
|
||||
const removeFeatureType = (id: string) =>
|
||||
form.setValue(
|
||||
"hotelFeaturesType",
|
||||
form.getValues("hotelFeaturesType").filter((v) => v !== id),
|
||||
);
|
||||
|
||||
data.hotelType.forEach((typeId) => {
|
||||
formData.append("hotel_type", typeId);
|
||||
});
|
||||
// 🧩 Submit
|
||||
const onSubmit = (data: z.infer<typeof formSchema>) => {
|
||||
const formData = new FormData();
|
||||
|
||||
data.hotelFeaturesType.forEach((typeId) => {
|
||||
formData.append("hotel_features", typeId);
|
||||
});
|
||||
formData.append("ticket", ticketId ? String(ticketId) : "");
|
||||
formData.append("name", data.title);
|
||||
formData.append("rating", data.rating);
|
||||
|
||||
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.forEach((id) => formData.append("hotel_type", id));
|
||||
data.hotelFeatures.forEach((id) => formData.append("hotel_features", id));
|
||||
|
||||
amenities.forEach((e, i) => {
|
||||
formData.append(`hotel_amenities[${i}]name`, e.name);
|
||||
@@ -266,33 +304,22 @@ const StepTwo = ({
|
||||
formData.append(`hotel_amenities[${i}]icon_name`, e.icon_name);
|
||||
});
|
||||
|
||||
mutate(formData);
|
||||
if (isEditMode && hotelDetail) {
|
||||
edit({
|
||||
body: formData,
|
||||
id: Number(hotelDetail[0].id),
|
||||
});
|
||||
} else {
|
||||
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),
|
||||
);
|
||||
};
|
||||
const mealPlans = [
|
||||
"Breakfast Only",
|
||||
"Half Board",
|
||||
"Full Board",
|
||||
"All Inclusive",
|
||||
];
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
@@ -465,6 +492,8 @@ const StepTwo = ({
|
||||
const selectedItem = allHotelFeature.find(
|
||||
(item) => String(item.id) === selectedValue,
|
||||
);
|
||||
console.log(allHotelFeature);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={selectedValue}
|
||||
@@ -608,7 +637,7 @@ const StepTwo = ({
|
||||
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")}
|
||||
{isPending || editPending ? t("Yuklanmoqda...") : t("Saqlash")}
|
||||
</button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { deleteTours, getAllTours } from "@/pages/tours/lib/api";
|
||||
import {
|
||||
addedPopularTours,
|
||||
deleteTours,
|
||||
getAllTours,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -10,6 +14,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Switch } from "@/shared/ui/switch";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -28,15 +33,18 @@ import {
|
||||
Plane,
|
||||
PlusCircle,
|
||||
Trash2,
|
||||
X,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const Tours = () => {
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(1);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const [showPopularDialog, setShowPopularDialog] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
@@ -45,18 +53,54 @@ const Tours = () => {
|
||||
queryFn: () => getAllTours({ page: page, page_size: 10 }),
|
||||
});
|
||||
|
||||
const { data: popularTour } = useQuery({
|
||||
queryKey: ["popular_tours"],
|
||||
queryFn: () =>
|
||||
getAllTours({ page: 1, page_size: 10, featured_tickets: true }),
|
||||
});
|
||||
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: (id: number) => deleteTours({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||||
setDeleteId(null);
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: popular } = useMutation({
|
||||
mutationFn: ({ id, value }: { id: number; value: number }) =>
|
||||
addedPopularTours({ id, value }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||||
queryClient.refetchQueries({ queryKey: ["popular_tours"] });
|
||||
},
|
||||
onError: () => {
|
||||
if (popularTour?.data.data.results.length === 5) {
|
||||
setShowPopularDialog(true);
|
||||
} else {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const confirmDelete = (id: number) => {
|
||||
mutate(id);
|
||||
};
|
||||
|
||||
const removeFromPopular = (id: number) => {
|
||||
popular({ id, value: 0 });
|
||||
setShowPopularDialog(false);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||
@@ -105,6 +149,9 @@ const Tours = () => {
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[180px]">{t("Mehmonxona")}</TableHead>
|
||||
<TableHead className="min-w-[200px]">{t("Narxi")}</TableHead>
|
||||
<TableHead className="min-w-[120px] text-center">
|
||||
{t("Popular")}
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[150px] text-center">
|
||||
{t("Операции")}
|
||||
</TableHead>
|
||||
@@ -139,6 +186,18 @@ const Tours = () => {
|
||||
</span>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-center">
|
||||
<Switch
|
||||
checked={tour.featured_tickets}
|
||||
onCheckedChange={() =>
|
||||
popular({
|
||||
id: tour.id,
|
||||
value: tour.featured_tickets ? 0 : 1,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-center">
|
||||
<div className="flex gap-2 justify-center">
|
||||
<Button
|
||||
@@ -172,6 +231,7 @@ const Tours = () => {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Delete Tour Dialog */}
|
||||
<Dialog open={deleteId !== null} onOpenChange={() => setDeleteId(null)}>
|
||||
<DialogContent className="sm:max-w-[425px] bg-gray-900">
|
||||
<DialogHeader>
|
||||
@@ -201,6 +261,63 @@ const Tours = () => {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Popular Tours Dialog */}
|
||||
<Dialog open={showPopularDialog} onOpenChange={setShowPopularDialog}>
|
||||
<DialogContent className="sm:max-w-[600px] bg-gray-900">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{t("Popular turlar (5/5)")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p className="text-muted-foreground mb-4">
|
||||
{t(
|
||||
"Popular turlar ro'yxati to'lgan. Yangi tur qo'shish uchun biror turni o'chiring.",
|
||||
)}
|
||||
</p>
|
||||
<div className="space-y-2 max-h-[400px] overflow-y-auto">
|
||||
{popularTour?.data.data.results.map((tour) => (
|
||||
<div
|
||||
key={tour.id}
|
||||
className="flex items-center justify-between p-3 border border-slate-700 rounded-lg hover:bg-slate-800/50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3 flex-1">
|
||||
<Plane className="w-4 h-4 text-primary flex-shrink-0" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-semibold truncate">
|
||||
{tour.destination}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{tour.duration_days} kun • {tour.hotel_name}
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-green-600 font-bold text-sm flex-shrink-0">
|
||||
{formatPrice(tour.price, true)}
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => removeFromPopular(tour.id)}
|
||||
className="ml-2 text-red-500 hover:text-red-600 hover:bg-red-500/10"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowPopularDialog(false)}
|
||||
>
|
||||
{t("Yopish")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex justify-end mt-10 gap-3">
|
||||
<button
|
||||
disabled={page === 1}
|
||||
|
||||
@@ -69,7 +69,9 @@ const TransportTable = ({
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
const [types, setTypes] = useState<"edit" | "create">("create");
|
||||
const [selectedIcon, setSelectedIcon] = useState("Bus");
|
||||
const [selectedIcon, setSelectedIcon] = useState("");
|
||||
console.log(selectedIcon);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
@@ -83,34 +85,35 @@ const TransportTable = ({
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("icon_name", selectedIcon);
|
||||
}, [selectedIcon]);
|
||||
}, [selectedIcon, form]);
|
||||
|
||||
const handleEdit = (id: number) => {
|
||||
setTypes("edit");
|
||||
setOpen(true);
|
||||
setEditId(id);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const { data: transportDetail } = useQuery({
|
||||
const { data: transportDetail, isLoading: isDetailLoading } = useQuery({
|
||||
queryKey: ["detail_transport", editId],
|
||||
queryFn: () => hotelTransportDetail({ id: editId! }),
|
||||
enabled: !!editId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (transportDetail) {
|
||||
form.setValue("name", transportDetail.data.data.name);
|
||||
if (transportDetail && editId) {
|
||||
const iconName = transportDetail.data.data.icon_name || "HelpCircle";
|
||||
form.setValue("name", transportDetail.data.data.name_uz);
|
||||
form.setValue("name_ru", transportDetail.data.data.name_ru);
|
||||
form.setValue("icon_name", transportDetail.data.data.icon_name);
|
||||
setSelectedIcon(transportDetail.data.data.icon_name);
|
||||
form.setValue("icon_name", iconName);
|
||||
setSelectedIcon(iconName);
|
||||
}
|
||||
}, [transportDetail, editId, form]);
|
||||
}, [transportDetail, editId, form, selectedIcon]);
|
||||
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => hotelTransportDelete({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["all_transport"] });
|
||||
toast.success(t("O‘chirildi"), { position: "top-center" });
|
||||
toast.success(t("O'chirildi"), { position: "top-center" });
|
||||
},
|
||||
onError: () =>
|
||||
toast.error(t("Xatolik yuz berdi"), { position: "top-center" }),
|
||||
@@ -124,9 +127,8 @@ const TransportTable = ({
|
||||
}) => hotelTranportCreate({ body }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["all_transport"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
toast.success(t("Muvaffaqiyatli qo‘shildi"), { position: "top-center" });
|
||||
handleCloseDialog();
|
||||
toast.success(t("Muvaffaqiyatli qo'shildi"), { position: "top-center" });
|
||||
},
|
||||
onError: () =>
|
||||
toast.error(t("Xatolik yuz berdi"), { position: "top-center" }),
|
||||
@@ -142,8 +144,7 @@ const TransportTable = ({
|
||||
}) => hotelTransportUpdate({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["all_transport"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
handleCloseDialog();
|
||||
toast.success(t("Tahrirlandi"), { position: "top-center" });
|
||||
},
|
||||
onError: () =>
|
||||
@@ -163,6 +164,12 @@ const TransportTable = ({
|
||||
|
||||
const handleDelete = (id: number) => deleteMutate({ id });
|
||||
|
||||
const handleCloseDialog = () => {
|
||||
setOpen(false);
|
||||
setEditId(null);
|
||||
form.reset();
|
||||
};
|
||||
|
||||
const columns = TranportColumns(handleEdit, handleDelete, t);
|
||||
|
||||
const table = useReactTable({
|
||||
@@ -185,14 +192,15 @@ const TransportTable = ({
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => {
|
||||
setOpen(true);
|
||||
setTypes("create");
|
||||
setEditId(null);
|
||||
setSelectedIcon("HelpCircle");
|
||||
form.reset();
|
||||
setSelectedIcon("");
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="mr-2" />
|
||||
{t("Qo‘shish")}
|
||||
{t("Qo'shish")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -250,81 +258,96 @@ const TransportTable = ({
|
||||
namePageSize="pageTransportSize"
|
||||
/>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (!isOpen) {
|
||||
handleCloseDialog();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<p className="text-xl font-semibold mb-4">
|
||||
{types === "create"
|
||||
? t("Yangi transport qo‘shish")
|
||||
? t("Yangi transport qo'shish")
|
||||
: t("Tahrirlash")}
|
||||
</p>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 p-2"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Nomi (uz)")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("Nomi (uz)")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name_ru"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Nomi (ru)")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("Nomi (ru)")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="icon_name"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Belgi (Icon)")}</FormLabel>
|
||||
<FormControl className="w-full">
|
||||
<IconSelect
|
||||
setSelectedIcon={setSelectedIcon}
|
||||
selectedIcon={selectedIcon}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="border-slate-600 text-white hover:bg-gray-600 bg-gray-600"
|
||||
>
|
||||
{t("Bekor qilish")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending || updatePending}>
|
||||
{isPending || updatePending ? (
|
||||
<Loader className="animate-spin" />
|
||||
) : types === "create" ? (
|
||||
t("Saqlash")
|
||||
) : (
|
||||
t("Tahrirlash")
|
||||
{isDetailLoading && types === "edit" ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Loader className="animate-spin w-8 h-8" />
|
||||
</div>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 p-2"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Nomi (uz)")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("Nomi (uz)")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name_ru"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Nomi (ru)")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("Nomi (ru)")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="icon_name"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Belgi (Icon)")}</FormLabel>
|
||||
<FormControl className="w-full">
|
||||
<IconSelect
|
||||
setSelectedIcon={setSelectedIcon}
|
||||
selectedIcon={selectedIcon}
|
||||
defaultIcon="HelpCircle"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleCloseDialog}
|
||||
className="border-slate-600 text-white hover:bg-gray-600 bg-gray-600"
|
||||
>
|
||||
{t("Bekor qilish")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending || updatePending}>
|
||||
{isPending || updatePending ? (
|
||||
<Loader className="animate-spin" />
|
||||
) : types === "create" ? (
|
||||
t("Saqlash")
|
||||
) : (
|
||||
t("Tahrirlash")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user