coutry crud added

This commit is contained in:
Samandar Turgunboyev
2025-12-04 12:41:01 +05:00
parent 394d158947
commit da46b6cac1
19 changed files with 2193 additions and 163 deletions

8
package-lock.json generated
View File

@@ -36,7 +36,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dayjs": "^1.11.18", "dayjs": "^1.11.18",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.25",
"i18next": "^25.5.2", "i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
@@ -4401,9 +4401,9 @@
} }
}, },
"node_modules/framer-motion": { "node_modules/framer-motion": {
"version": "12.23.24", "version": "12.23.25",
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.25.tgz",
"integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", "integrity": "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"motion-dom": "^12.23.23", "motion-dom": "^12.23.23",

View File

@@ -40,7 +40,7 @@
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"dayjs": "^1.11.18", "dayjs": "^1.11.18",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.25",
"i18next": "^25.5.2", "i18next": "^25.5.2",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",

View File

@@ -1,5 +1,7 @@
import type { import type {
AllAmenitiesData, AllAmenitiesData,
CountryDeatil,
CountryList,
CreateTourRes, CreateTourRes,
DetailAmenitiesData, DetailAmenitiesData,
GetAllTours, GetAllTours,
@@ -18,10 +20,13 @@ import type {
HotelAllFeaturesType, HotelAllFeaturesType,
HotelFeaturesDetail, HotelFeaturesDetail,
HotelFeaturesTypeDetail, HotelFeaturesTypeDetail,
HotelMealdetail,
HotelMealList,
} from "@/pages/tours/lib/type"; } from "@/pages/tours/lib/type";
import httpClient from "@/shared/config/api/httpClient"; import httpClient from "@/shared/config/api/httpClient";
import { import {
AMENITIES, AMENITIES,
COUNTRY,
GET_TICKET, GET_TICKET,
HOTEL, HOTEL,
HOTEL_BADGE, HOTEL_BADGE,
@@ -29,7 +34,9 @@ import {
HOTEL_FEATURES_TYPE, HOTEL_FEATURES_TYPE,
HOTEL_TARIF, HOTEL_TARIF,
HPTEL_TYPES, HPTEL_TYPES,
MEAL_PLAN,
POPULAR_TOURS, POPULAR_TOURS,
REGION,
TOUR_TRANSPORT, TOUR_TRANSPORT,
} from "@/shared/config/api/URLs"; } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios"; import type { AxiosResponse } from "axios";
@@ -500,10 +507,124 @@ const amenitiesUpdate = async ({
return response; return response;
}; };
const countryList = async (params: {
page: number;
page_size: number;
name?: string;
}): Promise<AxiosResponse<CountryList>> => {
const res = await httpClient.get(COUNTRY, { params });
return res;
};
const countryDeatil = async (
id: number,
): Promise<AxiosResponse<CountryDeatil>> => {
const res = await httpClient.get(`${COUNTRY}${id}/`);
return res;
};
const countryCreate = async (body: { name: string; name_ru: string }) => {
const res = await httpClient.post(`${COUNTRY}`, body);
return res;
};
const countryUpdate = async ({
body,
id,
}: {
id: number;
body: { name: string; name_ru: string };
}) => {
const res = await httpClient.patch(`${COUNTRY}${id}/`, body);
return res;
};
const countryDelete = async (id: number) => {
const res = await httpClient.delete(`${COUNTRY}${id}/`);
return res;
};
const regionList = async (params: {
page: number;
name?: string;
country: number;
page_size: number;
}): Promise<AxiosResponse<CountryList>> => {
const res = await httpClient.get(REGION, { params });
return res;
};
const regionDeatil = async (
id: number,
): Promise<AxiosResponse<CountryDeatil>> => {
const res = await httpClient.get(`${REGION}${id}/`);
return res;
};
const regionCreate = async (body: {
name: string;
name_ru: string;
country: number;
}) => {
const res = await httpClient.post(`${REGION}`, body);
return res;
};
const regionUpdate = async ({
body,
id,
}: {
id: number;
body: { name: string; name_ru: string; country: number };
}) => {
const res = await httpClient.patch(`${REGION}${id}/`, body);
return res;
};
const hotelMealList = async (params: {
page: number;
page_size: number;
name?: string;
}): Promise<AxiosResponse<HotelMealList>> => {
const res = await httpClient.get(`${MEAL_PLAN}`, { params });
return res;
};
const hotelMealDetail = async (
id: number,
): Promise<AxiosResponse<HotelMealdetail>> => {
const res = await httpClient.get(`${MEAL_PLAN}${id}/`);
return res;
};
const hotelMealDelete = async (id: number) => {
const res = await httpClient.delete(`${MEAL_PLAN}${id}/`);
return res;
};
const hotelMealCreate = async (body: { name: string; name_ru: string }) => {
const res = await httpClient.post(`${MEAL_PLAN}`, body);
return res;
};
const hotelMealUpdate = async ({
body,
id,
}: {
id: number;
body: { name: string; name_ru: string };
}) => {
const res = await httpClient.patch(`${MEAL_PLAN}${id}/`, body);
return res;
};
export { export {
addedPopularTours, addedPopularTours,
amenitiesCreate, amenitiesCreate,
amenitiesUpdate, amenitiesUpdate,
countryCreate,
countryDeatil,
countryDelete,
countryList,
countryUpdate,
createHotel, createHotel,
createTours, createTours,
deleteAmenities, deleteAmenities,
@@ -530,6 +651,11 @@ export {
hotelFeatureTypeDetail, hotelFeatureTypeDetail,
hotelFeatureTypeUpdate, hotelFeatureTypeUpdate,
hotelFeatureUpdate, hotelFeatureUpdate,
hotelMealCreate,
hotelMealDelete,
hotelMealDetail,
hotelMealList,
hotelMealUpdate,
hotelTarfiDetail, hotelTarfiDetail,
hotelTarif, hotelTarif,
hotelTarifCreate, hotelTarifCreate,
@@ -545,5 +671,9 @@ export {
hotelTypeDelete, hotelTypeDelete,
hotelTypeDetail, hotelTypeDetail,
hotelTypeUpdate, hotelTypeUpdate,
regionCreate,
regionDeatil,
regionList,
regionUpdate,
updateTours, updateTours,
}; };

View File

@@ -3,8 +3,10 @@
import type { import type {
AllAmenitiesDataRes, AllAmenitiesDataRes,
Badge, Badge,
CountryLisResult,
HotelFeatures, HotelFeatures,
HotelFeaturesType, HotelFeaturesType,
HotelMealListData,
Tarif, Tarif,
Transport, Transport,
Type, Type,
@@ -364,3 +366,111 @@ export const AmenitiesColumns = (
}, },
}, },
]; ];
export const CountryColumns = (
onEdit: (id: number) => void,
onDelete: (id: number) => void,
t: (key: string) => string,
setActiveTab?: Dispatch<SetStateAction<string>>,
setFeatureId?: Dispatch<SetStateAction<number | null>>,
): ColumnDef<CountryLisResult>[] => [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => <span>{row.original.id}</span>,
},
{
accessorKey: "name",
header: t("Nomi"),
cell: ({ row }) => <span>{row.original.name_uz}</span>,
},
{
accessorKey: "name",
header: () => <div>{t("Nomi")} (ru)</div>,
cell: ({ row }) => <span>{row.original.name_ru}</span>,
},
{
id: "actions",
header: () => <div className="text-right">{t("Harakatlar")}</div>,
cell: ({ row }) => {
const { t } = useTranslation();
return (
<div className="flex justify-end gap-2">
{setActiveTab && setFeatureId && (
<Button
size="sm"
variant="outline"
onClick={() => {
setActiveTab("region");
setFeatureId(row.original.id);
}}
>
{t("Ko'rish")}
</Button>
)}
<Button
size="sm"
variant="outline"
onClick={() => onEdit(row.original.id)}
>
{t("Tahrirlash")}
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => onDelete(row.original.id)}
>
{t("O'chirish")}
</Button>
</div>
);
},
},
];
export const MealColumns = (
onEdit: (id: number) => void,
onDelete: (id: number) => void,
t: (key: string) => string,
): ColumnDef<HotelMealListData>[] => [
{
accessorKey: "id",
header: "ID",
cell: ({ row }) => <span>{row.original.id}</span>,
},
{
accessorKey: "name",
header: t("Nomi"),
cell: ({ row }) => <span>{row.original.name_uz}</span>,
},
{
accessorKey: "name",
header: () => <div>{t("Nomi")} (ru)</div>,
cell: ({ row }) => <span>{row.original.name_ru}</span>,
},
{
id: "actions",
header: () => <div className="text-right">{t("Harakatlar")}</div>,
cell: ({ row }) => {
const { t } = useTranslation();
return (
<div className="flex justify-end gap-2">
<Button
size="sm"
variant="outline"
onClick={() => onEdit(row.original.id)}
>
{t("Tahrirlash")}
</Button>
<Button
size="sm"
variant="destructive"
onClick={() => onDelete(row.original.id)}
>
{t("O'chirish")}
</Button>
</div>
);
},
},
];

View File

@@ -23,18 +23,18 @@ export const TourformSchema = z.object({
max_person: z.string().min(1, { max_person: z.string().min(1, {
message: "Kamida 1 yo'lovchi bo'lishi kerak.", message: "Kamida 1 yo'lovchi bo'lishi kerak.",
}), }),
departure: z.string().min(2, { departure: z.string().min(1, {
message: "Ketish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.", message: "Ketish joyi eng kamida 1 ta belgidan iborat bo'lishi kerak.",
}), }),
departure_ru: z.string().min(2, { // departure_ru: z.string().min(2, {
message: "Ketish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.", // message: "Ketish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
}), // }),
destination: z.string().min(2, { destination: z.string().min(1, {
message: "Borish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.", message: "Borish joyi eng kamida 1 ta belgidan iborat bo'lishi kerak.",
}),
destination_ru: z.string().min(2, {
message: "Borish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
}), }),
// destination_ru: z.string().min(2, {
// message: "Borish joyi eng kamida 2 ta belgidan iborat bo'lishi kerak.",
// }),
location_name: z.string().min(2, { location_name: z.string().min(2, {
message: "Eng kamida 2 ta belgidan iborat bo'lishi kerak.", message: "Eng kamida 2 ta belgidan iborat bo'lishi kerak.",
}), }),

View File

@@ -11,7 +11,18 @@ export interface GetAllTours {
current_page: number; current_page: number;
results: { results: {
id: number; id: number;
destination: string; destination:
| {
id: number;
name: string;
name_ru: string;
country: {
id: number;
name: string;
name_ru: string;
};
}
| string;
featured_tickets: boolean; featured_tickets: boolean;
duration_days: number; duration_days: number;
hotel_name: string; hotel_name: string;
@@ -35,12 +46,26 @@ export interface GetOneTours {
price: number; price: number;
min_person: number; min_person: number;
max_person: number; max_person: number;
departure: string; departure: {
departure_ru: string; id: number;
departure_uz: string; name: string;
destination: string; name_ru: string;
destination_ru: string; country: {
destination_uz: string; id: number;
name: string;
name_ru: string;
};
};
destination: {
id: number;
name: string;
name_ru: string;
country: {
id: number;
name: string;
name_ru: string;
};
};
departure_time: string; departure_time: string;
travel_time: string; travel_time: string;
location_name: string; location_name: string;
@@ -151,10 +176,10 @@ export interface CreateTourRes {
price: number; price: number;
min_person: number; min_person: number;
max_person: number; max_person: number;
departure: string; departure: number;
departure_ru: string; // departure_ru: string;
destination: string; destination: number;
destination_ru: string; // destination_ru: string;
departure_time: string; departure_time: string;
travel_time: string; travel_time: string;
location_name: string; location_name: string;
@@ -349,6 +374,33 @@ export interface HotelFeaturesTypeDetail {
}; };
} }
export interface HotelMealList {
status: boolean;
data: {
links: {
previous: null | string;
next: null | string;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: HotelMealListData[];
};
}
export interface HotelMealdetail {
status: boolean;
data: HotelMealListData;
}
export interface HotelMealListData {
id: number;
name: string;
name_ru: string;
name_uz: string;
}
export interface GetDetailTours { export interface GetDetailTours {
status: boolean; status: boolean;
data: { data: {
@@ -363,12 +415,26 @@ export interface GetDetailTours {
price: number; price: number;
min_person: number; min_person: number;
max_person: number; max_person: number;
departure: string; departure: {
departure_ru: string; id: number;
departure_uz: string; name: string;
destination: string; name_ru: string;
destination_ru: string; country: {
destination_uz: string; id: number;
name: string;
name_ru: string;
};
};
destination: {
id: number;
name: string;
name_ru: string;
country: {
id: number;
name: string;
name_ru: string;
};
};
departure_time: string; departure_time: string;
travel_time: string; travel_time: string;
location_name: string; location_name: string;
@@ -492,7 +558,7 @@ export interface GetHotelRes {
id: number; id: number;
name: string; name: string;
rating: number; rating: number;
meal_plan: string; meal_plan: number;
ticket: number; ticket: number;
hotel_type: [ hotel_type: [
{ {
@@ -560,3 +626,35 @@ export interface DetailAmenitiesData {
icon_name: string; icon_name: string;
}; };
} }
export interface CountryList {
status: boolean;
data: {
links: {
previous: null | string;
next: null | string;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: CountryLisResult[];
};
}
export interface CountryLisResult {
id: number;
name: string;
name_ru: string;
name_uz: string;
}
export interface CountryDeatil {
status: boolean;
data: {
id: number;
name: string;
name_ru: string;
name_uz: string;
};
}

View File

@@ -0,0 +1,340 @@
import {
countryCreate,
countryDeatil,
countryDelete,
countryUpdate,
} from "@/pages/tours/lib/api";
import { CountryColumns } from "@/pages/tours/lib/column";
import type { CountryLisResult } from "@/pages/tours/lib/type";
import { Button } from "@/shared/ui/button";
import { Dialog, DialogContent } from "@/shared/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import RealPagination from "@/widgets/real-pagination/ui/RealPagination";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Loader, PlusIcon } from "lucide-react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import z from "zod";
const formSchema = z.object({
name: z.string().min(1, { message: "Majburiy maydon" }),
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
});
const CountryTable = ({
data,
page,
pageSize,
setActiveTab,
setFeatureId,
}: {
page: number;
pageSize: number;
setActiveTab: Dispatch<SetStateAction<string>>;
setFeatureId: Dispatch<SetStateAction<number | null>>;
data:
| {
links: {
previous: string | null;
next: string | null;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: CountryLisResult[];
}
| undefined;
}) => {
const { t } = useTranslation();
const [open, setOpen] = useState<boolean>(false);
const [editId, setEditId] = useState<number | null>(null);
const queryClient = useQueryClient();
const [types, setTypes] = useState<"edit" | "create">("create");
const handleEdit = (id: number) => {
setTypes("edit");
setOpen(true);
setEditId(id);
};
const { data: badgeDetail } = useQuery({
queryKey: ["detail_country", editId],
queryFn: () => countryDeatil(editId!),
enabled: !!editId,
});
const { mutate: deleteMutate } = useMutation({
mutationFn: (id: number) => countryDelete(id),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_country"] });
queryClient.refetchQueries({ queryKey: ["detail_country"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const handleDelete = (id: number) => {
deleteMutate(id);
};
const columns = CountryColumns(
handleEdit,
handleDelete,
t,
setActiveTab,
setFeatureId,
);
const { mutate: create, isPending } = useMutation({
mutationFn: (body: { name: string; name_ru: string }) =>
countryCreate(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_country"] });
queryClient.refetchQueries({ queryKey: ["detail_country"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const { mutate: update, isPending: updatePending } = useMutation({
mutationFn: ({
body,
id,
}: {
id: number;
body: {
name: string;
name_ru: string;
};
}) => countryUpdate({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["all_country"] });
queryClient.refetchQueries({ queryKey: ["detail_country"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
name_ru: "",
},
});
useEffect(() => {
if (badgeDetail) {
form.setValue("name", badgeDetail.data.data.name_uz);
form.setValue("name_ru", badgeDetail.data.data.name_ru);
}
}, [editId, badgeDetail]);
function onSubmit(values: z.infer<typeof formSchema>) {
if (types === "create") {
create({
name: values.name,
name_ru: values.name_ru,
});
} else if (types === "edit" && editId) {
update({
id: editId,
body: {
name: values.name,
name_ru: values.name_ru,
},
});
}
}
const table = useReactTable({
data: data?.results ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: data?.total_pages ?? 0,
state: {
pagination: {
pageIndex: page - 1,
pageSize: pageSize,
},
},
});
return (
<>
<div className="flex justify-end">
<Button
variant="default"
onClick={() => {
setOpen(true);
setTypes("create");
form.reset();
}}
>
<PlusIcon className="mr-2" />
{t("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination
table={table}
totalPages={data?.total_pages}
namePage="pageCountry"
namePageSize="pageSizeCountry"
/>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<p className="text-xl">
{types === "create" ? t("Saqlash") : 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")}</FormLabel>
<FormControl>
<Input placeholder={t("Nomi")} {...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>
)}
/>
<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">
{isPending || updatePending ? (
<Loader className="animate-spin" />
) : types === "create" ? (
t("Saqlash")
) : (
t("Tahrirlash")
)}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</>
);
};
export default CountryTable;

View File

@@ -0,0 +1,324 @@
import {
hotelTypeCreate,
hotelTypeDelete,
hotelTypeDetail,
hotelTypeUpdate,
} from "@/pages/tours/lib/api";
import { TypeColumns } from "@/pages/tours/lib/column";
import type { Type } from "@/pages/tours/lib/type";
import { Button } from "@/shared/ui/button";
import { Dialog, DialogContent } from "@/shared/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import RealPagination from "@/widgets/real-pagination/ui/RealPagination";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Loader, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import z from "zod";
const formSchema = z.object({
name: z.string().min(1, { message: "Majburiy maydon" }),
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
});
const HotelType = ({
data,
page,
pageSize,
}: {
page: number;
pageSize: number;
data:
| {
links: {
previous: string;
next: string;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: Type[];
}
| undefined;
}) => {
const { t } = useTranslation();
const [open, setOpen] = useState<boolean>(false);
const [editId, setEditId] = useState<number | null>(null);
const queryClient = useQueryClient();
const [types, setTypes] = useState<"edit" | "create">("create");
const handleEdit = (id: number) => {
setTypes("edit");
setOpen(true);
setEditId(id);
};
const { data: typeDetail } = useQuery({
queryKey: ["detail_type", editId],
queryFn: () => hotelTypeDetail({ id: editId! }),
enabled: !!editId,
});
const { mutate: deleteMutate } = useMutation({
mutationFn: ({ id }: { id: number }) => hotelTypeDelete({ id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["detail_type"] });
queryClient.refetchQueries({ queryKey: ["all_type"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const handleDelete = (id: number) => {
deleteMutate({ id });
};
const columns = TypeColumns(handleEdit, handleDelete, t);
const { mutate: create, isPending } = useMutation({
mutationFn: ({ body }: { body: { name: string; name_ru: string } }) =>
hotelTypeCreate({ body }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["detail_type"] });
queryClient.refetchQueries({ queryKey: ["all_type"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const { mutate: update, isPending: updatePending } = useMutation({
mutationFn: ({
body,
id,
}: {
id: number;
body: { name: string; name_ru: string };
}) => hotelTypeUpdate({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["detail_type"] });
queryClient.refetchQueries({ queryKey: ["all_type"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
name_ru: "",
},
});
useEffect(() => {
if (typeDetail) {
form.setValue("name", typeDetail.data.data.name_uz);
form.setValue("name_ru", typeDetail.data.data.name_ru);
}
}, [editId, typeDetail]);
function onSubmit(values: z.infer<typeof formSchema>) {
if (types === "create") {
create({
body: {
name: values.name,
name_ru: values.name_ru,
},
});
} else if (types === "edit" && editId) {
update({
id: editId,
body: {
name: values.name,
name_ru: values.name_ru,
},
});
}
}
const table = useReactTable({
data: data?.results ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: data?.total_pages ?? 0,
state: {
pagination: {
pageIndex: page - 1,
pageSize: pageSize,
},
},
});
return (
<>
<div className="flex justify-end">
<Button
variant="default"
onClick={() => {
setOpen(true);
setTypes("create");
form.reset();
}}
>
<PlusIcon className="mr-2" />
{t("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination table={table} totalPages={data?.total_pages} />
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<p className="text-xl">
{types === "create" ? t("Saqlash") : 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")}</FormLabel>
<FormControl>
<Input placeholder={t("Nomi")} {...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>
)}
/>
<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">
{isPending || updatePending ? (
<Loader className="animate-spin" />
) : types === "create" ? (
t("Saqlash")
) : (
t("Tahrirlash")
)}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</>
);
};
export default HotelType;

View File

@@ -1,11 +1,11 @@
import { import {
hotelTypeCreate, hotelMealCreate,
hotelTypeDelete, hotelMealDelete,
hotelTypeDetail, hotelMealDetail,
hotelTypeUpdate, hotelMealUpdate,
} from "@/pages/tours/lib/api"; } from "@/pages/tours/lib/api";
import { TypeColumns } from "@/pages/tours/lib/column"; import { MealColumns } from "@/pages/tours/lib/column";
import type { Type } from "@/pages/tours/lib/type"; import type { HotelMealListData } from "@/pages/tours/lib/type";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { Dialog, DialogContent } from "@/shared/ui/dialog"; import { Dialog, DialogContent } from "@/shared/ui/dialog";
import { import {
@@ -45,7 +45,7 @@ const formSchema = z.object({
name_ru: z.string().min(1, { message: "Majburiy maydon" }), name_ru: z.string().min(1, { message: "Majburiy maydon" }),
}); });
const MealTable = ({ const MealPlan = ({
data, data,
page, page,
pageSize, pageSize,
@@ -55,14 +55,14 @@ const MealTable = ({
data: data:
| { | {
links: { links: {
previous: string; previous: string | null;
next: string; next: string | null;
}; };
total_items: number; total_items: number;
total_pages: number; total_pages: number;
page_size: number; page_size: number;
current_page: number; current_page: number;
results: Type[]; results: HotelMealListData[];
} }
| undefined; | undefined;
}) => { }) => {
@@ -78,17 +78,17 @@ const MealTable = ({
setEditId(id); setEditId(id);
}; };
const { data: typeDetail } = useQuery({ const { data: badgeDetail } = useQuery({
queryKey: ["detail_type", editId], queryKey: ["detail_meal", editId],
queryFn: () => hotelTypeDetail({ id: editId! }), queryFn: () => hotelMealDetail(editId!),
enabled: !!editId, enabled: !!editId,
}); });
const { mutate: deleteMutate } = useMutation({ const { mutate: deleteMutate } = useMutation({
mutationFn: ({ id }: { id: number }) => hotelTypeDelete({ id }), mutationFn: ({ id }: { id: number }) => hotelMealDelete(id),
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["detail_type"] }); queryClient.refetchQueries({ queryKey: ["detail_meal"] });
queryClient.refetchQueries({ queryKey: ["all_type"] }); queryClient.refetchQueries({ queryKey: ["all_meal"] });
setOpen(false); setOpen(false);
form.reset(); form.reset();
}, },
@@ -104,14 +104,20 @@ const MealTable = ({
deleteMutate({ id }); deleteMutate({ id });
}; };
const columns = TypeColumns(handleEdit, handleDelete, t); const columns = MealColumns(handleEdit, handleDelete, t);
const { mutate: create, isPending } = useMutation({ const { mutate: create, isPending } = useMutation({
mutationFn: ({ body }: { body: { name: string; name_ru: string } }) => mutationFn: ({
hotelTypeCreate({ body }), body,
}: {
body: {
name: string;
name_ru: string;
};
}) => hotelMealCreate(body),
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["detail_type"] }); queryClient.refetchQueries({ queryKey: ["detail_meal"] });
queryClient.refetchQueries({ queryKey: ["all_type"] }); queryClient.refetchQueries({ queryKey: ["all_meal"] });
setOpen(false); setOpen(false);
form.reset(); form.reset();
}, },
@@ -129,11 +135,14 @@ const MealTable = ({
id, id,
}: { }: {
id: number; id: number;
body: { name: string; name_ru: string }; body: {
}) => hotelTypeUpdate({ body, id }), name: string;
name_ru: string;
};
}) => hotelMealUpdate({ body, id }),
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["detail_type"] }); queryClient.refetchQueries({ queryKey: ["detail_meal"] });
queryClient.refetchQueries({ queryKey: ["all_type"] }); queryClient.refetchQueries({ queryKey: ["all_meal"] });
setOpen(false); setOpen(false);
form.reset(); form.reset();
}, },
@@ -154,11 +163,11 @@ const MealTable = ({
}); });
useEffect(() => { useEffect(() => {
if (typeDetail) { if (badgeDetail) {
form.setValue("name", typeDetail.data.data.name_uz); form.setValue("name", badgeDetail.data.data.name_uz);
form.setValue("name_ru", typeDetail.data.data.name_ru); form.setValue("name_ru", badgeDetail.data.data.name_ru);
} }
}, [editId, typeDetail]); }, [editId, badgeDetail]);
function onSubmit(values: z.infer<typeof formSchema>) { function onSubmit(values: z.infer<typeof formSchema>) {
if (types === "create") { if (types === "create") {
@@ -192,7 +201,6 @@ const MealTable = ({
}, },
}, },
}); });
return ( return (
<> <>
<div className="flex justify-end"> <div className="flex justify-end">
@@ -256,7 +264,12 @@ const MealTable = ({
</Table> </Table>
</div> </div>
<RealPagination table={table} totalPages={data?.total_pages} /> <RealPagination
table={table}
totalPages={data?.total_pages}
namePage="pageMeal"
namePageSize="pageSizeMeal"
/>
<Dialog open={open} onOpenChange={setOpen}> <Dialog open={open} onOpenChange={setOpen}>
<DialogContent> <DialogContent>
@@ -321,4 +334,4 @@ const MealTable = ({
); );
}; };
export default MealTable; export default MealPlan;

View File

@@ -0,0 +1,335 @@
import {
hotelFeatureTypeDelete,
regionCreate,
regionDeatil,
regionUpdate,
} from "@/pages/tours/lib/api";
import { CountryColumns } from "@/pages/tours/lib/column";
import type { CountryLisResult } from "@/pages/tours/lib/type";
import { Button } from "@/shared/ui/button";
import { Dialog, DialogContent } from "@/shared/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import RealPagination from "@/widgets/real-pagination/ui/RealPagination";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Loader, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import z from "zod";
const formSchema = z.object({
name: z.string().min(1, { message: "Majburiy maydon" }),
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
});
const RegionTable = ({
data,
page,
pageSize,
featureId,
}: {
page: number;
featureId: number | null;
pageSize: number;
data:
| {
links: {
previous: string | null;
next: string | null;
};
total_items: number;
total_pages: number;
page_size: number;
current_page: number;
results: CountryLisResult[];
}
| undefined;
}) => {
const { t } = useTranslation();
const [open, setOpen] = useState<boolean>(false);
const [editId, setEditId] = useState<number | null>(null);
const queryClient = useQueryClient();
const [types, setTypes] = useState<"edit" | "create">("create");
const handleEdit = (id: number) => {
setTypes("edit");
setOpen(true);
setEditId(id);
};
const { data: badgeDetail } = useQuery({
queryKey: ["region_detail", editId],
queryFn: () => regionDeatil(editId!),
enabled: !!editId,
});
const { mutate: deleteMutate } = useMutation({
mutationFn: ({ id }: { id: number }) => hotelFeatureTypeDelete({ id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["region_detail"] });
queryClient.refetchQueries({ queryKey: ["all_region"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const handleDelete = (id: number) => {
deleteMutate({ id });
};
const columns = CountryColumns(handleEdit, handleDelete, t);
const { mutate: create, isPending } = useMutation({
mutationFn: (body: { name: string; name_ru: string; country: number }) =>
regionCreate(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["region_detail"] });
queryClient.refetchQueries({ queryKey: ["all_region"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const { mutate: update, isPending: updatePending } = useMutation({
mutationFn: ({
body,
id,
}: {
id: number;
body: {
name: string;
name_ru: string;
country: number;
};
}) => regionUpdate({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["region_detail"] });
queryClient.refetchQueries({ queryKey: ["all_region"] });
setOpen(false);
form.reset();
},
onError: () => {
toast.error(t("Xatolik yuz berdi"), {
position: "top-center",
richColors: true,
});
},
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
name: "",
name_ru: "",
},
});
useEffect(() => {
if (badgeDetail) {
form.setValue("name", badgeDetail.data.data.name_uz);
form.setValue("name_ru", badgeDetail.data.data.name_ru);
}
}, [editId, badgeDetail]);
function onSubmit(values: z.infer<typeof formSchema>) {
if (types === "create") {
create({
country: Number(featureId),
name: values.name,
name_ru: values.name_ru,
});
} else if (types === "edit" && editId) {
update({
id: editId,
body: {
country: Number(featureId),
name: values.name,
name_ru: values.name_ru,
},
});
}
}
const table = useReactTable({
data: data?.results ?? [],
columns,
getCoreRowModel: getCoreRowModel(),
manualPagination: true,
pageCount: data?.total_pages ?? 0,
state: {
pagination: {
pageIndex: page - 1,
pageSize: pageSize,
},
},
});
return (
<>
<div className="flex justify-end">
<Button
variant="default"
onClick={() => {
setOpen(true);
setTypes("create");
form.reset();
}}
>
<PlusIcon className="mr-2" />
{t("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination
table={table}
totalPages={data?.total_pages}
namePage="pageRegion"
namePageSize="pageSizeRegion"
/>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<p className="text-xl">
{types === "create" ? t("Saqlash") : 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")}</FormLabel>
<FormControl>
<Input placeholder={t("Nomi")} {...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>
)}
/>
<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">
{isPending || updatePending ? (
<Loader className="animate-spin" />
) : types === "create" ? (
t("Saqlash")
) : (
t("Tahrirlash")
)}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</>
);
};
export default RegionTable;

View File

@@ -1,10 +1,12 @@
"use client"; "use client";
import { import {
countryList,
createTours, createTours,
getAllAmenities, getAllAmenities,
hotelBadge, hotelBadge,
hotelTransport, hotelTransport,
regionList,
updateTours, updateTours,
} from "@/pages/tours/lib/api"; } from "@/pages/tours/lib/api";
import { TourformSchema } from "@/pages/tours/lib/form"; import { TourformSchema } from "@/pages/tours/lib/form";
@@ -12,12 +14,14 @@ import { useTicketStore } from "@/pages/tours/lib/store";
import type { GetOneTours } from "@/pages/tours/lib/type"; import type { GetOneTours } from "@/pages/tours/lib/type";
import TicketsImagesModel from "@/pages/tours/ui/TicketsImagesModel"; import TicketsImagesModel from "@/pages/tours/ui/TicketsImagesModel";
import formatPrice from "@/shared/lib/formatPrice"; import formatPrice from "@/shared/lib/formatPrice";
import { cn } from "@/shared/lib/utils";
import { Badge } from "@/shared/ui/badge"; import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { Calendar } from "@/shared/ui/calendar"; import { Calendar } from "@/shared/ui/calendar";
import { import {
Command, Command,
CommandGroup, CommandGroup,
CommandInput,
CommandItem, CommandItem,
CommandList, CommandList,
} from "@/shared/ui/command"; } from "@/shared/ui/command";
@@ -35,7 +39,16 @@ import { RadioGroup, RadioGroupItem } from "@/shared/ui/radio-group";
import { Textarea } from "@/shared/ui/textarea"; import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ChevronDownIcon, Loader2, SquareCheckBig, XIcon } from "lucide-react"; import { AnimatePresence, motion } from "framer-motion";
import {
Check,
ChevronDownIcon,
ChevronsUpDown,
Loader2,
MoveLeft,
SquareCheckBig,
XIcon,
} from "lucide-react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -72,9 +85,9 @@ const StepOne = ({
departure: "", departure: "",
// tarif: [], // tarif: [],
transport: [], transport: [],
departure_ru: "", // departure_ru: "",
destination: "", destination: "",
destination_ru: "", // destination_ru: "",
location_name: "", location_name: "",
location_name_ru: "", location_name_ru: "",
departureDateTime: { departureDateTime: {
@@ -151,10 +164,10 @@ const StepOne = ({
passenger_count: String(tour.passenger_count) ?? "1", passenger_count: String(tour.passenger_count) ?? "1",
min_person: String(tour.min_person) ?? "1", min_person: String(tour.min_person) ?? "1",
max_person: String(tour.max_person) ?? "1", max_person: String(tour.max_person) ?? "1",
departure: tour.departure_uz ?? "", departure: tour.departure?.id.toString() ?? "",
departure_ru: tour.departure_ru ?? "", // departure_ru: tour.departure_ru ?? "",
destination: tour.destination_uz ?? "", destination: tour.destination?.id.toString() ?? "",
destination_ru: tour.destination_ru ?? "", // destination_ru: tour.destination_ru ?? "",
location_name: tour.location_name_uz ?? "", location_name: tour.location_name_uz ?? "",
location_name_ru: tour.location_name_ru ?? "", location_name_ru: tour.location_name_ru ?? "",
hotel_info: tour.hotel_info_uz ?? "", hotel_info: tour.hotel_info_uz ?? "",
@@ -227,6 +240,10 @@ const StepOne = ({
// TicketStore uchun id // TicketStore uchun id
setId(tour.id); setId(tour.id);
setSelectedCountry(data.data.departure?.country?.id);
setSearchCity(data.data.departure?.name);
setSelectedCountryDes(data.data.destination?.country.id);
setSearchCityDes(data.data.destination?.name);
}, [isEditMode, data, form, setId]); }, [isEditMode, data, form, setId]);
const { watch, setValue } = form; const { watch, setValue } = form;
@@ -289,9 +306,9 @@ const StepOne = ({
formData.append("min_person", String(value.min_person)); formData.append("min_person", String(value.min_person));
formData.append("max_person", String(value.max_person)); formData.append("max_person", String(value.max_person));
formData.append("departure", String(value.departure)); formData.append("departure", String(value.departure));
formData.append("departure_ru", String(value.departure_ru)); // formData.append("departure_ru", String(value.departure_ru));
formData.append("destination", String(value.destination)); formData.append("destination", String(value.destination));
formData.append("destination_ru", String(value.destination_ru)); // formData.append("destination_ru", String(value.destination_ru));
formData.append( formData.append(
"departure_time", "departure_time",
value.departureDateTime.date?.toISOString(), value.departureDateTime.date?.toISOString(),
@@ -488,11 +505,77 @@ const StepOne = ({
// queryFn: () => hotelTarif({ page: 1, page_size: 10 }), // queryFn: () => hotelTarif({ page: 1, page_size: 10 }),
// }); // });
const [searchCountry, setSearchCountry] = useState<string>("");
const [searchCity, setSearchCity] = useState<string>("");
const [openCountry, setOpenCountry] = useState<boolean>(false);
const [openCity, setOpenCity] = useState<boolean>(false);
const [selectedCountry, setSelectedCountry] = useState<number | null>(null);
const { data: countryData, isLoading: countryLoad } = useQuery({
queryKey: ["all_country", searchCountry],
queryFn: () => countryList({ page: 1, page_size: 10, name: searchCountry }),
select: (res) => res.data.data,
});
const { data: regionData, isLoading: regionLoad } = useQuery({
queryKey: ["all_region", selectedCountry, searchCity],
queryFn: () =>
regionList({
page: 1,
page_size: 10,
country: selectedCountry!,
name: searchCity,
}),
select: (res) => res.data.data,
enabled: !!selectedCountry,
});
//destions select country
const [searchCountryDes, setSearchCountryDes] = useState<string>("");
const [searchCityDes, setSearchCityDes] = useState<string>("");
const [openCountryDes, setOpenCountryDes] = useState<boolean>(false);
const [openCityDes, setOpenCityDes] = useState<boolean>(false);
const [selectedCountryDes, setSelectedCountryDes] = useState<number | null>(
null,
);
const { data: countryDataDes, isLoading: countryLoadDes } = useQuery({
queryKey: ["all_country_des", searchCountryDes],
queryFn: () =>
countryList({ page: 1, page_size: 10, name: searchCountryDes }),
select: (res) => res.data.data,
});
const { data: regionDataDes, isLoading: regionLoadDes } = useQuery({
queryKey: ["all_region_des", selectedCountryDes, searchCityDes],
queryFn: () =>
regionList({
page: 1,
page_size: 10,
country: selectedCountryDes!,
name: searchCityDes,
}),
select: (res) => res.data.data,
enabled: !!selectedCountryDes,
});
const handleCountrySelect = (id: number | null) => {
setSelectedCountry(id);
form.setValue("departure", "");
setOpenCountry(false);
};
const handleCountrySelectDes = (id: number | null) => {
setSelectedCountryDes(id);
form.setValue("destination", "");
setOpenCountryDes(false);
};
const { data: transport } = useQuery({ const { data: transport } = useQuery({
queryKey: ["all_transport"], queryKey: ["all_transport"],
queryFn: () => hotelTransport({ page: 1, page_size: 10 }), queryFn: () => hotelTransport({ page: 1, page_size: 10 }),
}); });
console.log(form.formState.errors);
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
@@ -620,24 +703,224 @@ const StepOne = ({
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-start"> <div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-start">
<FormField <FormField
control={form.control}
name="departure" name="departure"
render={({ field }) => ( control={form.control}
<FormItem> render={({ field }) => {
<Label className="text-md">{t("Ketish joyi")}</Label> const selectedCity = regionData?.results.find(
<FormControl> (u) => String(u.id) === field.value,
<Input );
placeholder="Toshkent"
{...field} return (
className="h-12 !text-md" <FormItem className="flex flex-col gap-2">
/> <Label className="text-md">Ketish joyi</Label>
</FormControl> {!selectedCountry && (
<FormMessage /> <Popover open={openCountry} onOpenChange={setOpenCountry}>
</FormItem> <PopoverTrigger asChild>
)} <FormControl>
<Button
variant="outline"
className={cn(
"w-full h-12 justify-between transition-all duration-200",
!selectedCountry && "text-muted-foreground",
)}
>
{selectedCountry
? countryData?.results.find(
(c) => c.id === selectedCountry,
)?.name
: "Davlatni tanlang"}
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<AnimatePresence mode="wait">
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<PopoverContent className="!w-96 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
value={searchCountry}
onValueChange={setSearchCountry}
/>
<CommandList>
{countryLoad ? (
<div className="py-6 text-center">
<Loader2 className="animate-spin mx-auto" />
</div>
) : (
<CommandGroup>
{countryData?.results.map((c, index) => (
<motion.div
key={c.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.2,
delay: index * 0.03,
ease: "easeOut",
}}
>
<CommandItem
onSelect={() => {
handleCountrySelect(c.id);
setOpenCountry(false);
setTimeout(
() => setOpenCity(true),
200,
);
}}
className="cursor-pointer transition-colors"
>
<Check
className={cn(
"mr-2 h-4 transition-opacity duration-200",
selectedCountry === c.id
? "opacity-100"
: "opacity-0",
)}
/>
{c.name}
</CommandItem>
</motion.div>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</motion.div>
</AnimatePresence>
</Popover>
)}
{/* CITY DROPDOWN */}
{selectedCountry && (
<Popover open={openCity} onOpenChange={setOpenCity}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full h-12 justify-between transition-all duration-200",
!field.value && "text-muted-foreground",
)}
>
{selectedCity
? selectedCity.name
: "Shaharni tanlang"}
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, height: 0, y: -10 }}
animate={{
opacity: 1,
height: "auto",
y: 0,
transition: {
height: { duration: 0.3 },
opacity: { duration: 0.3, delay: 0.1 },
y: { duration: 0.3 },
},
}}
exit={{
opacity: 0,
height: 0,
y: -10,
transition: {
height: { duration: 0.25 },
opacity: { duration: 0.15 },
},
}}
style={{ overflow: "hidden" }}
>
<PopoverContent className="!w-96 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
value={searchCity}
onValueChange={setSearchCity}
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
>
<Button
className="w-fit ml-2 p-1 cursor-pointer hover:bg-accent transition-colors"
size={"icon"}
variant={"ghost"}
onClick={() => {
handleCountrySelect(null);
setOpenCity(false);
setTimeout(() => setOpenCountry(true), 200);
}}
>
<MoveLeft className="mr-1" />
<p>Orqaga</p>
</Button>
</motion.div>
<CommandList>
{regionLoad ? (
<div className="py-6 text-center">
<Loader2 className="animate-spin mx-auto" />
</div>
) : (
<CommandGroup>
{regionData?.results.map((r, index) => (
<motion.div
key={r.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.2,
delay: index * 0.03,
ease: "easeOut",
}}
>
<CommandItem
onSelect={() => {
field.onChange(String(r.id));
setOpenCity(false);
}}
className="cursor-pointer transition-colors"
>
<Check
className={cn(
"mr-2 h-4 transition-opacity duration-200",
field.value === String(r.id)
? "opacity-100"
: "opacity-0",
)}
/>
{r.name}
</CommandItem>
</motion.div>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</motion.div>
</AnimatePresence>
</Popover>
)}
<FormMessage />
</FormItem>
);
}}
/> />
<FormField {/* <FormField
control={form.control} control={form.control}
name="departure_ru" name="departure_ru"
render={({ field }) => ( render={({ field }) => (
@@ -653,27 +936,234 @@ const StepOne = ({
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> /> */}
<FormField <FormField
control={form.control}
name="destination" name="destination"
render={({ field }) => ( control={form.control}
<FormItem> render={({ field }) => {
<Label className="text-md">{t("Borish joyi")}</Label> const selectedCity = regionDataDes?.results.find(
<FormControl> (u) => String(u.id) === field.value,
<Input );
placeholder="Dubai"
{...field} return (
className="h-12 !text-md" <FormItem className="flex flex-col gap-2">
/> <Label className="text-md">Borish joyi</Label>
</FormControl>
<FormMessage /> {/* COUNTRY DROPDOWN */}
</FormItem> {!selectedCountryDes && (
)} <Popover
open={openCountryDes}
onOpenChange={setOpenCountryDes}
>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full h-12 justify-between transition-all duration-200",
!selectedCountryDes && "text-muted-foreground",
)}
>
{selectedCountryDes
? countryDataDes?.results.find(
(c) => c.id === selectedCountryDes,
)?.name
: "Davlatni tanlang"}
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<AnimatePresence mode="wait">
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3, ease: "easeOut" }}
>
<PopoverContent className="!w-96 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
value={searchCountryDes}
onValueChange={setSearchCountryDes}
/>
<CommandList>
{countryLoadDes ? (
<div className="py-6 text-center">
<Loader2 className="animate-spin mx-auto" />
</div>
) : (
<CommandGroup>
{countryDataDes?.results.map((c, index) => (
<motion.div
key={c.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.2,
delay: index * 0.03,
ease: "easeOut",
}}
>
<CommandItem
onSelect={() => {
handleCountrySelectDes(c.id);
setOpenCountryDes(false);
setTimeout(
() => setOpenCityDes(true),
200,
);
}}
className="cursor-pointer transition-colors"
>
<Check
className={cn(
"mr-2 h-4 transition-opacity duration-200",
selectedCountryDes === c.id
? "opacity-100"
: "opacity-0",
)}
/>
{c.name}
</CommandItem>
</motion.div>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</motion.div>
</AnimatePresence>
</Popover>
)}
{selectedCountryDes && (
<Popover open={openCityDes} onOpenChange={setOpenCityDes}>
<PopoverTrigger asChild>
<FormControl>
<Button
variant="outline"
className={cn(
"w-full h-12 justify-between transition-all duration-200",
!field.value && "text-muted-foreground",
)}
>
{selectedCity
? selectedCity.name
: "Shaharni tanlang"}
<ChevronsUpDown className="ml-2 h-4 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<AnimatePresence>
<motion.div
initial={{ opacity: 0, height: 0, y: -10 }}
animate={{
opacity: 1,
height: "auto",
y: 0,
transition: {
height: { duration: 0.3 },
opacity: { duration: 0.3, delay: 0.1 },
y: { duration: 0.3 },
},
}}
exit={{
opacity: 0,
height: 0,
y: -10,
transition: {
height: { duration: 0.25 },
opacity: { duration: 0.15 },
},
}}
style={{ overflow: "hidden" }}
>
<PopoverContent className="!w-96 p-0">
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
value={searchCityDes}
onValueChange={setSearchCityDes}
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.1 }}
>
<Button
className="w-fit ml-2 p-1 cursor-pointer hover:bg-accent transition-colors"
size={"icon"}
variant={"ghost"}
onClick={() => {
handleCountrySelectDes(null);
setOpenCityDes(false);
setTimeout(
() => setOpenCountryDes(true),
200,
);
}}
>
<MoveLeft className="mr-1" />
<p>Orqaga</p>
</Button>
</motion.div>
<CommandList>
{regionLoadDes ? (
<div className="py-6 text-center">
<Loader2 className="animate-spin mx-auto" />
</div>
) : (
<CommandGroup>
{regionDataDes?.results.map((r, index) => (
<motion.div
key={r.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{
duration: 0.2,
delay: index * 0.03,
ease: "easeOut",
}}
>
<CommandItem
onSelect={() => {
field.onChange(String(r.id));
setOpenCityDes(false);
}}
className="cursor-pointer transition-colors"
>
<Check
className={cn(
"mr-2 h-4 transition-opacity duration-200",
field.value === String(r.id)
? "opacity-100"
: "opacity-0",
)}
/>
{r.name}
</CommandItem>
</motion.div>
))}
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</motion.div>
</AnimatePresence>
</Popover>
)}
<FormMessage />
</FormItem>
);
}}
/> />
<FormField {/* <FormField
control={form.control} control={form.control}
name="destination_ru" name="destination_ru"
render={({ field }) => ( render={({ field }) => (
@@ -689,7 +1179,7 @@ const StepOne = ({
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
)} )}
/> /> */}
<FormField <FormField
control={form.control} control={form.control}

View File

@@ -6,6 +6,7 @@ import {
getHotel, getHotel,
hotelFeature, hotelFeature,
hotelFeatureType, hotelFeatureType,
hotelMealList,
hotelType, hotelType,
} from "@/pages/tours/lib/api"; } from "@/pages/tours/lib/api";
import { useTicketStore } from "@/pages/tours/lib/store"; import { useTicketStore } from "@/pages/tours/lib/store";
@@ -15,6 +16,16 @@ import type {
HotelFeaturesType, HotelFeaturesType,
Type, Type,
} from "@/pages/tours/lib/type"; } from "@/pages/tours/lib/type";
import { cn } from "@/shared/lib/utils";
import { Button } from "@/shared/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/shared/ui/command";
import { import {
Form, Form,
FormControl, FormControl,
@@ -24,6 +35,7 @@ import {
} from "@/shared/ui/form"; } from "@/shared/ui/form";
import { Input } from "@/shared/ui/input"; import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label"; import { Label } from "@/shared/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { import {
Select, Select,
SelectContent, SelectContent,
@@ -33,7 +45,7 @@ import {
} from "@/shared/ui/select"; } from "@/shared/ui/select";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { X } from "lucide-react"; import { Check, ChevronsUpDown, Loader2, X } from "lucide-react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@@ -100,18 +112,7 @@ const StepTwo = ({
form.setValue("title", hotel.name); form.setValue("title", hotel.name);
form.setValue("rating", String(hotel.rating)); form.setValue("rating", String(hotel.rating));
const mealPlan = form.setValue("mealPlan", hotel.meal_plan.toString());
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( form.setValue(
"hotelType", "hotelType",
@@ -285,6 +286,15 @@ const StepTwo = ({
); );
}; };
const [openMeal, setOpenMeal] = useState<boolean>(false);
const [searchMeal, setSearchMeal] = useState<string>("");
const { data: mealData, isLoading: mealLoad } = useQuery({
queryKey: ["all_meal", searchMeal],
queryFn: () => hotelMealList({ page: 1, page_size: 99, name: searchMeal }),
select: (res) => res.data.data,
});
const removeFeatureType = (id: string) => const removeFeatureType = (id: string) =>
form.setValue( form.setValue(
"hotelFeaturesType", "hotelFeaturesType",
@@ -302,15 +312,7 @@ const StepTwo = ({
formData.append(`hotel_amenities[${i}]`, String(e)); formData.append(`hotel_amenities[${i}]`, String(e));
}); });
const mealPlan = formData.append("meal_plan", data.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 && data.hotelType &&
data.hotelType.forEach((id) => formData.append("hotel_type", id)); data.hotelType.forEach((id) => formData.append("hotel_type", id));
@@ -331,13 +333,6 @@ const StepTwo = ({
} }
}; };
const mealPlans = [
"Breakfast Only",
"Half Board",
"Full Board",
"All Inclusive",
];
return ( return (
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6"> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
@@ -400,28 +395,89 @@ const StepTwo = ({
{/* Meal Plan */} {/* Meal Plan */}
<FormField <FormField
control={form.control}
name="mealPlan" name="mealPlan"
render={({ field }) => ( control={form.control}
<FormItem> render={({ field }) => {
<Label>{t("Taom rejasi")}</Label> const selectedUser = mealData?.results.find(
<FormControl> (u) => String(u.id) === field.value,
<Select value={field.value} onValueChange={field.onChange}> );
<SelectTrigger className="!h-12 w-full"> return (
<SelectValue placeholder={t("Tanlang")} /> <FormItem className="flex flex-col">
</SelectTrigger> <Label className="text-md">Taom rejasi</Label>
<SelectContent>
{mealPlans.map((plan) => ( <Popover open={openMeal} onOpenChange={setOpenMeal}>
<SelectItem key={plan} value={plan}> <PopoverTrigger asChild>
{t(plan)} <FormControl>
</SelectItem> <Button
))} type="button"
</SelectContent> variant="outline"
</Select> role="combobox"
</FormControl> aria-expanded={openMeal}
<FormMessage /> className={cn(
</FormItem> "w-full h-12 justify-between",
)} !field.value && "text-muted-foreground",
)}
>
{selectedUser
? `${selectedUser.name}`
: "Taom rejasi tanlang"}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent
className="w-[--radix-popover-trigger-width] p-0"
align="start"
>
<Command shouldFilter={false}>
<CommandInput
placeholder="Qidirish..."
className="h-9"
value={searchMeal}
onValueChange={setSearchMeal}
/>
<CommandList>
{mealLoad ? (
<div className="py-6 text-center text-sm">
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
</div>
) : mealData && mealData.results.length > 0 ? (
<CommandGroup>
{mealData.results.map((u) => (
<CommandItem
key={u.id}
value={`${u.id}`}
onSelect={() => {
field.onChange(String(u.id));
setOpenMeal(false);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
field.value === String(u.id)
? "opacity-100"
: "opacity-0",
)}
/>
{u.name}
</CommandItem>
))}
</CommandGroup>
) : (
<CommandEmpty>Taom rejasi topilmadi</CommandEmpty>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
);
}}
/> />
{/* Hotel Type */} {/* Hotel Type */}

View File

@@ -221,7 +221,7 @@ export default function TourDetailPage() {
{t("Jo'nash joyi")} {t("Jo'nash joyi")}
</p> </p>
<p className="font-semibold text-white"> <p className="font-semibold text-white">
{tour.departure} {tour.departure?.name}
</p> </p>
</div> </div>
</div> </div>
@@ -233,7 +233,7 @@ export default function TourDetailPage() {
{t("Yo'nalish")} {t("Yo'nalish")}
</p> </p>
<p className="font-semibold text-white"> <p className="font-semibold text-white">
{tour.destination} {tour.destination?.name}
</p> </p>
</div> </div>
</div> </div>

View File

@@ -188,7 +188,9 @@ const Tours = ({ user }: { user: Role }) => {
<TableCell> <TableCell>
<div className="flex items-center gap-2 font-semibold"> <div className="flex items-center gap-2 font-semibold">
<Plane className="w-4 h-4 text-primary" /> <Plane className="w-4 h-4 text-primary" />
{tour.destination} {typeof tour.destination === "object"
? tour.destination?.name
: tour.destination}
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-sm text-primary font-medium"> <TableCell className="text-sm text-primary font-medium">
@@ -319,7 +321,9 @@ const Tours = ({ user }: { user: Role }) => {
<Plane className="w-4 h-4 text-primary flex-shrink-0" /> <Plane className="w-4 h-4 text-primary flex-shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="font-semibold truncate"> <p className="font-semibold truncate">
{tour.destination} {typeof tour.destination === "object"
? tour.destination?.name
: tour.destination}
</p> </p>
<p className="text-xs text-muted-foreground"> <p className="text-xs text-muted-foreground">
{tour.duration_days} kun {tour.hotel_name} {tour.duration_days} kun {tour.hotel_name}

View File

@@ -1,19 +1,25 @@
"use client"; "use client";
import { import {
countryList,
getAllAmenities, getAllAmenities,
hotelBadge, hotelBadge,
hotelFeature, hotelFeature,
hotelFeatureType, hotelFeatureType,
hotelMealList,
hotelTarif, hotelTarif,
hotelTransport, hotelTransport,
hotelType, hotelType,
regionList,
} from "@/pages/tours/lib/api"; } from "@/pages/tours/lib/api";
import Amenities from "@/pages/tours/ui/Amenities"; import Amenities from "@/pages/tours/ui/Amenities";
import BadgeTable from "@/pages/tours/ui/BadgeTable"; import BadgeTable from "@/pages/tours/ui/BadgeTable";
import CountryTable from "@/pages/tours/ui/CountryTable";
import FeaturesTable from "@/pages/tours/ui/FeaturesTable"; import FeaturesTable from "@/pages/tours/ui/FeaturesTable";
import FeaturesTableType from "@/pages/tours/ui/FeaturesTableType"; import FeaturesTableType from "@/pages/tours/ui/FeaturesTableType";
import HotelType from "@/pages/tours/ui/HotelType";
import MealTable from "@/pages/tours/ui/MealTable"; import MealTable from "@/pages/tours/ui/MealTable";
import RegionTable from "@/pages/tours/ui/RegionTable";
import TarifTable from "@/pages/tours/ui/TarifTable"; import TarifTable from "@/pages/tours/ui/TarifTable";
import TransportTable from "@/pages/tours/ui/TransportTable"; import TransportTable from "@/pages/tours/ui/TransportTable";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
@@ -63,6 +69,7 @@ const ToursSetting: React.FC = () => {
const [searchParams] = useSearchParams(); const [searchParams] = useSearchParams();
const [activeTab, setActiveTab] = useState("badge"); const [activeTab, setActiveTab] = useState("badge");
const [featureId, setFeatureId] = useState<number | null>(null); const [featureId, setFeatureId] = useState<number | null>(null);
const [countryId, setCountryId] = useState<number | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
const page = parseInt(searchParams.get("page") || "1", 10); const page = parseInt(searchParams.get("page") || "1", 10);
@@ -120,6 +127,47 @@ const ToursSetting: React.FC = () => {
select: (res) => res.data.data, select: (res) => res.data.data,
}); });
const pageCountry = parseInt(searchParams.get("pageCountry") || "1", 10);
const pageSizeCountry = parseInt(
searchParams.get("pageSizeCountry") || "10",
10,
);
const {
data: countryData,
isLoading: countryLoad,
isError: countryError,
refetch: countryRef,
} = useQuery({
queryKey: ["all_country", pageCountry, pageSizeCountry],
queryFn: () =>
countryList({ page: pageCountry, page_size: pageSizeCountry }),
select: (res) => res.data.data,
});
const pageRegion = parseInt(searchParams.get("pageRegion") || "1", 10);
const pageSizeRegion = parseInt(
searchParams.get("pageSizeRegion") || "10",
10,
);
const {
data: regionData,
isLoading: regionLoad,
isError: regionError,
refetch: regionRef,
} = useQuery({
queryKey: ["all_region", pageRegion, pageSizeRegion, countryId],
queryFn: () =>
regionList({
page: pageRegion,
page_size: pageSizeRegion,
country: countryId!,
}),
select: (res) => res.data.data,
enabled: !!countryId,
});
const pageFeature = parseInt(searchParams.get("pageFeature") || "1", 10); const pageFeature = parseInt(searchParams.get("pageFeature") || "1", 10);
const pageSizeFeature = parseInt( const pageSizeFeature = parseInt(
searchParams.get("pageSizeFeature") || "10", searchParams.get("pageSizeFeature") || "10",
@@ -173,6 +221,20 @@ const ToursSetting: React.FC = () => {
select: (res) => res.data.data, select: (res) => res.data.data,
}); });
const pageMeal = parseInt(searchParams.get("pageMeal") || "1", 10);
const pageSizeMeal = parseInt(searchParams.get("pageSizeMeal") || "10", 10);
const {
data: mealData,
isLoading: mealLoad,
isError: mealError,
refetch: mealRef,
} = useQuery({
queryKey: ["all_meal", pageMeal, pageSizeMeal],
queryFn: () => hotelMealList({ page: pageMeal, page_size: pageSizeMeal }),
select: (res) => res.data.data,
});
const handleTabChange = (value: string) => { const handleTabChange = (value: string) => {
setActiveTab(value); setActiveTab(value);
navigate({ navigate({
@@ -191,6 +253,9 @@ const ToursSetting: React.FC = () => {
> >
<TabsList className="w-full"> <TabsList className="w-full">
<TabsTrigger value="badge">{t("Belgilar (Badge)")}</TabsTrigger> <TabsTrigger value="badge">{t("Belgilar (Badge)")}</TabsTrigger>
<TabsTrigger value="meal_plan">
{t("Ovqatlanish turlari")}
</TabsTrigger>
<TabsTrigger value="tarif">{t("Tariflar")}</TabsTrigger> <TabsTrigger value="tarif">{t("Tariflar")}</TabsTrigger>
<TabsTrigger value="transport">{t("Transport")}</TabsTrigger> <TabsTrigger value="transport">{t("Transport")}</TabsTrigger>
<TabsTrigger value="meal">{t("Qulayliklar")}</TabsTrigger> <TabsTrigger value="meal">{t("Qulayliklar")}</TabsTrigger>
@@ -198,6 +263,7 @@ const ToursSetting: React.FC = () => {
<TabsTrigger value="hotel_features"> <TabsTrigger value="hotel_features">
{t("Otel sharoitlari")} {t("Otel sharoitlari")}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="country">{t("Davlatlar")}</TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="badge" className="space-y-4"> <TabsContent value="badge" className="space-y-4">
@@ -273,7 +339,7 @@ const ToursSetting: React.FC = () => {
onRetry={typeRef} onRetry={typeRef}
/> />
) : ( ) : (
<MealTable <HotelType
data={typeData} data={typeData}
page={pageType} page={pageType}
pageSize={pageSizeType} pageSize={pageSizeType}
@@ -281,6 +347,23 @@ const ToursSetting: React.FC = () => {
)} )}
</TabsContent> </TabsContent>
<TabsContent value="meal_plan" className="space-y-4">
{mealLoad ? (
<LoadingDisplay />
) : mealError ? (
<ErrorDisplay
message={t("Otel turlarini yuklashda xatolik yuz berdi.")}
onRetry={mealRef}
/>
) : (
<MealTable
data={mealData}
page={pageMeal}
pageSize={pageSizeMeal}
/>
)}
</TabsContent>
<TabsContent value="hotel_features" className="space-y-4"> <TabsContent value="hotel_features" className="space-y-4">
{featureLoad ? ( {featureLoad ? (
<LoadingDisplay /> <LoadingDisplay />
@@ -317,6 +400,43 @@ const ToursSetting: React.FC = () => {
/> />
)} )}
</TabsContent> </TabsContent>
<TabsContent value="country" className="space-y-4">
{countryLoad ? (
<LoadingDisplay />
) : countryError ? (
<ErrorDisplay
message={t("Sharoit turlarini yuklashda xatolik yuz berdi.")}
onRetry={countryRef}
/>
) : (
<CountryTable
data={countryData}
page={pageCountry}
setActiveTab={setActiveTab}
setFeatureId={setCountryId}
pageSize={pageSizeCountry}
/>
)}
</TabsContent>
<TabsContent value="region" className="space-y-4">
{regionLoad ? (
<LoadingDisplay />
) : regionError ? (
<ErrorDisplay
message={t("Sharoit turlarini yuklashda xatolik yuz berdi.")}
onRetry={regionRef}
/>
) : (
<RegionTable
data={regionData}
page={pageRegion}
featureId={countryId}
pageSize={pageSizeRegion}
/>
)}
</TabsContent>
</Tabs> </Tabs>
</div> </div>
</div> </div>

View File

@@ -36,6 +36,9 @@ const BANNER = "dashboard/dashboard-site-banner/";
const TOUR_ADMIN = "dashboard/dashboard-tour-admin/"; const TOUR_ADMIN = "dashboard/dashboard-tour-admin/";
const PAYMENT_AGENCY = "dashboard/dashboard-site-agency-payments/"; const PAYMENT_AGENCY = "dashboard/dashboard-site-agency-payments/";
const PAYOT_REQUEST = "dashboard/dashboard-agency-payout-request/"; const PAYOT_REQUEST = "dashboard/dashboard-agency-payout-request/";
const COUNTRY = "dashboard/dashboard-ticket-settings-country/";
const REGION = "dashboard/dashboard-ticket-settings-region/";
const MEAL_PLAN = "dashboard/dashboard-hotel-meal-plan/";
export { export {
AGENCY_ORDERS, AGENCY_ORDERS,
@@ -45,6 +48,7 @@ export {
AUTH_LOGIN, AUTH_LOGIN,
BANNER, BANNER,
BASE_URL, BASE_URL,
COUNTRY,
DOWNLOAD_PDF, DOWNLOAD_PDF,
FAQ, FAQ,
FAQ_CATEGORIES, FAQ_CATEGORIES,
@@ -60,12 +64,14 @@ export {
HOTEL_FEATURES_TYPE, HOTEL_FEATURES_TYPE,
HOTEL_TARIF, HOTEL_TARIF,
HPTEL_TYPES, HPTEL_TYPES,
MEAL_PLAN,
NEWS, NEWS,
NEWS_CATEGORY, NEWS_CATEGORY,
OFFERTA, OFFERTA,
PAYMENT_AGENCY, PAYMENT_AGENCY,
PAYOT_REQUEST, PAYOT_REQUEST,
POPULAR_TOURS, POPULAR_TOURS,
REGION,
SITE_SEO, SITE_SEO,
SITE_SETTING, SITE_SETTING,
SUPPORT_AGENCY, SUPPORT_AGENCY,

View File

@@ -10,6 +10,8 @@
"Xodimlar": "Сотрудники", "Xodimlar": "Сотрудники",
"Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz": "Вы хотите добавить нового пользователя в агентство", "Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz": "Вы хотите добавить нового пользователя в агентство",
"Foydalanuvchi qo'shish": "Добавить пользователя", "Foydalanuvchi qo'shish": "Добавить пользователя",
"Ovqatlanish turlari": "Виды питания",
"Davlatlar": "Государство",
"Byudjet": "Бюджет", "Byudjet": "Бюджет",
"Turlar": "Туры", "Turlar": "Туры",
"Tur sozlamalari": "Настройки туров", "Tur sozlamalari": "Настройки туров",

View File

@@ -12,6 +12,8 @@
"Turlar": "Turlar", "Turlar": "Turlar",
"Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz": "Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz", "Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz": "Siz agentlikga yangi foydalanuvchi qo'shmoqchimisiz",
"Foydalanuvchi qo'shish": "Foydalanuvchi qo'shish", "Foydalanuvchi qo'shish": "Foydalanuvchi qo'shish",
"Ovqalanish turlari": "Ovqalanish turlari",
"Davlatlar": "Davlatlar",
"Tur sozlamalari": "Tur sozlamalari", "Tur sozlamalari": "Tur sozlamalari",
"Bronlar": "Bronlar", "Bronlar": "Bronlar",
"Yangiliklar": "Yangiliklar", "Yangiliklar": "Yangiliklar",

View File

@@ -1,5 +1,5 @@
import * as React from "react";
import * as PopoverPrimitive from "@radix-ui/react-popover"; import * as PopoverPrimitive from "@radix-ui/react-popover";
import * as React from "react";
import { cn } from "@/shared/lib/utils"; import { cn } from "@/shared/lib/utils";
@@ -43,4 +43,4 @@ function PopoverAnchor({
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />; return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
} }
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }; export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger };