Merge pull request #9 from SamandarTurgunboyev/samandar

update
This commit is contained in:
Samandar Turg'unboev
2025-11-07 17:21:06 +05:00
committed by GitHub
5 changed files with 497 additions and 406 deletions

View File

@@ -496,7 +496,7 @@ export default function Seo() {
<button <button
onClick={handleSave} 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" 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 ? ( {isPending || editPending ? (

View File

@@ -51,10 +51,10 @@ export const TourformSchema = z.object({
message: "Iltimos, visa talabliligini tanlang", message: "Iltimos, visa talabliligini tanlang",
}), }),
departureDateTime: z.object({ departureDateTime: z.object({
date: z.date({ message: "Jonash vaqti majburiy" }), date: z.date({ message: "Jo'nash vaqti majburiy" }),
time: z time: z
.string() .string()
.min(1, { message: "Jonash vaqti majburiy" }) .min(1, { message: "Jo'nash vaqti majburiy" })
.refine( .refine(
(val) => { (val) => {
const parts = val.split(":"); const parts = val.split(":");
@@ -76,10 +76,10 @@ export const TourformSchema = z.object({
), ),
}), }),
travelDateTime: z.object({ travelDateTime: z.object({
date: z.date({ message: "Jonash vaqti majburiy" }), date: z.date({ message: "Jo'nash vaqti majburiy" }),
time: z time: z
.string() .string()
.min(1, { message: "Jonash vaqti majburiy" }) .min(1, { message: "Jo'nash vaqti majburiy" })
.refine( .refine(
(val) => { (val) => {
const parts = val.split(":"); const parts = val.split(":");
@@ -125,14 +125,19 @@ export const TourformSchema = z.object({
.optional(), .optional(),
banner: z.any().nullable(), banner: z.any().nullable(),
images: z images: z
.array(z.union([z.instanceof(File), z.string()])) .array(
.min(1, { message: "Kamida bitta rasm yuklang." }), 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(), amenities: z.array(z.number()).optional(),
// 🔹 Quyidagilar endi ixtiyoriy (required emas)
hotel_services: z hotel_services: z
.array( .array(
z.object({ z.object({
id: z.number().optional(),
image: z.any().nullable(), image: z.any().nullable(),
title: z.string().min(1, "Xizmat nomi majburiy"), title: z.string().min(1, "Xizmat nomi majburiy"),
title_ru: z.string().min(1, { message: "Majburiy maydon" }), title_ru: z.string().min(1, { message: "Majburiy maydon" }),
@@ -145,6 +150,7 @@ export const TourformSchema = z.object({
hotel_meals: z hotel_meals: z
.array( .array(
z.object({ z.object({
id: z.number().optional(),
image: z.any().nullable(), image: z.any().nullable(),
title: z.string().min(1, "Xizmat nomi majburiy"), title: z.string().min(1, "Xizmat nomi majburiy"),
title_ru: z.string().min(1, "Majburiy maydon"), title_ru: z.string().min(1, "Majburiy maydon"),
@@ -157,10 +163,15 @@ export const TourformSchema = z.object({
ticket_itinerary: z ticket_itinerary: z
.array( .array(
z.object({ z.object({
id: z.number().optional(), // Edit uchun
ticket_itinerary_image: z.array( ticket_itinerary_image: z.array(
z.union([
z.object({ z.object({
image: z.union([z.instanceof(File), z.string()]), image: z.union([z.instanceof(File), z.string()]),
}), }),
z.instanceof(File),
z.string(),
]),
), ),
title: z.string().min(1, "Sarlavha majburiy"), title: z.string().min(1, "Sarlavha majburiy"),
title_ru: z.string().min(1, "Sarlavha (RU) majburiy"), title_ru: z.string().min(1, "Sarlavha (RU) majburiy"),

View File

@@ -70,11 +70,7 @@ export interface GetOneTours {
}; };
}, },
]; ];
ticket_images: [ ticket_images: [{ id: number; image: string }];
{
image: string;
},
];
ticket_amenities: { ticket_amenities: {
icon_name: string; icon_name: string;
id: number; id: number;
@@ -84,6 +80,7 @@ export interface GetOneTours {
}[]; }[];
ticket_included_services: [ ticket_included_services: [
{ {
id: number;
image: string; image: string;
title: string; title: string;
title_ru: string; title_ru: string;
@@ -94,6 +91,7 @@ export interface GetOneTours {
}, },
]; ];
ticket_itinerary: { ticket_itinerary: {
id: number;
title: string; title: string;
title_ru: string; title_ru: string;
title_uz: string; title_uz: string;
@@ -111,8 +109,8 @@ export interface GetOneTours {
}, },
]; ];
}[]; }[];
ticket_hotel_meals: [ ticket_hotel_meals: {
{ id: number;
image: string; image: string;
name: string; name: string;
name_ru: string; name_ru: string;
@@ -120,8 +118,7 @@ export interface GetOneTours {
desc: string; desc: string;
desc_ru: string; desc_ru: string;
desc_uz: string; desc_uz: string;
}, }[];
];
tariff: [ tariff: [
{ {
tariff: number; tariff: number;

View File

@@ -111,87 +111,33 @@ const StepOne = ({
const tour = data.data; const tour = data.data;
// 🔹 Oddiy text maydonlar // 🔹 Jo'nash vaqti
form.setValue("title", tour.title_uz ?? ""); let departureDateTime = undefined;
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 ?? []);
// 🔹 Jonash vaqti
if (tour.departure_time) { if (tour.departure_time) {
const d = new Date(tour.departure_time); const d = new Date(tour.departure_time);
form.setValue("departureDateTime", { departureDateTime = {
date: d, date: d,
time: d.toTimeString().slice(0, 8), time: d.toTimeString().slice(0, 8),
}); };
} }
// 🔹 Qaytish vaqti // 🔹 Qaytish vaqti
let travelDateTime = undefined;
if (tour.travel_time) { if (tour.travel_time) {
const d = new Date(tour.travel_time); const d = new Date(tour.travel_time);
form.setValue("travelDateTime", { travelDateTime = {
date: d, date: d,
time: d.toTimeString().slice(0, 8), 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 // 🔹 Transport
const transports = const transports =
tour.transports?.map((t, i) => ({ tour.transports?.map((t, i) => ({
transport: i + 1, transport: i + 1,
price: t.price ?? 0, price: t.price ?? 0,
})) ?? []; })) ?? [];
form.setValue("transport", transports); setTransportPrices(transports.map((t) => formatPrice(t.price ?? 0)));
setTransportPrices(transports.map((t) => formatPrice(t.price ?? 0))); // 👈 YANGI QOSHILGAN
// 🔹 Tarif // 🔹 Tarif
const tariffs = const tariffs =
@@ -199,52 +145,90 @@ const StepOne = ({
tariff: t.tariff ?? 0, tariff: t.tariff ?? 0,
price: t.price ?? 0, price: t.price ?? 0,
})) ?? []; })) ?? [];
form.setValue("tarif", tariffs);
setTarifDisplayPrice(tariffs.map((t) => formatPrice(t.price ?? 0))); setTarifDisplayPrice(tariffs.map((t) => formatPrice(t.price ?? 0)));
// 🔹 Yonalishlar (ticket_itinerary) form.reset({
form.setValue( title: tour.title_uz ?? "",
"ticket_itinerary", 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) => ({ 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: ticket_itinerary_image:
item.ticket_itinerary_image?.map((img) => ({ item.ticket_itinerary_image?.map((img) => ({
image: img.image, image: img.image,
})) ?? [], })) ?? [],
title: item.title ?? "",
title_ru: item.title_ru ?? "",
duration: item.duration ?? 1,
ticket_itinerary_destinations: ticket_itinerary_destinations:
item.ticket_itinerary_destinations?.map((d) => ({ item.ticket_itinerary_destinations?.map((dest) => ({
name: d.name ?? "", name: dest.name_uz ?? dest.name ?? "",
name_ru: d.name_ru ?? "", name_ru: dest.name_ru ?? "",
})) ?? [], })) ?? [],
})) ?? [], })) ?? [],
); banner: tour.image_banner ?? null,
images: tour.ticket_images.map((img) => ({
// 🔹 Banner va rasmlar id: img.id,
form.setValue("banner", tour.image_banner ?? null); image: img.image,
form.setValue("images", tour.ticket_images?.map((img) => img.image) ?? []); })),
extra_service:
// 🔹 Bepul xizmatlar (extra_service)
form.setValue(
"extra_service",
tour.extra_service?.map((s) => ({ tour.extra_service?.map((s) => ({
name: s.name_uz ?? s.name ?? "", name: s.name_uz ?? s.name ?? "",
name_ru: s.name_ru ?? "", name_ru: s.name_ru ?? "",
})) ?? [], })) ?? [],
); paid_extra_service:
// 🔹 Pullik xizmatlar (paid_extra_service)
form.setValue(
"paid_extra_service",
tour.paid_extra_service?.map((s) => ({ tour.paid_extra_service?.map((s) => ({
name: s.name_uz ?? s.name ?? "", name: s.name_uz ?? s.name ?? "",
name_ru: s.name_ru ?? "", name_ru: s.name_ru ?? "",
price: s.price ?? 0, price: s.price ?? 0,
})) ?? [], })) ?? [],
); });
// 🔹 TicketStore uchun id // Display price
setDisplayPrice(formatPrice(tour.price ?? 0));
// TicketStore uchun id
setId(tour.id); setId(tour.id);
}, [isEditMode, data, form, setId]); }, [isEditMode, data, form, setId]);
@@ -287,6 +271,7 @@ const StepOne = ({
function onSubmit(value: z.infer<typeof TourformSchema>) { function onSubmit(value: z.infer<typeof TourformSchema>) {
const formData = new FormData(); const formData = new FormData();
// Asosiy ma'lumotlar
formData.append("title", value.title); formData.append("title", value.title);
formData.append("location_name", value.location_name); formData.append("location_name", value.location_name);
formData.append("location_name_ru", value.location_name_ru); formData.append("location_name_ru", value.location_name_ru);
@@ -294,7 +279,6 @@ const StepOne = ({
"visa_required", "visa_required",
value.visa_required === "yes" ? "true" : "false", value.visa_required === "yes" ? "true" : "false",
); );
formData.append("title_ru", value.title_ru); formData.append("title_ru", value.title_ru);
formData.append("price", String(value.price)); formData.append("price", String(value.price));
formData.append("min_person", String(value.min_person)); 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("hotel_meals_ru", value.hotel_meals_info_ru);
formData.append("duration_days", String(value.duration)); formData.append("duration_days", String(value.duration));
formData.append("rating", String("0.0")); formData.append("rating", String("0.0"));
if (value.banner instanceof File) { if (value.banner instanceof File) {
formData.append("image_banner", value.banner); formData.append("image_banner", value.banner);
} }
console.log(value.banner, "value.banner");
// Tarif va transport
value.tarif?.forEach((e, i) => { value.tarif?.forEach((e, i) => {
formData.append(`tariff[${i}]tariff`, String(e.tariff)); formData.append(`tariff[${i}]tariff`, String(e.tariff));
formData.append(`tariff[${i}]price`, String(e.price)); formData.append(`tariff[${i}]price`, String(e.price));
}); });
value.transport?.forEach((e, i) => { value.transport?.forEach((e, i) => {
formData.append(`transports[${i}]transport`, String(e.transport)); formData.append(`transports[${i}]transport`, String(e.transport));
formData.append(`transports[${i}]price`, String(e.price)); formData.append(`transports[${i}]price`, String(e.price));
}); });
value.badges?.forEach((e) => { value.badges?.forEach((e) => {
formData.append(`badge`, String(e)); formData.append(`badge`, String(e));
}); });
value.amenities?.forEach((e) => { value.amenities?.forEach((e) => {
formData.append(`ticket_amenities`, String(e)); formData.append(`ticket_amenities`, String(e));
}); });
value.images.forEach((e) => { value.images.forEach((e) => {
if (e instanceof File) { if (e.id) {
formData.append(`ticket_images`, e); 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 &&
value.hotel_services.forEach((e, i) => { value.hotel_services.forEach((e, i) => {
if (e.id && typeof e.image === "string") {
formData.append(`ticket_included_services_ids`, String(e.id));
} else {
// Yangi xizmat yoki o'zgartirilgan xizmat
if (e.image instanceof File) { if (e.image instanceof File) {
formData.append(`ticket_included_services[${i}]image`, e.image); 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`, e.title);
formData.append(`ticket_included_services[${i}]title_ru`, e.title_ru); 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_ru`, e.desc_ru);
formData.append(`ticket_included_services[${i}]desc`, e.description); formData.append(`ticket_included_services[${i}]desc`, e.description);
} }
}); });
// 🔹 Ticket Itinerary - edit rejimida ID yuborish
value.ticket_itinerary?.forEach((itinerary, i) => { value.ticket_itinerary?.forEach((itinerary, i) => {
itinerary.ticket_itinerary_image.forEach((img) => { if (itinerary.id) {
if (img.image instanceof File) { // 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`, itinerary.title);
formData.append(`ticket_itinerary[${i}]title_ru`, itinerary.title_ru); formData.append(`ticket_itinerary[${i}]title_ru`, itinerary.title_ru);
formData.append( formData.append(
`ticket_itinerary[${i}]duration`, `ticket_itinerary[${i}]duration`,
String(itinerary.duration), String(itinerary.duration),
); );
}
});
// Har bir itinerary uchun asosiy maydonlar
// 🖼 Rasmlar (faqat yangi yuklangan File-larni yuborish) // Rasmlar
if (Array.isArray(itinerary.ticket_itinerary_image)) { if (Array.isArray(itinerary.ticket_itinerary_image)) {
itinerary.ticket_itinerary_image.forEach((img, j) => { itinerary.ticket_itinerary_image.forEach((img, j) => {
// img -> File yoki { image: File | string } shaklida bolishi mumkin let file: File | null = null;
const file =
img instanceof File if (img instanceof File) {
? img file = img;
: img?.image instanceof File } else if (
? img.image typeof img === "object" &&
: null; "image" in img &&
img.image instanceof File
) {
file = img.image;
}
if (file) { if (file) {
formData.append( formData.append(
@@ -383,10 +402,9 @@ const StepOne = ({
} }
}); });
} }
itinerary.ticket_itinerary_image.forEach((img) => {
// Destinations
if (Array.isArray(itinerary.ticket_itinerary_destinations)) { if (Array.isArray(itinerary.ticket_itinerary_destinations)) {
if (img.image instanceof File) {
// 📍 Destinations (yonalishlar)
itinerary.ticket_itinerary_destinations.forEach((dest, k) => { itinerary.ticket_itinerary_destinations.forEach((dest, k) => {
formData.append( formData.append(
`ticket_itinerary[${i}]ticket_itinerary_destinations[${k}]name`, `ticket_itinerary[${i}]ticket_itinerary_destinations[${k}]name`,
@@ -398,30 +416,38 @@ const StepOne = ({
); );
}); });
} }
}
});
}); });
value.hotel_meals.forEach((e, i) => { value.hotel_meals.forEach((e, i) => {
if (e.id && typeof e.image === "string") {
// Mavjud meal (o'zgartirilmagan)
formData.append(`ticket_hotel_meals_ids`, String(e.id));
} else {
// Yangi meal yoki o'zgartirilgan meal
if (e.image instanceof File) { if (e.image instanceof File) {
formData.append(`ticket_hotel_meals[${i}]image`, e.image); 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`, e.title);
formData.append(`ticket_hotel_meals[${i}]name_ru`, e.title_ru); 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`, e.description);
formData.append(`ticket_hotel_meals[${i}]desc_ru`, e.desc_ru); formData.append(`ticket_hotel_meals[${i}]desc_ru`, e.desc_ru);
} }
}); });
// Extra services
value.extra_service && value.extra_service &&
value.extra_service.forEach((e, i) => { value.extra_service.forEach((e, i) => {
formData.append(`extra_service[${i}]name`, e.name); formData.append(`extra_service[${i}]name`, e.name);
formData.append(`extra_service[${i}]name_ru`, e.name_ru); formData.append(`extra_service[${i}]name_ru`, e.name_ru);
}); });
value.paid_extra_service && value.paid_extra_service &&
value.paid_extra_service.forEach((e, i) => { value.paid_extra_service.forEach((e, i) => {
formData.append(`paid_extra_service[${i}]name`, e.name); 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}]name_ru`, e.name_ru);
formData.append(`paid_extra_service[${i}]price`, String(e.price)); formData.append(`paid_extra_service[${i}]price`, String(e.price));
}); });
if (isEditMode && id) { if (isEditMode && id) {
update({ update({
body: formData, body: formData,
@@ -1412,7 +1438,15 @@ const StepOne = ({
form={form} form={form}
name="images" name="images"
label={t("Qo'shimcha rasmlar")} 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 <FormField
@@ -1963,12 +1997,43 @@ const StepOne = ({
<FormField <FormField
control={form.control} control={form.control}
name="ticket_itinerary" name="ticket_itinerary"
render={() => ( render={() => {
// Helper function to get image source
const getImageSrc = (imageItem: any): string => {
// Agar object bo'lsa va image property bo'lsa
if (
imageItem &&
typeof imageItem === "object" &&
"image" in imageItem
) {
const img = imageItem.image;
// File bo'lsa
if (img instanceof File) {
return URL.createObjectURL(img);
}
// String (URL) bo'lsa
if (typeof img === "string") {
return img;
}
}
// Agar to'g'ridan-to'g'ri File bo'lsa
if (imageItem instanceof File) {
return URL.createObjectURL(imageItem);
}
// Agar string bo'lsa
if (typeof imageItem === "string") {
return imageItem;
}
// Default fallback
return "";
};
return (
<FormItem> <FormItem>
<Label className="text-md">{t("Yo'nalishlar")}</Label> <Label className="text-md">{t("Yo'nalishlar")}</Label>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{/* mavjud yonalishlar */} {/* mavjud yo'nalishlar */}
<div className="flex flex-wrap gap-4"> <div className="flex flex-wrap gap-4">
{form.watch("ticket_itinerary")?.map((item, idx) => ( {form.watch("ticket_itinerary")?.map((item, idx) => (
<div <div
@@ -1976,13 +2041,7 @@ const StepOne = ({
className="relative w-48 border rounded-xl overflow-hidden shadow-sm" className="relative w-48 border rounded-xl overflow-hidden shadow-sm"
> >
<img <img
src={ src={getImageSrc(item.ticket_itinerary_image[0])}
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} alt={item.title}
className="object-cover w-full h-32" className="object-cover w-full h-32"
/> />
@@ -2013,7 +2072,7 @@ const StepOne = ({
))} ))}
</div> </div>
{/* yangi yonalish qoshish formasi */} {/* yangi yo'nalish qo'shish formasi */}
<div className="flex flex-col gap-3 border rounded-xl p-4 bg-muted/10"> <div className="flex flex-col gap-3 border rounded-xl p-4 bg-muted/10">
<Label className="text-md font-semibold"> <Label className="text-md font-semibold">
{t("Yo'nalish qo'shish")} {t("Yo'nalish qo'shish")}
@@ -2128,7 +2187,8 @@ const StepOne = ({
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} );
}}
/> />
<div className="w-full flex justify-end"> <div className="w-full flex justify-end">

View File

@@ -12,12 +12,18 @@ import { ImagePlus, XIcon } from "lucide-react";
import { useEffect, useId, useState } from "react"; import { useEffect, useId, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
interface ImageObject {
id?: number;
image: string | File;
}
interface TicketsImagesModelProps { interface TicketsImagesModelProps {
form: any; // React Hook Form control form: any;
name: string; name: string;
label?: string; label?: string;
imageUrl?: string | string[] | undefined; imageUrl?: string | File | ImageObject | (string | File | ImageObject)[];
multiple?: boolean; multiple?: boolean;
includeId?: boolean; // agar true bolsa — {id, image} jonatiladi
} }
export default function TicketsImagesModel({ export default function TicketsImagesModel({
@@ -26,20 +32,92 @@ export default function TicketsImagesModel({
label = "Rasmlar", label = "Rasmlar",
multiple = true, multiple = true,
imageUrl, imageUrl,
includeId = false,
}: TicketsImagesModelProps) { }: TicketsImagesModelProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const [previews, setPreviews] = useState<string[]>([]); const [previews, setPreviews] = useState<string[]>([]);
const inputId = useId(); const inputId = useId();
useEffect(() => { useEffect(() => {
if (imageUrl) { if (!imageUrl) return;
const urls = Array.isArray(imageUrl) ? imageUrl : [imageUrl]; const items = Array.isArray(imageUrl) ? imageUrl : [imageUrl];
setPreviews(urls);
// 🔥 form bilan sinxronlash if (multiple) {
form.setValue(name, urls); 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);
} }
}, [imageUrl, form, name]); });
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, 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 ( return (
<FormField <FormField
@@ -48,47 +126,17 @@ export default function TicketsImagesModel({
render={() => ( render={() => (
<FormItem> <FormItem>
<Label className="text-md">{label}</Label> <Label className="text-md">{label}</Label>
<FormControl> <FormControl>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
{/* File Input */}
<Input <Input
id={inputId} id={inputId}
type="file" type="file"
accept="image/*" accept="image/*"
multiple={multiple} multiple={multiple}
className="hidden" className="hidden"
onChange={(e) => { onChange={(e) => handleFileChange(e.target.files)}
const newFiles = e.target.files
? Array.from(e.target.files)
: [];
const currentValue = form.getValues(name) || [];
const currentUrls = previews || [];
if (multiple) {
// ✅ eski URLlarni 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)] : [],
);
}
}}
/> />
{/* Upload Zone */}
<label <label
htmlFor={inputId} 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" 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"> <p className="font-semibold text-white">
{t("Rasmlarni tanlang")} {t("Rasmlarni tanlang")}
</p> </p>
{multiple ? (
<p className="text-sm text-white"> <p className="text-sm text-white">
{t("Bir nechta rasm yuklashingiz mumkin")} {multiple
? t("Bir nechta rasm yuklashingiz mumkin")
: t("Faqat bitta rasm yuklash mumkin")}
</p> </p>
) : (
<p className="text-sm text-white">
{t("Faqat bitta rasm yuklash mumkin")}
</p>
)}
</label> </label>
{/* Preview Images */}
{previews.length > 0 && ( {previews.length > 0 && (
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
{previews.map((src, i) => ( {previews.map((src, i) => (
@@ -121,28 +164,9 @@ export default function TicketsImagesModel({
alt={`preview-${i}`} alt={`preview-${i}`}
className="object-cover w-full h-full" className="object-cover w-full h-full"
/> />
{/* Delete Button */}
<button <button
type="button" type="button"
onClick={() => { onClick={() => handleDelete(i)}
if (multiple) {
// ✅ Kop 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([]);
}
}}
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 shadow hover:bg-white transition" className="absolute top-1 right-1 bg-white/80 rounded-full p-1 shadow hover:bg-white transition"
> >
<XIcon className="size-4 text-destructive" /> <XIcon className="size-4 text-destructive" />
@@ -153,7 +177,6 @@ export default function TicketsImagesModel({
)} )}
</div> </div>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}