api ulandi
This commit is contained in:
353
src/pages/tours/ui/BadgeTable.tsx
Normal file
353
src/pages/tours/ui/BadgeTable.tsx
Normal file
@@ -0,0 +1,353 @@
|
||||
import {
|
||||
hotelBadgeCreate,
|
||||
hotelBadgeDelete,
|
||||
hotelBadgeDetail,
|
||||
hotelBadgeUpdate,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import { BadgesColumns } from "@/pages/tours/lib/column";
|
||||
import type { Badge } 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" }),
|
||||
color: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
|
||||
const BadgeTable = ({
|
||||
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: Badge[];
|
||||
}
|
||||
| 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_badge", editId],
|
||||
queryFn: () => hotelBadgeDetail({ id: editId! }),
|
||||
enabled: !!editId,
|
||||
});
|
||||
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => hotelBadgeDelete({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_badge"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_badge"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
deleteMutate({ id });
|
||||
};
|
||||
|
||||
const columns = BadgesColumns(handleEdit, handleDelete, t);
|
||||
|
||||
const { mutate: create, isPending } = useMutation({
|
||||
mutationFn: ({
|
||||
body,
|
||||
}: {
|
||||
body: { name: string; color: string; name_ru: string };
|
||||
}) => hotelBadgeCreate({ body }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_badge"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_badge"] });
|
||||
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; color: string; name_ru: string };
|
||||
}) => hotelBadgeUpdate({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_badge"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_badge"] });
|
||||
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: "",
|
||||
color: "#000000",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (badgeDetail) {
|
||||
form.setValue("color", badgeDetail.data.data.color);
|
||||
form.setValue("name", badgeDetail.data.data.name);
|
||||
form.setValue("name_ru", badgeDetail.data.data.name_ru);
|
||||
}
|
||||
}, [editId, badgeDetail]);
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
if (types === "create") {
|
||||
create({
|
||||
body: {
|
||||
color: values.color,
|
||||
name: values.name,
|
||||
name_ru: values.name_ru,
|
||||
},
|
||||
});
|
||||
} else if (types === "edit" && editId) {
|
||||
update({
|
||||
id: editId,
|
||||
body: {
|
||||
color: values.color,
|
||||
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("Qo‘shish")}
|
||||
</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>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="color"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Rang")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="flex items-center gap-3">
|
||||
<Input
|
||||
type="color"
|
||||
{...field}
|
||||
className="h-12 p-1 cursor-pointer rounded-md border border-gray-400"
|
||||
/>
|
||||
<span className="text-sm text-gray-400">
|
||||
{field.value}
|
||||
</span>
|
||||
</div>
|
||||
</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 BadgeTable;
|
||||
@@ -1,15 +1,28 @@
|
||||
"use client";
|
||||
|
||||
import { getOneTours } from "@/pages/tours/lib/api";
|
||||
import StepOne from "@/pages/tours/ui/StepOne";
|
||||
import StepTwo from "@/pages/tours/ui/StepTwo";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Hotel, Plane } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useParams } from "react-router-dom";
|
||||
|
||||
const CreateEditTour = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { t } = useTranslation();
|
||||
const isEditMode = useMemo(() => !!id, [id]);
|
||||
const [step, setStep] = useState(1);
|
||||
const { data } = useQuery({
|
||||
queryKey: ["tours_detail", id],
|
||||
queryFn: () => {
|
||||
return getOneTours({ id: Number(id) });
|
||||
},
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="p-8 w-full mx-auto bg-gray-900">
|
||||
@@ -21,16 +34,18 @@ const CreateEditTour = () => {
|
||||
<div
|
||||
className={`flex-1 text-center py-2 rounded-l-lg ${step === 1 ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground border"}`}
|
||||
>
|
||||
1. Tur ma'lumotlari <Plane className="w-5 h-5 inline ml-2" />
|
||||
1. {t("Tur ma'lumotlari")} <Plane className="w-5 h-5 inline ml-2" />
|
||||
</div>
|
||||
<div
|
||||
className={`flex-1 text-center py-2 rounded-r-lg ${step === 2 ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground border"}`}
|
||||
>
|
||||
2. Mehmonxona <Hotel className="w-5 h-5 inline ml-2" />
|
||||
2. {t("Mehmonxona")} <Hotel className="w-5 h-5 inline ml-2" />
|
||||
</div>
|
||||
</div>
|
||||
{step === 1 && <StepOne setStep={setStep} />}
|
||||
{step === 2 && <StepTwo setStep={setStep} />}
|
||||
{step === 1 && (
|
||||
<StepOne setStep={setStep} data={data} isEditMode={isEditMode} />
|
||||
)}
|
||||
{step === 2 && <StepTwo data={data} isEditMode={isEditMode} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
351
src/pages/tours/ui/FeaturesTable.tsx
Normal file
351
src/pages/tours/ui/FeaturesTable.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
import {
|
||||
hotelFeatureCreate,
|
||||
hotelFeatureDelete,
|
||||
hotelFeatureDetail,
|
||||
hotelFeatureUpdate,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import { FeatureColumns } from "@/pages/tours/lib/column";
|
||||
import type { HotelFeatures } 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 FeaturesTable = ({
|
||||
data,
|
||||
page,
|
||||
pageSize,
|
||||
setActiveTab,
|
||||
setFeatureId,
|
||||
}: {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
setActiveTab: Dispatch<SetStateAction<string>>;
|
||||
setFeatureId: Dispatch<SetStateAction<number | null>>;
|
||||
data:
|
||||
| {
|
||||
links: {
|
||||
previous: string;
|
||||
next: string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
results: HotelFeatures[];
|
||||
}
|
||||
| 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_feature", editId],
|
||||
queryFn: () => hotelFeatureDetail({ id: editId! }),
|
||||
enabled: !!editId,
|
||||
});
|
||||
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => hotelFeatureDelete({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_feature"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_feature"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
deleteMutate({ id });
|
||||
};
|
||||
|
||||
const columns = FeatureColumns(
|
||||
handleEdit,
|
||||
handleDelete,
|
||||
t,
|
||||
setActiveTab,
|
||||
setFeatureId,
|
||||
);
|
||||
|
||||
const { mutate: create, isPending } = useMutation({
|
||||
mutationFn: ({
|
||||
body,
|
||||
}: {
|
||||
body: {
|
||||
hotel_feature_type_name: string;
|
||||
hotel_feature_type_name_ru: string;
|
||||
};
|
||||
}) => hotelFeatureCreate({ body }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_feature"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_feature"] });
|
||||
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: {
|
||||
hotel_feature_type_name: string;
|
||||
hotel_feature_type_name_ru: string;
|
||||
};
|
||||
}) => hotelFeatureUpdate({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_feature"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_feature"] });
|
||||
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.hotel_feature_type_name);
|
||||
form.setValue(
|
||||
"name_ru",
|
||||
badgeDetail.data.data.hotel_feature_type_name_ru,
|
||||
);
|
||||
}
|
||||
}, [editId, badgeDetail]);
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
if (types === "create") {
|
||||
create({
|
||||
body: {
|
||||
hotel_feature_type_name: values.name,
|
||||
hotel_feature_type_name_ru: values.name_ru,
|
||||
},
|
||||
});
|
||||
} else if (types === "edit" && editId) {
|
||||
update({
|
||||
id: editId,
|
||||
body: {
|
||||
hotel_feature_type_name: values.name,
|
||||
hotel_feature_type_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("Qo‘shish")}
|
||||
</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="pageFeature"
|
||||
namePageSize="pageSizeFeature"
|
||||
/>
|
||||
|
||||
<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 FeaturesTable;
|
||||
342
src/pages/tours/ui/FeaturesTableType.tsx
Normal file
342
src/pages/tours/ui/FeaturesTableType.tsx
Normal file
@@ -0,0 +1,342 @@
|
||||
import {
|
||||
hotelFeatureTypeCreate,
|
||||
hotelFeatureTypeDelete,
|
||||
hotelFeatureTypeDetail,
|
||||
hotelFeatureTypeUpdate,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import { FeatureTypeColumns } from "@/pages/tours/lib/column";
|
||||
import type { HotelFeaturesType } 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 FeaturesTableType = ({
|
||||
data,
|
||||
page,
|
||||
pageSize,
|
||||
featureId,
|
||||
}: {
|
||||
page: number;
|
||||
featureId: number | null;
|
||||
pageSize: number;
|
||||
data:
|
||||
| {
|
||||
links: {
|
||||
previous: string;
|
||||
next: string;
|
||||
};
|
||||
total_items: number;
|
||||
total_pages: number;
|
||||
page_size: number;
|
||||
current_page: number;
|
||||
results: HotelFeaturesType[];
|
||||
}
|
||||
| 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_feature_type", editId],
|
||||
queryFn: () => hotelFeatureTypeDetail({ id: editId! }),
|
||||
enabled: !!editId,
|
||||
});
|
||||
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => hotelFeatureTypeDelete({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_feature_type"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_feature_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 = FeatureTypeColumns(handleEdit, handleDelete, t);
|
||||
|
||||
const { mutate: create, isPending } = useMutation({
|
||||
mutationFn: ({
|
||||
body,
|
||||
}: {
|
||||
body: {
|
||||
feature_name: string;
|
||||
feature_name_ru: string;
|
||||
feature_type: number;
|
||||
};
|
||||
}) => hotelFeatureTypeCreate({ body }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_feature_type"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_feature_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: {
|
||||
feature_name: string;
|
||||
feature_name_ru: string;
|
||||
};
|
||||
}) => hotelFeatureTypeUpdate({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["detail_feature_type"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_feature_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 (badgeDetail) {
|
||||
form.setValue("name", badgeDetail.data.data.feature_name);
|
||||
form.setValue("name_ru", badgeDetail.data.data.feature_name_ru);
|
||||
}
|
||||
}, [editId, badgeDetail]);
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
if (types === "create") {
|
||||
create({
|
||||
body: {
|
||||
feature_name: values.name,
|
||||
feature_name_ru: values.name_ru,
|
||||
feature_type: featureId!,
|
||||
},
|
||||
});
|
||||
} else if (types === "edit" && editId) {
|
||||
update({
|
||||
id: editId,
|
||||
body: {
|
||||
feature_name: values.name,
|
||||
feature_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("Qo‘shish")}
|
||||
</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="pageFeature"
|
||||
namePageSize="pageSizeFeature"
|
||||
/>
|
||||
|
||||
<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 FeaturesTableType;
|
||||
324
src/pages/tours/ui/MealTable.tsx
Normal file
324
src/pages/tours/ui/MealTable.tsx
Normal 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 MealTable = ({
|
||||
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);
|
||||
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("Qo‘shish")}
|
||||
</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 MealTable;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
createHotel,
|
||||
hotelFeature,
|
||||
hotelFeatureType,
|
||||
hotelType,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import { useTicketStore } from "@/pages/tours/lib/store";
|
||||
import type {
|
||||
GetOneTours,
|
||||
HotelFeatures,
|
||||
HotelFeaturesType,
|
||||
Type,
|
||||
} from "@/pages/tours/lib/type";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -15,45 +30,64 @@ import {
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { X } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import z from "zod";
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string().min(2, {
|
||||
message: "Sarlavha kamida 2 ta belgidan iborat bo‘lishi kerak.",
|
||||
message: "Sarlavha kamida 2 ta belgidan iborat bo'lishi kerak",
|
||||
}),
|
||||
rating: z.number(),
|
||||
rating: z.number().min(1).max(5),
|
||||
mealPlan: z.string().min(1, { message: "Taom rejasi tanlanishi majburiy" }),
|
||||
hotelType: z
|
||||
.string()
|
||||
.min(1, { message: "Mehmonxona turi tanlanishi majburiy" }),
|
||||
.array(z.string())
|
||||
.min(1, { message: "Kamida 1 ta mehmonxona turi tanlang" }),
|
||||
hotelFeatures: z
|
||||
.array(z.string())
|
||||
.min(1, { message: "Kamida 1 ta xususiyat tanlanishi kerak" }),
|
||||
.min(1, { message: "Kamida 1 ta xususiyat tanlang" }),
|
||||
hotelFeaturesType: z
|
||||
.array(z.string())
|
||||
.min(1, { message: "Kamida 1 ta tur tanlang" }),
|
||||
});
|
||||
|
||||
const StepTwo = ({
|
||||
setStep,
|
||||
data,
|
||||
isEditMode,
|
||||
}: {
|
||||
setStep: Dispatch<SetStateAction<number>>;
|
||||
data: GetOneTours | undefined;
|
||||
isEditMode: boolean;
|
||||
}) => {
|
||||
const { amenities, id: ticketId } = useTicketStore();
|
||||
const navigator = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
rating: 3.0,
|
||||
mealPlan: "",
|
||||
hotelType: "",
|
||||
hotelType: [],
|
||||
hotelFeatures: [],
|
||||
hotelFeaturesType: [],
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit() {
|
||||
navigator("tours");
|
||||
}
|
||||
useEffect(() => {
|
||||
if (isEditMode && data?.data) {
|
||||
const tourData = data.data;
|
||||
|
||||
form.setValue("title", tourData.hotel_name);
|
||||
form.setValue("rating", Number(tourData.hotel_rating));
|
||||
form.setValue("mealPlan", tourData.hotel_meals);
|
||||
}
|
||||
}, [isEditMode, data, form]);
|
||||
|
||||
const mealPlans = [
|
||||
"Breakfast Only",
|
||||
@@ -61,24 +95,221 @@ const StepTwo = ({
|
||||
"Full Board",
|
||||
"All Inclusive",
|
||||
];
|
||||
const hotelTypes = ["Hotel", "Resort", "Guest House", "Apartment"];
|
||||
const [allHotelTypes, setAllHotelTypes] = useState<Type[]>([]);
|
||||
const [allHotelFeature, setAllHotelFeature] = useState<HotelFeatures[]>([]);
|
||||
const [allHotelFeatureType, setAllHotelFeatureType] = useState<
|
||||
HotelFeaturesType[]
|
||||
>([]);
|
||||
|
||||
const [featureTypeMapping, setFeatureTypeMapping] = useState<
|
||||
Record<string, string[]>
|
||||
>({});
|
||||
|
||||
const selectedHotelFeatures = form.watch("hotelFeatures");
|
||||
|
||||
useEffect(() => {
|
||||
const loadAll = async () => {
|
||||
try {
|
||||
let page = 1;
|
||||
let results: Type[] = [];
|
||||
let hasNext = true;
|
||||
|
||||
while (hasNext) {
|
||||
const res = await hotelType({ page, page_size: 50 });
|
||||
const data = res.data.data;
|
||||
results = [...results, ...data.results];
|
||||
hasNext = !!data.links.next;
|
||||
page++;
|
||||
}
|
||||
setAllHotelTypes(results);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
loadAll();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadAll = async () => {
|
||||
try {
|
||||
let page = 1;
|
||||
let results: HotelFeatures[] = [];
|
||||
let hasNext = true;
|
||||
|
||||
while (hasNext) {
|
||||
const res = await hotelFeature({ page, page_size: 50 });
|
||||
const data = res.data.data;
|
||||
results = [...results, ...data.results];
|
||||
hasNext = !!data.links.next;
|
||||
page++;
|
||||
}
|
||||
setAllHotelFeature(results);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
loadAll();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedHotelFeatures.length === 0) {
|
||||
setAllHotelFeatureType([]);
|
||||
setFeatureTypeMapping({});
|
||||
return;
|
||||
}
|
||||
|
||||
const loadAll = async () => {
|
||||
try {
|
||||
const selectedFeatureIds = selectedHotelFeatures
|
||||
.map((featureId) => Number(featureId))
|
||||
.filter((id) => !isNaN(id));
|
||||
|
||||
if (selectedFeatureIds.length === 0) return;
|
||||
|
||||
let allResults: HotelFeaturesType[] = [];
|
||||
const newMapping: Record<string, string[]> = {};
|
||||
|
||||
for (const featureId of selectedFeatureIds) {
|
||||
let page = 1;
|
||||
let hasNext = true;
|
||||
const featureTypes: string[] = [];
|
||||
|
||||
while (hasNext) {
|
||||
const res = await hotelFeatureType({
|
||||
page,
|
||||
page_size: 50,
|
||||
feature_type: featureId,
|
||||
});
|
||||
const data = res.data.data;
|
||||
allResults = [...allResults, ...data.results];
|
||||
|
||||
data.results.forEach((item: HotelFeaturesType) => {
|
||||
featureTypes.push(String(item.id));
|
||||
});
|
||||
|
||||
hasNext = !!data.links.next;
|
||||
page++;
|
||||
}
|
||||
|
||||
newMapping[String(featureId)] = featureTypes;
|
||||
}
|
||||
|
||||
const uniqueResults = allResults.filter(
|
||||
(item, index, self) =>
|
||||
index === self.findIndex((t) => t.id === item.id),
|
||||
);
|
||||
|
||||
setAllHotelFeatureType(uniqueResults);
|
||||
setFeatureTypeMapping(newMapping);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
loadAll();
|
||||
}, [selectedHotelFeatures]);
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: FormData) => createHotel({ body }),
|
||||
onSuccess: () => {
|
||||
navigator("/tours");
|
||||
toast.success(t("Muvaffaqiyatli saqlandi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const removeHotelType = (typeId: string) => {
|
||||
const current = form.getValues("hotelType");
|
||||
form.setValue(
|
||||
"hotelType",
|
||||
current.filter((val) => val !== typeId),
|
||||
);
|
||||
};
|
||||
|
||||
const onSubmit = (data: z.infer<typeof formSchema>) => {
|
||||
const formData = new FormData();
|
||||
formData.append("ticket", ticketId ? ticketId?.toString() : "");
|
||||
formData.append("name", data.title);
|
||||
formData.append("rating", String(data.rating));
|
||||
formData.append(
|
||||
"meal_plan",
|
||||
data.mealPlan === "Breakfast Only"
|
||||
? "breakfast"
|
||||
: data.mealPlan === "All Inclusive"
|
||||
? "all_inclusive"
|
||||
: data.mealPlan === "Half Board"
|
||||
? "half_board"
|
||||
: data.mealPlan === "Full Board"
|
||||
? "full_board"
|
||||
: "all_inclusive",
|
||||
);
|
||||
|
||||
data.hotelType.forEach((typeId) => {
|
||||
formData.append("hotel_type", typeId);
|
||||
});
|
||||
|
||||
data.hotelFeaturesType.forEach((typeId) => {
|
||||
formData.append("hotel_features", typeId);
|
||||
});
|
||||
|
||||
amenities.forEach((e, i) => {
|
||||
formData.append(`hotel_amenities[${i}]name`, e.name);
|
||||
formData.append(`hotel_amenities[${i}]name_ru`, e.name_ru);
|
||||
formData.append(`hotel_amenities[${i}]icon_name`, e.icon_name);
|
||||
});
|
||||
|
||||
mutate(formData);
|
||||
};
|
||||
|
||||
const removeHotelFeature = (featureId: string) => {
|
||||
const currentFeatures = form.getValues("hotelFeatures");
|
||||
const currentFeatureTypes = form.getValues("hotelFeaturesType");
|
||||
|
||||
const typesToRemove = featureTypeMapping[featureId] || [];
|
||||
|
||||
form.setValue(
|
||||
"hotelFeatures",
|
||||
currentFeatures.filter((val) => val !== featureId),
|
||||
);
|
||||
|
||||
form.setValue(
|
||||
"hotelFeaturesType",
|
||||
currentFeatureTypes.filter((val) => !typesToRemove.includes(val)),
|
||||
);
|
||||
};
|
||||
|
||||
const removeFeatureType = (typeId: string) => {
|
||||
const currentValues = form.getValues("hotelFeaturesType");
|
||||
form.setValue(
|
||||
"hotelFeaturesType",
|
||||
currentValues.filter((val) => val !== typeId),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
|
||||
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-start">
|
||||
<div className="grid grid-cols-2 gap-4 max-lg:grid-cols-1 items-end justify-end">
|
||||
{/* Mehmonxona nomi */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Mehmonxona nomi</Label>
|
||||
<Label>{t("Mehmonxona nomi")}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Toshkent - Dubay"
|
||||
{...field}
|
||||
className="h-12 !text-md"
|
||||
className="h-12"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -86,25 +317,36 @@ const StepTwo = ({
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Mehmonxona rating */}
|
||||
{/* Rating */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="rating"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Mehmonxona raytingi</Label>
|
||||
<Label>{t("Mehmonxona reytingi")}</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="3.0"
|
||||
{...field}
|
||||
className="h-12 !text-md"
|
||||
value={field.value}
|
||||
className="h-12"
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
if (/^\d*\.?\d*$/.test(val)) {
|
||||
// Faqat raqam va nuqta kiritishga ruxsat berish
|
||||
if (/^\d*\.?\d*$/.test(val) || val === "") {
|
||||
field.onChange(val);
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
const val = e.target.value;
|
||||
if (val && !isNaN(parseFloat(val))) {
|
||||
// Agar 1 xonali bo'lsa, .0 qo'shish
|
||||
const num = parseFloat(val);
|
||||
if (val.indexOf(".") === -1) {
|
||||
field.onChange(num.toFixed(1));
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -116,31 +358,22 @@ const StepTwo = ({
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="mealPlan"
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Meal Plan</Label>
|
||||
<Label>{t("Taom rejasi")}</Label>
|
||||
<FormControl>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="mealPlan"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue placeholder="Taom rejasini tanlang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mealPlans.map((plan) => (
|
||||
<SelectItem key={plan} value={plan}>
|
||||
{plan}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue placeholder={t("Tanlang")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{mealPlans.map((plan) => (
|
||||
<SelectItem key={plan} value={plan}>
|
||||
{t(plan)}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -151,134 +384,232 @@ const StepTwo = ({
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hotelType"
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Mehmonxona turi</Label>
|
||||
<Label>{t("Mehmonxona turlari")}</Label>
|
||||
<FormControl>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="hotelType"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue placeholder="Mehmonxona turini tanlang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{hotelTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
<div className="space-y-2">
|
||||
{field.value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||||
{field.value.map((selectedValue) => {
|
||||
const selectedItem = allHotelTypes.find(
|
||||
(item) => String(item.id) === selectedValue,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={selectedValue}
|
||||
className="flex items-center gap-1 bg-purple-600 text-white px-3 py-1 rounded-md text-sm"
|
||||
>
|
||||
<span>{selectedItem?.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeHotelType(selectedValue)}
|
||||
className="hover:bg-purple-700 rounded-full p-0.5"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(value) => {
|
||||
if (!field.value.includes(value)) {
|
||||
field.onChange([...field.value, value]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
field.value.length > 0
|
||||
? t("Yana tanlang...")
|
||||
: t("Tanlang")
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allHotelTypes
|
||||
.filter(
|
||||
(type) => !field.value.includes(String(type.id)),
|
||||
)
|
||||
.map((type) => (
|
||||
<SelectItem key={type.id} value={String(type.id)}>
|
||||
{type.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
{/* Hotel Features */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hotelFeatures"
|
||||
render={() => (
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Mehmonxona qulayliklar</Label>
|
||||
<Label>{t("Mehmonxona xususiyatlari")}</Label>
|
||||
<FormControl>
|
||||
<div className="space-y-2">
|
||||
{field.value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||||
{field.value.map((selectedValue) => {
|
||||
const selectedItem = allHotelFeature.find(
|
||||
(item) => String(item.id) === selectedValue,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={selectedValue}
|
||||
className="flex items-center gap-1 bg-blue-600 text-white px-3 py-1 rounded-md text-sm"
|
||||
>
|
||||
<span>
|
||||
{selectedItem?.hotel_feature_type_name}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
removeHotelFeature(selectedValue)
|
||||
}
|
||||
className="hover:bg-blue-700 rounded-full p-0.5"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{form.watch("amenities").map((item, idx) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Icon =
|
||||
(LucideIcons as any)[item.icon_name] || XIcon;
|
||||
return (
|
||||
<Badge
|
||||
key={idx}
|
||||
variant="secondary"
|
||||
className="px-3 py-1 text-sm flex items-center gap-2"
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
<span>{item.name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const current = form.getValues("amenities");
|
||||
form.setValue(
|
||||
"amenities",
|
||||
current.filter((_, i: number) => i !== idx),
|
||||
);
|
||||
}}
|
||||
className="ml-1 text-muted-foreground hover:text-destructive"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</button>
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex gap-3 items-center">
|
||||
<IconSelect
|
||||
setSelectedIcon={setSelectedIcon}
|
||||
selectedIcon={selectedIcon}
|
||||
/>
|
||||
|
||||
<Input
|
||||
id="amenity_name"
|
||||
placeholder="Qulaylik nomi (masalan: Wi-Fi)"
|
||||
className="h-12 !text-md flex-1"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const nameInput = document.getElementById(
|
||||
"amenity_name",
|
||||
) as HTMLInputElement;
|
||||
|
||||
if (selectedIcon && nameInput.value) {
|
||||
const current = form.getValues("amenities");
|
||||
form.setValue("amenities", [
|
||||
...current,
|
||||
{
|
||||
icon_name: selectedIcon,
|
||||
name: nameInput.value,
|
||||
},
|
||||
]);
|
||||
nameInput.value = "";
|
||||
setSelectedIcon("");
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(value) => {
|
||||
if (!field.value.includes(value)) {
|
||||
field.onChange([...field.value, value]);
|
||||
}
|
||||
}}
|
||||
className="h-12"
|
||||
>
|
||||
Qo‘shish
|
||||
</Button>
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
field.value.length > 0
|
||||
? t("Yana tanlang...")
|
||||
: t("Tanlang")
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allHotelFeature
|
||||
.filter(
|
||||
(type) => !field.value.includes(String(type.id)),
|
||||
)
|
||||
.map((type) => (
|
||||
<SelectItem key={type.id} value={String(type.id)}>
|
||||
{type.hotel_feature_type_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setStep(1)}
|
||||
className="mt-6 px-6 py-3 bg-gray-600 text-white rounded-md"
|
||||
>
|
||||
Ortga
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
className="mt-6 px-6 py-3 bg-blue-600 text-white rounded-md"
|
||||
>
|
||||
Saqlash
|
||||
</button>
|
||||
/>
|
||||
|
||||
{/* Hotel Feature Type */}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hotelFeaturesType"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>{t("Xususiyat turlari")}</Label>
|
||||
<FormControl>
|
||||
<div className="space-y-2">
|
||||
{field.value.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 p-2 border rounded-md">
|
||||
{field.value.map((selectedValue) => {
|
||||
const selectedItem = allHotelFeatureType.find(
|
||||
(item) => String(item.id) === selectedValue,
|
||||
);
|
||||
return (
|
||||
<div
|
||||
key={selectedValue}
|
||||
className="flex items-center gap-1 bg-green-600 text-white px-3 py-1 rounded-md text-sm"
|
||||
>
|
||||
<span>{selectedItem?.feature_name}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeFeatureType(selectedValue)}
|
||||
className="hover:bg-green-700 rounded-full p-0.5"
|
||||
>
|
||||
<X size={14} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Select
|
||||
value=""
|
||||
onValueChange={(value) => {
|
||||
if (!field.value.includes(value)) {
|
||||
field.onChange([...field.value, value]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="!h-12 w-full">
|
||||
<SelectValue
|
||||
placeholder={
|
||||
selectedHotelFeatures.length === 0
|
||||
? t("Avval xususiyat tanlang")
|
||||
: field.value.length > 0
|
||||
? t("Yana tanlang...")
|
||||
: t("Tanlang")
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allHotelFeatureType.length === 0 ? (
|
||||
<div className="p-2 text-sm text-gray-500">
|
||||
{t("Avval mehmonxona xususiyatini tanlang")}
|
||||
</div>
|
||||
) : (
|
||||
allHotelFeatureType
|
||||
.filter(
|
||||
(type) => !field.value.includes(String(type.id)),
|
||||
)
|
||||
.map((type) => (
|
||||
<SelectItem key={type.id} value={String(type.id)}>
|
||||
{type.feature_name}
|
||||
</SelectItem>
|
||||
))
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isPending}
|
||||
className="mt-6 px-6 py-3 bg-blue-600 text-white rounded-md disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isPending ? t("Yuklanmoqda...") : t("Saqlash")}
|
||||
</button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
|
||||
320
src/pages/tours/ui/TarifTable.tsx
Normal file
320
src/pages/tours/ui/TarifTable.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import {
|
||||
hotelTarfiDetail,
|
||||
hotelTarifCreate,
|
||||
hotelTarifDelete,
|
||||
hoteltarifUpdate,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import { TarifColumns } from "@/pages/tours/lib/column";
|
||||
import type { Tarif } 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 TarifTable = ({
|
||||
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: Tarif[];
|
||||
}
|
||||
| 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: tarifDetail } = useQuery({
|
||||
queryKey: ["tarif_badge", editId],
|
||||
queryFn: () => hotelTarfiDetail({ id: editId! }),
|
||||
enabled: !!editId,
|
||||
});
|
||||
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => hotelTarifDelete({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["tarif_badge"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_tarif"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(t("Xatolik yuz berdi"), {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
deleteMutate({ id });
|
||||
};
|
||||
|
||||
const columns = TarifColumns(handleEdit, handleDelete, t);
|
||||
|
||||
const { mutate: create, isPending } = useMutation({
|
||||
mutationFn: ({ body }: { body: { name: string; name_ru: string } }) =>
|
||||
hotelTarifCreate({ body }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["tarif_badge"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_tarif"] });
|
||||
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 };
|
||||
}) => hoteltarifUpdate({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["tarif_badge"] });
|
||||
queryClient.refetchQueries({ queryKey: ["all_tarif"] });
|
||||
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 (tarifDetail) {
|
||||
form.setValue("name", tarifDetail.data.data.name);
|
||||
}
|
||||
}, [editId, tarifDetail]);
|
||||
|
||||
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" disabled>
|
||||
<PlusIcon className="mr-2" />
|
||||
{t("Qo‘shish")}
|
||||
</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="pageTarif"
|
||||
namePageSize="pageTarifSize"
|
||||
/>
|
||||
|
||||
<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 TarifTable;
|
||||
@@ -9,21 +9,37 @@ import {
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { ImagePlus, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface TicketsImagesModelProps {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
form: any;
|
||||
form: any; // React Hook Form control
|
||||
name: string;
|
||||
label?: string;
|
||||
imageUrl?: string | string[] | undefined;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
export default function TicketsImagesModel({
|
||||
form,
|
||||
name,
|
||||
label = "Rasmlar",
|
||||
multiple = true,
|
||||
imageUrl,
|
||||
}: TicketsImagesModelProps) {
|
||||
const { t } = useTranslation();
|
||||
const [previews, setPreviews] = useState<string[]>([]);
|
||||
const inputId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
if (imageUrl) {
|
||||
if (Array.isArray(imageUrl)) {
|
||||
setPreviews(imageUrl);
|
||||
} else {
|
||||
setPreviews([imageUrl]);
|
||||
}
|
||||
}
|
||||
}, [imageUrl]);
|
||||
|
||||
return (
|
||||
<FormField
|
||||
@@ -32,39 +48,61 @@ export default function TicketsImagesModel({
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Label className="text-md">{label}</Label>
|
||||
|
||||
<FormControl>
|
||||
<div className="flex flex-col gap-3">
|
||||
{/* File Input */}
|
||||
<Input
|
||||
id="ticket-images"
|
||||
id={inputId}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
multiple={multiple}
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
const newFiles = e.target.files
|
||||
? Array.from(e.target.files)
|
||||
: [];
|
||||
const existingFiles = form.getValues(name) || [];
|
||||
const allFiles = [...existingFiles, ...newFiles];
|
||||
|
||||
form.setValue(name, allFiles);
|
||||
const urls = allFiles.map((file) =>
|
||||
URL.createObjectURL(file),
|
||||
);
|
||||
setPreviews(urls);
|
||||
if (multiple) {
|
||||
// ✅ Bir nechta rasm
|
||||
const allFiles = [
|
||||
...(form.getValues(name) || []),
|
||||
...newFiles,
|
||||
];
|
||||
form.setValue(name, allFiles);
|
||||
const urls = allFiles.map((file: File) =>
|
||||
URL.createObjectURL(file),
|
||||
);
|
||||
setPreviews(urls);
|
||||
} else {
|
||||
// ✅ Faqat bitta rasm (banner)
|
||||
const singleFile = newFiles[0] || null;
|
||||
form.setValue(name, singleFile);
|
||||
setPreviews(
|
||||
singleFile ? [URL.createObjectURL(singleFile)] : [],
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Upload Zone */}
|
||||
<label
|
||||
htmlFor="ticket-images"
|
||||
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"
|
||||
>
|
||||
<ImagePlus className="size-8 text-muted-foreground mb-2" />
|
||||
<p className="font-semibold text-white">Rasmlarni tanlang</p>
|
||||
<p className="text-sm text-white">
|
||||
Bir nechta rasm yuklashingiz mumkin
|
||||
<p className="font-semibold text-white">
|
||||
{t("Rasmlarni tanlang")}
|
||||
</p>
|
||||
{multiple ? (
|
||||
<p className="text-sm text-white">
|
||||
{t("Bir nechta rasm yuklashingiz mumkin")}
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-sm text-white">
|
||||
{t("Faqat bitta rasm yuklash mumkin")}
|
||||
</p>
|
||||
)}
|
||||
</label>
|
||||
|
||||
{/* Preview Images */}
|
||||
@@ -80,19 +118,29 @@ export default function TicketsImagesModel({
|
||||
alt={`preview-${i}`}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
|
||||
{/* Delete Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newFiles = form
|
||||
.getValues(name)
|
||||
.filter((_: File, idx: number) => idx !== i);
|
||||
const newPreviews = previews.filter(
|
||||
(_: string, idx: number) => idx !== i,
|
||||
);
|
||||
form.setValue(name, newFiles);
|
||||
setPreviews(newPreviews);
|
||||
if (multiple) {
|
||||
// ✅ Ko‘p rasm holati
|
||||
const currentFiles = form.getValues(name) || [];
|
||||
const newFiles = currentFiles.filter(
|
||||
(_: File, idx: number) => idx !== i,
|
||||
);
|
||||
form.setValue(name, newFiles);
|
||||
const newPreviews = previews.filter(
|
||||
(_: string, idx: number) => idx !== i,
|
||||
);
|
||||
setPreviews(newPreviews);
|
||||
} else {
|
||||
// ✅ Bitta rasm holati
|
||||
form.setValue(name, null);
|
||||
setPreviews([]);
|
||||
}
|
||||
}}
|
||||
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 shadow hover:bg-white"
|
||||
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 shadow hover:bg-white transition"
|
||||
>
|
||||
<XIcon className="size-4 text-destructive" />
|
||||
</button>
|
||||
@@ -102,6 +150,7 @@ export default function TicketsImagesModel({
|
||||
)}
|
||||
</div>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card";
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
Utensils,
|
||||
} from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
type TourDetail = {
|
||||
@@ -59,6 +61,7 @@ type TourDetail = {
|
||||
};
|
||||
|
||||
export default function TourDetailPage() {
|
||||
const { t } = useTranslation();
|
||||
const params = useParams();
|
||||
const router = useNavigate();
|
||||
const [tour, setTour] = useState<TourDetail | null>(null);
|
||||
@@ -251,10 +254,10 @@ export default function TourDetailPage() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<DollarSign className="w-5 h-5 text-green-400" />
|
||||
<p className="text-sm text-gray-400">Narxi</p>
|
||||
<p className="text-sm text-gray-400">{t("Narxi")}</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{tour.price.toLocaleString()} so'm
|
||||
{formatPrice(tour.price, true)}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -263,10 +266,10 @@ export default function TourDetailPage() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Clock className="w-5 h-5 text-blue-400" />
|
||||
<p className="text-sm text-gray-400">Davomiyligi</p>
|
||||
<p className="text-sm text-gray-400">{t("Davomiyligi")}</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{tour.duration_days} kun
|
||||
{tour.duration_days} {t("kun")}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -275,10 +278,10 @@ export default function TourDetailPage() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Users className="w-5 h-5 text-purple-400" />
|
||||
<p className="text-sm text-gray-400">Yo'lovchilar</p>
|
||||
<p className="text-sm text-gray-400">{t("Yo'lovchilar")}</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
{tour.passenger_count} kishi
|
||||
{tour.passenger_count} {t("kishi")}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -287,7 +290,7 @@ export default function TourDetailPage() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Calendar className="w-5 h-5 text-yellow-400" />
|
||||
<p className="text-sm text-gray-400">Jo'nash sanasi</p>
|
||||
<p className="text-sm text-gray-400">{t("Jo'nash sanasi")}</p>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-white">
|
||||
{new Date(tour.departure_date).toLocaleDateString("uz-UZ")}
|
||||
@@ -302,31 +305,31 @@ export default function TourDetailPage() {
|
||||
value="overview"
|
||||
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
|
||||
>
|
||||
Umumiy
|
||||
{t("Umumiy")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="itinerary"
|
||||
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
|
||||
>
|
||||
Marshshrut
|
||||
{t("Marshshrut")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="services"
|
||||
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
|
||||
>
|
||||
Xizmatlar
|
||||
{t("Xizmatlar")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="hotel"
|
||||
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
|
||||
>
|
||||
Mehmonxona
|
||||
{t("Mehmonxona")}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="reviews"
|
||||
className="data-[state=active]:bg-gray-700 data-[state=active]:text-white text-gray-400"
|
||||
>
|
||||
Sharhlar
|
||||
{t("Sharhlar")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -334,7 +337,7 @@ export default function TourDetailPage() {
|
||||
<Card className="border-gray-700 shadow-lg bg-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl text-white">
|
||||
Tur haqida ma'lumot
|
||||
{t("Tur haqida ma'lumot")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@@ -343,7 +346,9 @@ export default function TourDetailPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<MapPin className="w-5 h-5 text-green-400 mt-1" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Jo'nash joyi</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{t("Jo'nash joyi")}
|
||||
</p>
|
||||
<p className="font-semibold text-white">
|
||||
{tour.departure}
|
||||
</p>
|
||||
@@ -353,7 +358,9 @@ export default function TourDetailPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<MapPin className="w-5 h-5 text-blue-400 mt-1" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Yo'nalish</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{t("Yo'nalish")}
|
||||
</p>
|
||||
<p className="font-semibold text-white">
|
||||
{tour.destination}
|
||||
</p>
|
||||
@@ -363,7 +370,7 @@ export default function TourDetailPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<Globe className="w-5 h-5 text-purple-400 mt-1" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Tillar</p>
|
||||
<p className="text-sm text-gray-400">{t("Tillar")}</p>
|
||||
<p className="font-semibold text-white">
|
||||
{tour.languages}
|
||||
</p>
|
||||
@@ -375,7 +382,9 @@ export default function TourDetailPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<Hotel className="w-5 h-5 text-yellow-400 mt-1" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Mehmonxona</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{t("Mehmonxona")}
|
||||
</p>
|
||||
<p className="font-semibold text-white">
|
||||
{tour.hotel_info}
|
||||
</p>
|
||||
@@ -385,7 +394,9 @@ export default function TourDetailPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<Utensils className="w-5 h-5 text-green-400 mt-1" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Ovqatlanish</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
{t("Ovqatlanish")}
|
||||
</p>
|
||||
<p className="font-semibold text-white">
|
||||
{tour.hotel_meals}
|
||||
</p>
|
||||
@@ -395,7 +406,7 @@ export default function TourDetailPage() {
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle2 className="w-5 h-5 text-blue-400 mt-1" />
|
||||
<div>
|
||||
<p className="text-sm text-gray-400">Tarif</p>
|
||||
<p className="text-sm text-gray-400">{t("Tarif")}</p>
|
||||
<p className="font-semibold text-white capitalize">
|
||||
{tour.tariff[0]?.name}
|
||||
</p>
|
||||
@@ -406,7 +417,7 @@ export default function TourDetailPage() {
|
||||
|
||||
<div className="pt-6 border-t border-gray-700">
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">
|
||||
Qulayliklar
|
||||
{t("Qulayliklar")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4">
|
||||
{tour.ticket_amenities.map((amenity, idx) => (
|
||||
@@ -430,7 +441,7 @@ export default function TourDetailPage() {
|
||||
<Card className="border-gray-700 shadow-lg bg-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl text-white">
|
||||
Sayohat marshshruti
|
||||
{t("Sayohat marshshruti")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@@ -444,7 +455,7 @@ export default function TourDetailPage() {
|
||||
variant="outline"
|
||||
className="text-base border-gray-600 text-gray-300"
|
||||
>
|
||||
{day.duration}-kun
|
||||
{day.duration}-{t("kun")}
|
||||
</Badge>
|
||||
<h3 className="text-xl font-semibold text-white">
|
||||
{day.title}
|
||||
@@ -492,7 +503,7 @@ export default function TourDetailPage() {
|
||||
<Card className="border-gray-700 shadow-lg bg-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl text-white">
|
||||
Narxga kiritilgan xizmatlar
|
||||
{t("Narxga kiritilgan xizmatlar")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -526,7 +537,7 @@ export default function TourDetailPage() {
|
||||
<Card className="border-gray-700 shadow-lg bg-gray-800">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl text-white">
|
||||
Mehmonxona va ovqatlanish
|
||||
{t("Mehmonxona va ovqatlanish")}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
@@ -542,7 +553,7 @@ export default function TourDetailPage() {
|
||||
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold mb-4 text-white">
|
||||
Ovqatlanish tafsilotlari
|
||||
{t("Ovqatlanish tafsilotlari")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{tour.ticket_hotel_meals.map((meal, idx) => (
|
||||
@@ -576,7 +587,7 @@ export default function TourDetailPage() {
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-2xl text-white">
|
||||
Mijozlar sharhlari
|
||||
{t("Mijozlar sharhlari")}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
@@ -586,7 +597,7 @@ export default function TourDetailPage() {
|
||||
{tour.rating}
|
||||
</span>
|
||||
<span className="text-gray-400">
|
||||
({tour.ticket_comments.length} sharh)
|
||||
({tour.ticket_comments.length} {t("sharh")})
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -621,9 +632,9 @@ export default function TourDetailPage() {
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-gray-400 mb-1">Tur firmasi</p>
|
||||
<p className="text-sm text-gray-400 mb-1">{t("Tur firmasi")}</p>
|
||||
<p className="text-xl font-semibold text-white">
|
||||
Firma ID: {tour.travel_agency_id}
|
||||
{t("Firma ID")}: {tour.travel_agency_id}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
@@ -631,7 +642,7 @@ export default function TourDetailPage() {
|
||||
onClick={() => router(`/agencies/${tour.travel_agency_id}`)}
|
||||
className="border-gray-700 bg-gray-800 hover:bg-gray-700 text-gray-300"
|
||||
>
|
||||
Firma sahifasiga o'tish
|
||||
{t("Firma sahifasiga o'tish")}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { deleteTours, getAllTours } from "@/pages/tours/lib/api";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -16,65 +18,79 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { Edit, Plane, PlusCircle, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Edit,
|
||||
Loader2,
|
||||
Plane,
|
||||
PlusCircle,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
type Tour = {
|
||||
id: number;
|
||||
image?: string;
|
||||
tickets: string;
|
||||
min_price: string;
|
||||
max_price: string;
|
||||
top_duration: string;
|
||||
top_destinations: string;
|
||||
hotel_features_by_type: string;
|
||||
hotel_types: string;
|
||||
hotel_amenities: string;
|
||||
};
|
||||
|
||||
const Tours = () => {
|
||||
const [tours, setTours] = useState<Tour[]>([]);
|
||||
const { t } = useTranslation();
|
||||
const [page, setPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(3);
|
||||
const [deleteId, setDeleteId] = useState<number | null>(null);
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useEffect(() => {
|
||||
const mockData: Tour[] = Array.from({ length: 10 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
image: `/dubai-marina.jpg`,
|
||||
tickets: `Bilet turi ${i + 1}`,
|
||||
min_price: `${200 + i * 50}$`,
|
||||
max_price: `${400 + i * 70}$`,
|
||||
top_duration: `${3 + i} kun`,
|
||||
top_destinations: `Shahar ${i + 1}`,
|
||||
hotel_features_by_type: "Spa, Wi-Fi, Pool",
|
||||
hotel_types: "5 yulduzli mehmonxona",
|
||||
hotel_amenities: "Nonushta, Parking, Bar",
|
||||
}));
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ["all_tours", page],
|
||||
queryFn: () => getAllTours({ page: page, page_size: 10 }),
|
||||
});
|
||||
|
||||
const itemsPerPage = 6;
|
||||
const start = (page - 1) * itemsPerPage;
|
||||
const end = start + itemsPerPage;
|
||||
|
||||
setTotalPages(Math.ceil(mockData.length / itemsPerPage));
|
||||
setTours(mockData.slice(start, end));
|
||||
}, [page]);
|
||||
|
||||
const confirmDelete = () => {
|
||||
if (deleteId !== null) {
|
||||
setTours((prev) => prev.filter((t) => t.id !== deleteId));
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: (id: number) => deleteTours({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["all_tours"] });
|
||||
setDeleteId(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const confirmDelete = (id: number) => {
|
||||
mutate(id);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
|
||||
<p className="text-slate-400">{t("Ma'lumotlar yuklanmoqda...")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 w-full text-center text-white gap-4">
|
||||
<AlertTriangle className="w-10 h-10 text-red-500" />
|
||||
<p className="text-lg">
|
||||
{t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
refetch();
|
||||
}}
|
||||
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg px-5 py-2 hover:opacity-90"
|
||||
>
|
||||
{t("Qayta urinish")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 text-foreground py-10 px-5 w-full">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h1 className="text-3xl font-semibold">Turlar ro'yxati</h1>
|
||||
<h1 className="text-3xl font-semibold">{t("Turlar ro'yxati")}</h1>
|
||||
<Button onClick={() => navigate("/tours/create")} variant="default">
|
||||
<PlusCircle className="w-5 h-5 mr-2" /> Yangi tur qo'shish
|
||||
<PlusCircle className="w-5 h-5 mr-2" /> {t("Yangi tur qo'shish")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -83,18 +99,19 @@ const Tours = () => {
|
||||
<TableHeader>
|
||||
<TableRow className="bg-muted/50">
|
||||
<TableHead className="w-[50px] text-center">#</TableHead>
|
||||
<TableHead className="min-w-[150px]">Manzil</TableHead>
|
||||
<TableHead className="min-w-[120px]">Davomiyligi</TableHead>
|
||||
<TableHead className="min-w-[180px]">Mehmonxona</TableHead>
|
||||
<TableHead className="min-w-[200px]">Narx Oralig'i</TableHead>
|
||||
<TableHead className="min-w-[200px]">Imkoniyatlar</TableHead>
|
||||
<TableHead className="min-w-[150px]">{t("Manzil")}</TableHead>
|
||||
<TableHead className="min-w-[120px]">
|
||||
{t("Davomiyligi")}
|
||||
</TableHead>
|
||||
<TableHead className="min-w-[180px]">{t("Mehmonxona")}</TableHead>
|
||||
<TableHead className="min-w-[200px]">{t("Narxi")}</TableHead>
|
||||
<TableHead className="min-w-[150px] text-center">
|
||||
Amallar
|
||||
{t("Операции")}
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{tours.map((tour, idx) => (
|
||||
{data?.data.data.results.map((tour, idx) => (
|
||||
<TableRow key={tour.id}>
|
||||
<TableCell className="font-medium text-center">
|
||||
{(page - 1) * 6 + idx + 1}
|
||||
@@ -102,28 +119,25 @@ const Tours = () => {
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2 font-semibold">
|
||||
<Plane className="w-4 h-4 text-primary" />
|
||||
{tour.top_destinations}
|
||||
{tour.destination}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-primary font-medium">
|
||||
{tour.top_duration}
|
||||
{tour.duration_days} kun
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col">
|
||||
<span className="font-medium">{tour.hotel_types}</span>
|
||||
<span className="font-medium">{tour.hotel_name}</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{tour.tickets}
|
||||
{tour.hotel_rating} {t("yulduzli mehmonxona")}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<span className="font-bold text-base text-green-600">
|
||||
{tour.min_price} – {tour.max_price}
|
||||
{formatPrice(tour.price, true)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm">
|
||||
{tour.hotel_amenities}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-center">
|
||||
<div className="flex gap-2 justify-center">
|
||||
@@ -148,7 +162,7 @@ const Tours = () => {
|
||||
size="sm"
|
||||
onClick={() => navigate(`/tours/${tour.id}`)}
|
||||
>
|
||||
Batafsil
|
||||
{t("Batafsil")}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -158,50 +172,67 @@ const Tours = () => {
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Delete Confirmation Dialog - Faqat bitta */}
|
||||
<Dialog open={deleteId !== null} onOpenChange={() => setDeleteId(null)}>
|
||||
<DialogContent className="sm:max-w-[425px] bg-gray-900">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
Turni o'chirishni tasdiqlang
|
||||
{t("Turni o'chirishni tasdiqlang")}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-4">
|
||||
<p className="text-muted-foreground">
|
||||
Haqiqatan ham bu turni o'chirib tashlamoqchimisiz? Bu amalni ortga
|
||||
qaytarib bo'lmaydi.
|
||||
{t(
|
||||
"Haqiqatan ham bu turni o'chirib tashlamoqchimisiz? Bu amalni ortga qaytarib bo'lmaydi.",
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<DialogFooter className="gap-4 flex">
|
||||
<Button variant="outline" onClick={() => setDeleteId(null)}>
|
||||
Bekor qilish
|
||||
{t("Bekor qilish")}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={confirmDelete}>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => confirmDelete(deleteId!)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
O'chirish
|
||||
{t("O'chirish")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<div className="flex justify-center mt-10 gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
<div className="flex justify-end mt-10 gap-3">
|
||||
<button
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage((p) => Math.max(p - 1, 1))}
|
||||
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
Oldingi
|
||||
</Button>
|
||||
<span className="text-sm flex items-center">
|
||||
Sahifa {page} / {totalPages}
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={page === totalPages}
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
</button>
|
||||
{[...Array(data?.data.data.total_pages)].map((_, i) => (
|
||||
<button
|
||||
key={i}
|
||||
onClick={() => setPage(i + 1)}
|
||||
className={`px-4 py-2 rounded-lg border transition-all font-medium ${
|
||||
page === i + 1
|
||||
? "bg-gradient-to-r from-blue-600 to-cyan-600 border-blue-500 text-white shadow-lg shadow-cyan-500/50"
|
||||
: "border-slate-600 text-slate-300 hover:bg-slate-700/50 hover:border-slate-500"
|
||||
}`}
|
||||
>
|
||||
{i + 1}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
disabled={page === data?.data.data.total_pages}
|
||||
onClick={() =>
|
||||
setPage((p) =>
|
||||
Math.min(p + 1, data ? data.data.data.total_pages : 1),
|
||||
)
|
||||
}
|
||||
className="p-2 rounded-lg border border-slate-600 text-slate-300 hover:bg-slate-700/50 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
Keyingi
|
||||
</Button>
|
||||
<ChevronRight className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,458 +1,241 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
hotelBadge,
|
||||
hotelFeature,
|
||||
hotelFeatureType,
|
||||
hotelTarif,
|
||||
hotelTransport,
|
||||
hotelType,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import BadgeTable from "@/pages/tours/ui/BadgeTable";
|
||||
import FeaturesTable from "@/pages/tours/ui/FeaturesTable";
|
||||
import FeaturesTableType from "@/pages/tours/ui/FeaturesTableType";
|
||||
import MealTable from "@/pages/tours/ui/MealTable";
|
||||
import TarifTable from "@/pages/tours/ui/TarifTable";
|
||||
import TransportTable from "@/pages/tours/ui/TransportTable";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent } from "@/shared/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { Edit2, Plus, Search, Trash2 } from "lucide-react";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { AlertTriangle, Loader2 } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
interface Badge {
|
||||
id: number;
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface Tariff {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface Transport {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
}
|
||||
|
||||
interface MealPlan {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface HotelType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
type TabId = "badges" | "tariffs" | "transports" | "mealPlans" | "hotelTypes";
|
||||
|
||||
type DataItem = Badge | Tariff | Transport | MealPlan | HotelType;
|
||||
|
||||
interface FormField {
|
||||
name: string;
|
||||
label: string;
|
||||
type: "text" | "number" | "color" | "textarea" | "select";
|
||||
required: boolean;
|
||||
options?: string[];
|
||||
min?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
interface Tab {
|
||||
id: TabId;
|
||||
label: string;
|
||||
}
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
const ToursSetting: React.FC = () => {
|
||||
const [activeTab, setActiveTab] = useState<TabId>("badges");
|
||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
|
||||
const [modalMode, setModalMode] = useState<"add" | "edit">("add");
|
||||
const [currentItem, setCurrentItem] = useState<DataItem | null>(null);
|
||||
const { t } = useTranslation();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [activeTab, setActiveTab] = useState("badge");
|
||||
const [featureId, setFeatureId] = useState<number | null>(null);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [badges, setBadges] = useState<Badge[]>([
|
||||
{ id: 1, name: "Bestseller", color: "#FFD700" },
|
||||
{ id: 2, name: "Yangi", color: "#4CAF50" },
|
||||
]);
|
||||
const page = parseInt(searchParams.get("page") || "1", 10);
|
||||
const pageSize = parseInt(searchParams.get("pageSize") || "10", 10);
|
||||
|
||||
const [tariffs, setTariffs] = useState<Tariff[]>([
|
||||
{ id: 1, name: "Standart", price: 500 },
|
||||
{ id: 2, name: "Premium", price: 1000 },
|
||||
]);
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ["all_badge", page, pageSize],
|
||||
queryFn: () => hotelBadge({ page, page_size: pageSize }),
|
||||
select: (res) => res.data.data,
|
||||
});
|
||||
|
||||
const [transports, setTransports] = useState<Transport[]>([
|
||||
{ id: 1, name: "Avtobus", price: 200 },
|
||||
{ id: 2, name: "Minivan", price: 500 },
|
||||
]);
|
||||
const pageTarif = parseInt(searchParams.get("pageTarif") || "1", 10);
|
||||
const pageSizeTarif = parseInt(searchParams.get("pageTarifSize") || "10", 10);
|
||||
|
||||
const [mealPlans, setMealPlans] = useState<MealPlan[]>([
|
||||
{ id: 1, name: "BB (Bed & Breakfast)" },
|
||||
{ id: 2, name: "HB (Half Board)" },
|
||||
{ id: 3, name: "FB (Full Board)" },
|
||||
]);
|
||||
const {
|
||||
data: tarifData,
|
||||
isLoading: tarifLoad,
|
||||
isError: tarifError,
|
||||
refetch: tarifRef,
|
||||
} = useQuery({
|
||||
queryKey: ["all_tarif", pageTarif, pageSizeTarif],
|
||||
queryFn: () => hotelTarif({ page: pageTarif, page_size: pageSizeTarif }),
|
||||
select: (res) => res.data.data,
|
||||
});
|
||||
|
||||
const [hotelTypes, setHotelTypes] = useState<HotelType[]>([
|
||||
{ id: 1, name: "3 Yulduz" },
|
||||
{ id: 2, name: "5 Yulduz" },
|
||||
]);
|
||||
|
||||
const [formData, setFormData] = useState<Partial<DataItem>>({});
|
||||
|
||||
const getCurrentData = (): DataItem[] => {
|
||||
switch (activeTab) {
|
||||
case "badges":
|
||||
return badges;
|
||||
case "tariffs":
|
||||
return tariffs;
|
||||
case "transports":
|
||||
return transports;
|
||||
case "mealPlans":
|
||||
return mealPlans;
|
||||
case "hotelTypes":
|
||||
return hotelTypes;
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getSetterFunction = (): React.Dispatch<
|
||||
React.SetStateAction<DataItem[]>
|
||||
> => {
|
||||
switch (activeTab) {
|
||||
case "badges":
|
||||
return setBadges as React.Dispatch<React.SetStateAction<DataItem[]>>;
|
||||
case "tariffs":
|
||||
return setTariffs as React.Dispatch<React.SetStateAction<DataItem[]>>;
|
||||
case "transports":
|
||||
return setTransports as React.Dispatch<
|
||||
React.SetStateAction<DataItem[]>
|
||||
>;
|
||||
case "mealPlans":
|
||||
return setMealPlans as React.Dispatch<React.SetStateAction<DataItem[]>>;
|
||||
case "hotelTypes":
|
||||
return setHotelTypes as React.Dispatch<
|
||||
React.SetStateAction<DataItem[]>
|
||||
>;
|
||||
default:
|
||||
return (() => {}) as React.Dispatch<React.SetStateAction<DataItem[]>>;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredData = getCurrentData().filter((item) =>
|
||||
Object.values(item).some((val) =>
|
||||
val?.toString().toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
),
|
||||
const pageTransport = parseInt(searchParams.get("pageTransport") || "1", 10);
|
||||
const pageSizeTransport = parseInt(
|
||||
searchParams.get("pageTransportSize") || "10",
|
||||
10,
|
||||
);
|
||||
|
||||
const getFormFields = (): FormField[] => {
|
||||
switch (activeTab) {
|
||||
case "badges":
|
||||
return [
|
||||
{ name: "name", label: "Nomi", type: "text", required: true },
|
||||
{ name: "color", label: "Rang", type: "color", required: true },
|
||||
];
|
||||
case "tariffs":
|
||||
return [
|
||||
{ name: "name", label: "Tarif nomi", type: "text", required: true },
|
||||
{ name: "price", label: "Narx", type: "number", required: true },
|
||||
];
|
||||
case "transports":
|
||||
return [
|
||||
{
|
||||
name: "name",
|
||||
label: "Transport nomi",
|
||||
type: "text",
|
||||
required: true,
|
||||
},
|
||||
{ name: "capacity", label: "Sig'im", type: "number", required: true },
|
||||
];
|
||||
case "mealPlans":
|
||||
return [{ name: "name", label: "Nomi", type: "text", required: true }];
|
||||
case "hotelTypes":
|
||||
return [
|
||||
{ name: "name", label: "Tur nomi", type: "text", required: true },
|
||||
];
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
const {
|
||||
data: transportData,
|
||||
isLoading: transportLoad,
|
||||
isError: transportError,
|
||||
refetch: transportRef,
|
||||
} = useQuery({
|
||||
queryKey: ["all_transport", pageTransport, pageSizeTransport],
|
||||
queryFn: () =>
|
||||
hotelTransport({ page: pageTransport, page_size: pageSizeTransport }),
|
||||
select: (res) => res.data.data,
|
||||
});
|
||||
|
||||
const openModal = (
|
||||
mode: "add" | "edit",
|
||||
item: DataItem | null = null,
|
||||
): void => {
|
||||
setModalMode(mode);
|
||||
setCurrentItem(item);
|
||||
if (mode === "edit" && item) {
|
||||
setFormData(item);
|
||||
} else {
|
||||
setFormData({});
|
||||
}
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
const pageType = parseInt(searchParams.get("pageType") || "1", 10);
|
||||
const pageSizeType = parseInt(searchParams.get("pageTypeSize") || "10", 10);
|
||||
|
||||
const closeModal = (): void => {
|
||||
setIsModalOpen(false);
|
||||
setFormData({});
|
||||
setCurrentItem(null);
|
||||
};
|
||||
const {
|
||||
data: typeData,
|
||||
isLoading: typeLoad,
|
||||
isError: typeError,
|
||||
refetch: typeRef,
|
||||
} = useQuery({
|
||||
queryKey: ["all_type", pageType, pageSizeType],
|
||||
queryFn: () => hotelType({ page: pageType, page_size: pageSizeType }),
|
||||
select: (res) => res.data.data,
|
||||
});
|
||||
|
||||
const handleSubmit = (): void => {
|
||||
const setter = getSetterFunction();
|
||||
const pageFeature = parseInt(searchParams.get("pageFeature") || "1", 10);
|
||||
const pageSizeFeature = parseInt(
|
||||
searchParams.get("pageSizeFeature") || "10",
|
||||
10,
|
||||
);
|
||||
|
||||
if (modalMode === "add") {
|
||||
const newId = Math.max(...getCurrentData().map((i) => i.id), 0) + 1;
|
||||
setter([...getCurrentData(), { ...formData, id: newId } as DataItem]);
|
||||
} else {
|
||||
setter(
|
||||
getCurrentData().map((item) =>
|
||||
item.id === currentItem?.id ? { ...item, ...formData } : item,
|
||||
),
|
||||
);
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
const {
|
||||
data: featureData,
|
||||
isLoading: featureLoad,
|
||||
isError: featureError,
|
||||
refetch: featureRef,
|
||||
} = useQuery({
|
||||
queryKey: ["all_feature", pageFeature, pageSizeFeature],
|
||||
queryFn: () =>
|
||||
hotelFeature({ page: pageFeature, page_size: pageSizeFeature }),
|
||||
select: (res) => res.data.data,
|
||||
});
|
||||
|
||||
const handleDelete = (id: number): void => {
|
||||
if (window.confirm("Rostdan ham o'chirmoqchimisiz?")) {
|
||||
const setter = getSetterFunction();
|
||||
setter(getCurrentData().filter((item) => item.id !== id));
|
||||
}
|
||||
};
|
||||
const {
|
||||
data: featureTypeData,
|
||||
isLoading: featureTypeLoad,
|
||||
isError: featureTypeError,
|
||||
refetch: featureTypeRef,
|
||||
} = useQuery({
|
||||
queryKey: ["all_feature_type", pageFeature, pageSizeFeature, featureId],
|
||||
queryFn: () =>
|
||||
hotelFeatureType({
|
||||
page: pageFeature,
|
||||
page_size: pageSizeFeature,
|
||||
feature_type: featureId!,
|
||||
}),
|
||||
select: (res) => res.data.data,
|
||||
enabled: !!featureId,
|
||||
});
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{ id: "badges", label: "Belgilar" },
|
||||
{ id: "tariffs", label: "Tariflar" },
|
||||
{ id: "transports", label: "Transportlar" },
|
||||
{ id: "mealPlans", label: "Ovqatlanish" },
|
||||
{ id: "hotelTypes", label: "Otel turlari" },
|
||||
];
|
||||
if (
|
||||
isLoading ||
|
||||
tarifLoad ||
|
||||
transportLoad ||
|
||||
typeLoad ||
|
||||
featureLoad ||
|
||||
featureTypeLoad
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 text-white gap-4 w-full">
|
||||
<Loader2 className="w-10 h-10 animate-spin text-cyan-400" />
|
||||
<p className="text-slate-400">{t("Ma'lumotlar yuklanmoqda...")}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const getFieldValue = (fieldName: string): string | number => {
|
||||
return (formData as Record<string, string | number>)[fieldName] || "";
|
||||
if (
|
||||
isError ||
|
||||
tarifError ||
|
||||
transportError ||
|
||||
typeError ||
|
||||
featureError ||
|
||||
featureTypeError
|
||||
) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-screen bg-slate-900 w-full text-center text-white gap-4">
|
||||
<AlertTriangle className="w-10 h-10 text-red-500" />
|
||||
<p className="text-lg">
|
||||
{t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => {
|
||||
refetch();
|
||||
tarifRef();
|
||||
transportRef();
|
||||
typeRef();
|
||||
featureRef();
|
||||
featureTypeRef();
|
||||
}}
|
||||
className="bg-gradient-to-r from-blue-600 to-cyan-600 text-white rounded-lg px-5 py-2 hover:opacity-90"
|
||||
>
|
||||
{t("Qayta urinish")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
navigate({
|
||||
pathname: window.location.pathname,
|
||||
search: "",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-900 p-6 w-full">
|
||||
<div className="max-w-7xl mx-auto space-y-6">
|
||||
<h1 className="text-3xl font-bold">Tur Sozlamalari</h1>
|
||||
|
||||
<div className="min-h-screen bg-gray-900 p-6 w-full text-white">
|
||||
<div className="max-w-[90%] mx-auto space-y-6">
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onValueChange={(v) => {
|
||||
setActiveTab(v as TabId);
|
||||
setSearchTerm("");
|
||||
}}
|
||||
onValueChange={handleTabChange}
|
||||
className="w-full"
|
||||
>
|
||||
<TabsList className="grid w-full grid-cols-5">
|
||||
{tabs.map((tab) => (
|
||||
<TabsTrigger key={tab.id} value={tab.id}>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="badge">{t("Belgilar (Badge)")}</TabsTrigger>
|
||||
<TabsTrigger value="tarif">{t("Tariflar")}</TabsTrigger>
|
||||
<TabsTrigger value="transport">{t("Transport")}</TabsTrigger>
|
||||
{/* <TabsTrigger value="meal">{t("Ovqatlanish")}</TabsTrigger> */}
|
||||
<TabsTrigger value="hotel_type">{t("Otel turlari")}</TabsTrigger>
|
||||
<TabsTrigger value="hotel_features">
|
||||
{t("Otel sharoitlari")}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="badge" className="space-y-4">
|
||||
<BadgeTable data={data} page={page} pageSize={pageSize} />
|
||||
</TabsContent>
|
||||
<TabsContent value="tarif" className="space-y-4">
|
||||
<TarifTable
|
||||
data={tarifData}
|
||||
page={pageTarif}
|
||||
pageSize={pageSizeTarif}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="transport" className="space-y-4">
|
||||
<TransportTable
|
||||
data={transportData}
|
||||
page={pageTransport}
|
||||
pageSize={pageSizeTransport}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="hotel_type" className="space-y-4">
|
||||
<MealTable
|
||||
data={typeData}
|
||||
page={pageTransport}
|
||||
pageSize={pageSizeTransport}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="hotel_features" className="space-y-4">
|
||||
<FeaturesTable
|
||||
data={featureData}
|
||||
page={pageFeature}
|
||||
pageSize={pageSizeFeature}
|
||||
setActiveTab={setActiveTab}
|
||||
setFeatureId={setFeatureId}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="feature_type" className="space-y-4">
|
||||
<FeaturesTableType
|
||||
data={featureTypeData}
|
||||
page={pageFeature}
|
||||
featureId={featureId}
|
||||
pageSize={pageSizeFeature}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<Card className="bg-gray-900">
|
||||
<CardContent className="pt-6">
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-between items-center">
|
||||
<div className="relative w-full sm:w-96">
|
||||
<Search
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 text-muted-foreground"
|
||||
size={20}
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Qidirish..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => openModal("add")}
|
||||
className="w-full sm:w-auto cursor-pointer"
|
||||
>
|
||||
<Plus size={20} className="mr-2" />
|
||||
Yangi qo'shish
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="bg-gray-900">
|
||||
<div className="overflow-x-auto">
|
||||
<div className="min-w-full">
|
||||
<div className="border-b">
|
||||
<div className="flex">
|
||||
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-20">
|
||||
ID
|
||||
</div>
|
||||
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider flex-1">
|
||||
Nomi
|
||||
</div>
|
||||
{activeTab === "badges" && (
|
||||
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-48">
|
||||
Rang
|
||||
</div>
|
||||
)}
|
||||
{(activeTab === "tariffs" || activeTab === "transports") && (
|
||||
<div className="px-6 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
|
||||
Narx
|
||||
</div>
|
||||
)}
|
||||
<div className="px-6 py-3 text-right text-xs font-medium text-muted-foreground uppercase tracking-wider w-32">
|
||||
Amallar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{filteredData.length === 0 ? (
|
||||
<div className="px-6 py-8 text-center text-muted-foreground">
|
||||
Ma'lumot topilmadi
|
||||
</div>
|
||||
) : (
|
||||
filteredData.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center hover:bg-accent/50 transition-colors"
|
||||
>
|
||||
<div className="px-6 py-4 w-20">{item.id}</div>
|
||||
<div className="px-6 py-4 font-medium flex-1">
|
||||
{item.name}
|
||||
</div>
|
||||
{activeTab === "badges" && (
|
||||
<div className="px-6 py-4 w-48">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-6 h-6 rounded border"
|
||||
style={{ backgroundColor: (item as Badge).color }}
|
||||
/>
|
||||
<span>{(item as Badge).color}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{(activeTab === "tariffs" ||
|
||||
activeTab === "transports") && (
|
||||
<div className="px-6 py-4 w-32">
|
||||
{(item as Tariff).price}
|
||||
</div>
|
||||
)}
|
||||
<div className="px-6 py-4 w-32">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => openModal("edit", item)}
|
||||
>
|
||||
<Edit2 size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} className="text-destructive" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Dialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
||||
<DialogContent className="sm:max-w-[500px] bg-gray-900">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{modalMode === "add" ? "Yangi qo'shish" : "Tahrirlash"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
{getFormFields().map((field) => (
|
||||
<div key={field.name} className="space-y-2">
|
||||
<Label htmlFor={field.name}>
|
||||
{field.label}
|
||||
{field.required && (
|
||||
<span className="text-destructive">*</span>
|
||||
)}
|
||||
</Label>
|
||||
{field.type === "textarea" ? (
|
||||
<Textarea
|
||||
id={field.name}
|
||||
value={getFieldValue(field.name) as string}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
[field.name]: e.target.value,
|
||||
})
|
||||
}
|
||||
required={field.required}
|
||||
rows={3}
|
||||
/>
|
||||
) : field.type === "select" ? (
|
||||
<Select
|
||||
value={getFieldValue(field.name) as string}
|
||||
onValueChange={(value) =>
|
||||
setFormData({ ...formData, [field.name]: value })
|
||||
}
|
||||
required={field.required}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Tanlang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{field.options?.map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>
|
||||
{opt}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
id={field.name}
|
||||
type={field.type}
|
||||
value={getFieldValue(field.name)}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
[field.name]:
|
||||
field.type === "number"
|
||||
? Number(e.target.value)
|
||||
: e.target.value,
|
||||
})
|
||||
}
|
||||
required={field.required}
|
||||
min={field.min}
|
||||
max={field.max}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={closeModal}
|
||||
className="flex-1"
|
||||
>
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button type="button" onClick={handleSubmit} className="flex-1">
|
||||
Saqlash
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
334
src/pages/tours/ui/TransportTable.tsx
Normal file
334
src/pages/tours/ui/TransportTable.tsx
Normal file
@@ -0,0 +1,334 @@
|
||||
import {
|
||||
hotelTranportCreate,
|
||||
hotelTransportDelete,
|
||||
hotelTransportDetail,
|
||||
hotelTransportUpdate,
|
||||
} from "@/pages/tours/lib/api";
|
||||
import { TranportColumns } from "@/pages/tours/lib/column";
|
||||
import type { Transport } 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 IconSelect from "@/shared/ui/iocnSelect";
|
||||
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" }),
|
||||
icon_name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
|
||||
const TransportTable = ({
|
||||
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: Transport[];
|
||||
}
|
||||
| undefined;
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [editId, setEditId] = useState<number | null>(null);
|
||||
const [types, setTypes] = useState<"edit" | "create">("create");
|
||||
const [selectedIcon, setSelectedIcon] = useState("Bus");
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
name: "",
|
||||
name_ru: "",
|
||||
icon_name: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
form.setValue("icon_name", selectedIcon);
|
||||
}, [selectedIcon]);
|
||||
|
||||
const handleEdit = (id: number) => {
|
||||
setTypes("edit");
|
||||
setOpen(true);
|
||||
setEditId(id);
|
||||
};
|
||||
|
||||
const { data: transportDetail } = useQuery({
|
||||
queryKey: ["detail_transport", editId],
|
||||
queryFn: () => hotelTransportDetail({ id: editId! }),
|
||||
enabled: !!editId,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (transportDetail) {
|
||||
form.setValue("name", transportDetail.data.data.name);
|
||||
form.setValue("name_ru", transportDetail.data.data.name_ru);
|
||||
form.setValue("icon_name", transportDetail.data.data.icon_name);
|
||||
setSelectedIcon(transportDetail.data.data.icon_name);
|
||||
}
|
||||
}, [transportDetail, editId, form]);
|
||||
|
||||
const { mutate: deleteMutate } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => hotelTransportDelete({ id }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["all_transport"] });
|
||||
toast.success(t("O‘chirildi"), { position: "top-center" });
|
||||
},
|
||||
onError: () =>
|
||||
toast.error(t("Xatolik yuz berdi"), { position: "top-center" }),
|
||||
});
|
||||
|
||||
const { mutate: create, isPending } = useMutation({
|
||||
mutationFn: ({
|
||||
body,
|
||||
}: {
|
||||
body: { name: string; name_ru: string; icon_name: string };
|
||||
}) => hotelTranportCreate({ body }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["all_transport"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
toast.success(t("Muvaffaqiyatli qo‘shildi"), { position: "top-center" });
|
||||
},
|
||||
onError: () =>
|
||||
toast.error(t("Xatolik yuz berdi"), { position: "top-center" }),
|
||||
});
|
||||
|
||||
const { mutate: update, isPending: updatePending } = useMutation({
|
||||
mutationFn: ({
|
||||
body,
|
||||
id,
|
||||
}: {
|
||||
id: number;
|
||||
body: { name: string; name_ru: string; icon_name: string };
|
||||
}) => hotelTransportUpdate({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["all_transport"] });
|
||||
setOpen(false);
|
||||
form.reset();
|
||||
toast.success(t("Tahrirlandi"), { position: "top-center" });
|
||||
},
|
||||
onError: () =>
|
||||
toast.error(t("Xatolik yuz berdi"), { position: "top-center" }),
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof formSchema>) {
|
||||
const body = {
|
||||
name: values.name,
|
||||
name_ru: values.name_ru,
|
||||
icon_name: selectedIcon || values.icon_name,
|
||||
};
|
||||
|
||||
if (types === "create") create({ body });
|
||||
if (types === "edit" && editId) update({ id: editId, body });
|
||||
}
|
||||
|
||||
const handleDelete = (id: number) => deleteMutate({ id });
|
||||
|
||||
const columns = TranportColumns(handleEdit, handleDelete, t);
|
||||
|
||||
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();
|
||||
setSelectedIcon("");
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="mr-2" />
|
||||
{t("Qo‘shish")}
|
||||
</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="pageTransport"
|
||||
namePageSize="pageTransportSize"
|
||||
/>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<p className="text-xl font-semibold mb-4">
|
||||
{types === "create"
|
||||
? t("Yangi transport qo‘shish")
|
||||
: t("Tahrirlash")}
|
||||
</p>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-6 p-2"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Nomi (uz)")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("Nomi (uz)")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name_ru"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Nomi (ru)")}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t("Nomi (ru)")} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="icon_name"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("Belgi (Icon)")}</FormLabel>
|
||||
<FormControl className="w-full">
|
||||
<IconSelect
|
||||
setSelectedIcon={setSelectedIcon}
|
||||
selectedIcon={selectedIcon}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-4">
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="border-slate-600 text-white hover:bg-gray-600 bg-gray-600"
|
||||
>
|
||||
{t("Bekor qilish")}
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending || updatePending}>
|
||||
{isPending || updatePending ? (
|
||||
<Loader className="animate-spin" />
|
||||
) : types === "create" ? (
|
||||
t("Saqlash")
|
||||
) : (
|
||||
t("Tahrirlash")
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransportTable;
|
||||
Reference in New Issue
Block a user