@@ -496,7 +496,7 @@ export default function Seo() {
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={allSeo && allSeo.length !== 0}
|
||||
disabled={!edit && allSeo && allSeo.length !== 0}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700 text-white font-semibold py-3 rounded-lg transition-colors disabled:opacity-50"
|
||||
>
|
||||
{isPending || editPending ? (
|
||||
|
||||
@@ -51,10 +51,10 @@ export const TourformSchema = z.object({
|
||||
message: "Iltimos, visa talabliligini tanlang",
|
||||
}),
|
||||
departureDateTime: z.object({
|
||||
date: z.date({ message: "Jo‘nash vaqti majburiy" }),
|
||||
date: z.date({ message: "Jo'nash vaqti majburiy" }),
|
||||
time: z
|
||||
.string()
|
||||
.min(1, { message: "Jo‘nash vaqti majburiy" })
|
||||
.min(1, { message: "Jo'nash vaqti majburiy" })
|
||||
.refine(
|
||||
(val) => {
|
||||
const parts = val.split(":");
|
||||
@@ -76,10 +76,10 @@ export const TourformSchema = z.object({
|
||||
),
|
||||
}),
|
||||
travelDateTime: z.object({
|
||||
date: z.date({ message: "Jo‘nash vaqti majburiy" }),
|
||||
date: z.date({ message: "Jo'nash vaqti majburiy" }),
|
||||
time: z
|
||||
.string()
|
||||
.min(1, { message: "Jo‘nash vaqti majburiy" })
|
||||
.min(1, { message: "Jo'nash vaqti majburiy" })
|
||||
.refine(
|
||||
(val) => {
|
||||
const parts = val.split(":");
|
||||
@@ -125,14 +125,19 @@ export const TourformSchema = z.object({
|
||||
.optional(),
|
||||
banner: z.any().nullable(),
|
||||
images: z
|
||||
.array(z.union([z.instanceof(File), z.string()]))
|
||||
.min(1, { message: "Kamida bitta rasm yuklang." }),
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
image: z.union([z.instanceof(File), z.string()]).nullable(),
|
||||
}),
|
||||
)
|
||||
.min(1, { message: "Kamida bitta rasm kiriting." }),
|
||||
amenities: z.array(z.number()).optional(),
|
||||
|
||||
// 🔹 Quyidagilar endi ixtiyoriy (required emas)
|
||||
hotel_services: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
image: z.any().nullable(),
|
||||
title: z.string().min(1, "Xizmat nomi majburiy"),
|
||||
title_ru: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
@@ -145,6 +150,7 @@ export const TourformSchema = z.object({
|
||||
hotel_meals: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number().optional(),
|
||||
image: z.any().nullable(),
|
||||
title: z.string().min(1, "Xizmat nomi majburiy"),
|
||||
title_ru: z.string().min(1, "Majburiy maydon"),
|
||||
@@ -157,10 +163,15 @@ export const TourformSchema = z.object({
|
||||
ticket_itinerary: z
|
||||
.array(
|
||||
z.object({
|
||||
id: z.number().optional(), // Edit uchun
|
||||
ticket_itinerary_image: z.array(
|
||||
z.object({
|
||||
image: z.union([z.instanceof(File), z.string()]),
|
||||
}),
|
||||
z.union([
|
||||
z.object({
|
||||
image: z.union([z.instanceof(File), z.string()]),
|
||||
}),
|
||||
z.instanceof(File),
|
||||
z.string(),
|
||||
]),
|
||||
),
|
||||
title: z.string().min(1, "Sarlavha majburiy"),
|
||||
title_ru: z.string().min(1, "Sarlavha (RU) majburiy"),
|
||||
|
||||
@@ -70,11 +70,7 @@ export interface GetOneTours {
|
||||
};
|
||||
},
|
||||
];
|
||||
ticket_images: [
|
||||
{
|
||||
image: string;
|
||||
},
|
||||
];
|
||||
ticket_images: [{ id: number; image: string }];
|
||||
ticket_amenities: {
|
||||
icon_name: string;
|
||||
id: number;
|
||||
@@ -84,6 +80,7 @@ export interface GetOneTours {
|
||||
}[];
|
||||
ticket_included_services: [
|
||||
{
|
||||
id: number;
|
||||
image: string;
|
||||
title: string;
|
||||
title_ru: string;
|
||||
@@ -94,6 +91,7 @@ export interface GetOneTours {
|
||||
},
|
||||
];
|
||||
ticket_itinerary: {
|
||||
id: number;
|
||||
title: string;
|
||||
title_ru: string;
|
||||
title_uz: string;
|
||||
@@ -111,17 +109,16 @@ export interface GetOneTours {
|
||||
},
|
||||
];
|
||||
}[];
|
||||
ticket_hotel_meals: [
|
||||
{
|
||||
image: string;
|
||||
name: string;
|
||||
name_ru: string;
|
||||
name_uz: string;
|
||||
desc: string;
|
||||
desc_ru: string;
|
||||
desc_uz: string;
|
||||
},
|
||||
];
|
||||
ticket_hotel_meals: {
|
||||
id: number;
|
||||
image: string;
|
||||
name: string;
|
||||
name_ru: string;
|
||||
name_uz: string;
|
||||
desc: string;
|
||||
desc_ru: string;
|
||||
desc_uz: string;
|
||||
}[];
|
||||
tariff: [
|
||||
{
|
||||
tariff: number;
|
||||
|
||||
@@ -111,87 +111,33 @@ const StepOne = ({
|
||||
|
||||
const tour = data.data;
|
||||
|
||||
// 🔹 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));
|
||||
|
||||
form.setValue("passenger_count", tour.passenger_count ?? 1);
|
||||
form.setValue("min_person", tour.min_person ?? 1);
|
||||
form.setValue("max_person", tour.max_person ?? 1);
|
||||
|
||||
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 ?? "");
|
||||
|
||||
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 ?? "");
|
||||
|
||||
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 ?? []);
|
||||
|
||||
// 🔹 Jo‘nash vaqti
|
||||
// 🔹 Jo'nash vaqti
|
||||
let departureDateTime = undefined;
|
||||
if (tour.departure_time) {
|
||||
const d = new Date(tour.departure_time);
|
||||
form.setValue("departureDateTime", {
|
||||
departureDateTime = {
|
||||
date: d,
|
||||
time: d.toTimeString().slice(0, 8),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
// 🔹 Qaytish vaqti
|
||||
let travelDateTime = undefined;
|
||||
if (tour.travel_time) {
|
||||
const d = new Date(tour.travel_time);
|
||||
form.setValue("travelDateTime", {
|
||||
travelDateTime = {
|
||||
date: d,
|
||||
time: d.toTimeString().slice(0, 8),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
form.setValue(
|
||||
"amenities",
|
||||
tour.ticket_amenities.map((e) => e.id),
|
||||
);
|
||||
|
||||
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
|
||||
setTransportPrices(transports.map((t) => formatPrice(t.price ?? 0)));
|
||||
|
||||
// 🔹 Tarif
|
||||
const tariffs =
|
||||
@@ -199,52 +145,90 @@ const StepOne = ({
|
||||
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 ?? "",
|
||||
})) ?? [],
|
||||
})) ?? [],
|
||||
);
|
||||
form.reset({
|
||||
title: tour.title_uz ?? "",
|
||||
title_ru: tour.title_ru ?? "",
|
||||
price: tour.price ?? 0,
|
||||
passenger_count: tour.passenger_count ?? 1,
|
||||
min_person: tour.min_person ?? 1,
|
||||
max_person: tour.max_person ?? 1,
|
||||
departure: tour.departure_uz ?? "",
|
||||
departure_ru: tour.departure_ru ?? "",
|
||||
destination: tour.destination_uz ?? "",
|
||||
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: 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,
|
||||
})) ?? [],
|
||||
});
|
||||
|
||||
// 🔹 Banner va rasmlar
|
||||
form.setValue("banner", tour.image_banner ?? null);
|
||||
form.setValue("images", tour.ticket_images?.map((img) => img.image) ?? []);
|
||||
// Display price
|
||||
setDisplayPrice(formatPrice(tour.price ?? 0));
|
||||
|
||||
// 🔹 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
|
||||
// TicketStore uchun id
|
||||
setId(tour.id);
|
||||
}, [isEditMode, data, form, setId]);
|
||||
|
||||
@@ -287,6 +271,7 @@ const StepOne = ({
|
||||
function onSubmit(value: z.infer<typeof TourformSchema>) {
|
||||
const formData = new FormData();
|
||||
|
||||
// Asosiy ma'lumotlar
|
||||
formData.append("title", value.title);
|
||||
formData.append("location_name", value.location_name);
|
||||
formData.append("location_name_ru", value.location_name_ru);
|
||||
@@ -294,7 +279,6 @@ const StepOne = ({
|
||||
"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));
|
||||
@@ -319,61 +303,96 @@ 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"));
|
||||
|
||||
if (value.banner instanceof File) {
|
||||
formData.append("image_banner", value.banner);
|
||||
}
|
||||
console.log(value.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 instanceof File) {
|
||||
formData.append(`ticket_images`, 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.image instanceof File) {
|
||||
formData.append(`ticket_included_services[${i}]image`, e.image);
|
||||
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);
|
||||
}
|
||||
});
|
||||
value.ticket_itinerary?.forEach((itinerary, i) => {
|
||||
itinerary.ticket_itinerary_image.forEach((img) => {
|
||||
if (img.image instanceof File) {
|
||||
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),
|
||||
);
|
||||
}
|
||||
});
|
||||
// Har bir itinerary uchun asosiy maydonlar
|
||||
|
||||
// 🖼 Rasmlar (faqat yangi yuklangan File-larni yuborish)
|
||||
// 🔹 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) => {
|
||||
// img -> File yoki { image: File | string } shaklida bo‘lishi mumkin
|
||||
const file =
|
||||
img instanceof File
|
||||
? img
|
||||
: img?.image instanceof File
|
||||
? img.image
|
||||
: null;
|
||||
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(
|
||||
@@ -383,45 +402,52 @@ const StepOne = ({
|
||||
}
|
||||
});
|
||||
}
|
||||
itinerary.ticket_itinerary_image.forEach((img) => {
|
||||
if (Array.isArray(itinerary.ticket_itinerary_destinations)) {
|
||||
if (img.image instanceof File) {
|
||||
// 📍 Destinations (yo‘nalishlar)
|
||||
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,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 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,
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
value.hotel_meals.forEach((e, i) => {
|
||||
if (e.image instanceof File) {
|
||||
formData.append(`ticket_hotel_meals[${i}]image`, e.image);
|
||||
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,
|
||||
@@ -1412,7 +1438,15 @@ const StepOne = ({
|
||||
form={form}
|
||||
name="images"
|
||||
label={t("Qo'shimcha rasmlar")}
|
||||
imageUrl={data?.data.ticket_images?.map((img) => img.image)}
|
||||
includeId={true}
|
||||
imageUrl={
|
||||
isEditMode && data?.data?.ticket_images
|
||||
? data.data.ticket_images.map((img) => ({
|
||||
id: img.id,
|
||||
image: img.image,
|
||||
}))
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
@@ -1963,172 +1997,198 @@ const StepOne = ({
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ticket_itinerary"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Label className="text-md">{t("Yo'nalishlar")}</Label>
|
||||
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 "";
|
||||
};
|
||||
|
||||
<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={
|
||||
item.ticket_itinerary_image[0]?.image instanceof File
|
||||
? URL.createObjectURL(
|
||||
item.ticket_itinerary_image[0]?.image,
|
||||
)
|
||||
: item.ticket_itinerary_image[0]?.image // agar serverdan kelsa
|
||||
}
|
||||
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>
|
||||
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>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
))}
|
||||
</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.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>
|
||||
))}
|
||||
|
||||
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>
|
||||
|
||||
{/* 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>
|
||||
)}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="w-full flex justify-end">
|
||||
|
||||
@@ -12,12 +12,18 @@ import { ImagePlus, XIcon } from "lucide-react";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface ImageObject {
|
||||
id?: number;
|
||||
image: string | File;
|
||||
}
|
||||
|
||||
interface TicketsImagesModelProps {
|
||||
form: any; // React Hook Form control
|
||||
form: any;
|
||||
name: string;
|
||||
label?: string;
|
||||
imageUrl?: string | string[] | undefined;
|
||||
imageUrl?: string | File | ImageObject | (string | File | ImageObject)[];
|
||||
multiple?: boolean;
|
||||
includeId?: boolean; // agar true bo‘lsa — {id, image} jo‘natiladi
|
||||
}
|
||||
|
||||
export default function TicketsImagesModel({
|
||||
@@ -26,20 +32,92 @@ export default function TicketsImagesModel({
|
||||
label = "Rasmlar",
|
||||
multiple = true,
|
||||
imageUrl,
|
||||
includeId = false,
|
||||
}: TicketsImagesModelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [previews, setPreviews] = useState<string[]>([]);
|
||||
const inputId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
if (imageUrl) {
|
||||
const urls = Array.isArray(imageUrl) ? imageUrl : [imageUrl];
|
||||
setPreviews(urls);
|
||||
if (!imageUrl) return;
|
||||
const items = Array.isArray(imageUrl) ? imageUrl : [imageUrl];
|
||||
|
||||
// 🔥 form bilan sinxronlash
|
||||
form.setValue(name, urls);
|
||||
if (multiple) {
|
||||
const values: any[] = [];
|
||||
const urls: string[] = [];
|
||||
|
||||
items.forEach((item) => {
|
||||
if (typeof item === "string") {
|
||||
urls.push(item);
|
||||
values.push(includeId ? { image: item } : item);
|
||||
} else if (item instanceof File) {
|
||||
urls.push(URL.createObjectURL(item));
|
||||
values.push(includeId ? { image: item } : item);
|
||||
} else if (typeof item === "object" && "image" in item) {
|
||||
const img = item.image;
|
||||
urls.push(typeof img === "string" ? img : URL.createObjectURL(img));
|
||||
values.push(includeId ? { id: item.id, image: img } : img);
|
||||
}
|
||||
});
|
||||
|
||||
setPreviews(urls);
|
||||
form.setValue(name, values);
|
||||
} else {
|
||||
const single = items[0];
|
||||
let url = "";
|
||||
let valueToSet: any = null;
|
||||
|
||||
if (typeof single === "string") {
|
||||
url = single;
|
||||
valueToSet = single;
|
||||
} else if (single instanceof File) {
|
||||
url = URL.createObjectURL(single);
|
||||
valueToSet = single;
|
||||
} else if (typeof single === "object" && "image" in single) {
|
||||
const img = single.image;
|
||||
url = typeof img === "string" ? img : URL.createObjectURL(img);
|
||||
valueToSet =
|
||||
includeId && single.id ? { id: single.id, image: img } : img;
|
||||
}
|
||||
|
||||
setPreviews(url ? [url] : []);
|
||||
form.setValue(name, valueToSet); // 🔥 array emas
|
||||
}
|
||||
}, [imageUrl, form, name]);
|
||||
}, [imageUrl, form, name, multiple, includeId]);
|
||||
|
||||
const handleFileChange = (files: FileList | null) => {
|
||||
if (!files) return;
|
||||
const newFiles = Array.from(files);
|
||||
|
||||
if (multiple) {
|
||||
const current = form.getValues(name) || [];
|
||||
const newValue = includeId
|
||||
? [...current, ...newFiles.map((f) => ({ image: f }))]
|
||||
: [...current, ...newFiles];
|
||||
form.setValue(name, newValue);
|
||||
setPreviews([
|
||||
...previews,
|
||||
...newFiles.map((f) => URL.createObjectURL(f)),
|
||||
]);
|
||||
} else {
|
||||
const singleFile = newFiles[0];
|
||||
const valueToSet = includeId ? { image: singleFile } : singleFile;
|
||||
form.setValue(name, valueToSet); // 🔥 array emas
|
||||
setPreviews(singleFile ? [URL.createObjectURL(singleFile)] : []);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
if (multiple) {
|
||||
const current = form.getValues(name) || [];
|
||||
const newValue = current.filter((_: any, i: number) => i !== index);
|
||||
form.setValue(name, newValue);
|
||||
setPreviews(previews.filter((_, i) => i !== index));
|
||||
} else {
|
||||
form.setValue(name, null);
|
||||
setPreviews([]);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormField
|
||||
@@ -48,47 +126,17 @@ export default function TicketsImagesModel({
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Label className="text-md">{label}</Label>
|
||||
|
||||
<FormControl>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* File Input */}
|
||||
<Input
|
||||
id={inputId}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple={multiple}
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const newFiles = e.target.files
|
||||
? Array.from(e.target.files)
|
||||
: [];
|
||||
const currentValue = form.getValues(name) || [];
|
||||
const currentUrls = previews || [];
|
||||
|
||||
if (multiple) {
|
||||
// ✅ eski URL’larni va yangi fayllarni birlashtirish
|
||||
const combined = [...currentValue, ...newFiles];
|
||||
form.setValue(name, combined);
|
||||
|
||||
const newPreviews = [
|
||||
...currentUrls,
|
||||
...newFiles.map((file: File) =>
|
||||
URL.createObjectURL(file),
|
||||
),
|
||||
];
|
||||
setPreviews(newPreviews);
|
||||
} else {
|
||||
// ✅ bitta rasm holati
|
||||
const singleFile = newFiles[0] || null;
|
||||
form.setValue(name, singleFile);
|
||||
setPreviews(
|
||||
singleFile ? [URL.createObjectURL(singleFile)] : [],
|
||||
);
|
||||
}
|
||||
}}
|
||||
onChange={(e) => handleFileChange(e.target.files)}
|
||||
/>
|
||||
|
||||
{/* Upload Zone */}
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="border-2 border-dashed border-gray-300 h-40 rounded-2xl flex flex-col justify-center items-center cursor-pointer hover:bg-muted/20 transition"
|
||||
@@ -97,18 +145,13 @@ export default function TicketsImagesModel({
|
||||
<p className="font-semibold text-white">
|
||||
{t("Rasmlarni tanlang")}
|
||||
</p>
|
||||
{multiple ? (
|
||||
<p className="text-sm text-white">
|
||||
{t("Bir nechta rasm yuklashingiz mumkin")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-white">
|
||||
{t("Faqat bitta rasm yuklash mumkin")}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-sm text-white">
|
||||
{multiple
|
||||
? t("Bir nechta rasm yuklashingiz mumkin")
|
||||
: t("Faqat bitta rasm yuklash mumkin")}
|
||||
</p>
|
||||
</label>
|
||||
|
||||
{/* Preview Images */}
|
||||
{previews.length > 0 && (
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{previews.map((src, i) => (
|
||||
@@ -121,28 +164,9 @@ export default function TicketsImagesModel({
|
||||
alt={`preview-${i}`}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (multiple) {
|
||||
// ✅ Ko‘p rasm holati
|
||||
const currentFiles = form.getValues(name) || [];
|
||||
const newFiles = currentFiles.filter(
|
||||
(_: File, idx: number) => idx !== i,
|
||||
);
|
||||
form.setValue(name, newFiles);
|
||||
const newPreviews = previews.filter(
|
||||
(_: string, idx: number) => idx !== i,
|
||||
);
|
||||
setPreviews(newPreviews);
|
||||
} else {
|
||||
// ✅ Bitta rasm holati
|
||||
form.setValue(name, null);
|
||||
setPreviews([]);
|
||||
}
|
||||
}}
|
||||
onClick={() => handleDelete(i)}
|
||||
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 shadow hover:bg-white transition"
|
||||
>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
@@ -153,7 +177,6 @@ export default function TicketsImagesModel({
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user