api ulandi

This commit is contained in:
Samandar Turgunboyev
2025-10-25 18:42:01 +05:00
parent 1a08775451
commit 05b752daf2
84 changed files with 11179 additions and 3724 deletions

View 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("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination table={table} totalPages={data?.total_pages} />
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<p className="text-xl">
{types === "create" ? t("Saqlash") : t("Tahrirlash")}
</p>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 p-2"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Nomi")}</FormLabel>
<FormControl>
<Input placeholder={t("Nomi")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name_ru"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Nomi (ru)")}</FormLabel>
<FormControl>
<Input placeholder={t("Nomi (ru)")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<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;

View File

@@ -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>
);
};

View 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("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination
table={table}
totalPages={data?.total_pages}
namePage="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;

View 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("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination
table={table}
totalPages={data?.total_pages}
namePage="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;

View File

@@ -0,0 +1,324 @@
import {
hotelTypeCreate,
hotelTypeDelete,
hotelTypeDetail,
hotelTypeUpdate,
} from "@/pages/tours/lib/api";
import { TypeColumns } from "@/pages/tours/lib/column";
import type { Type } from "@/pages/tours/lib/type";
import { Button } from "@/shared/ui/button";
import { Dialog, DialogContent } from "@/shared/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import RealPagination from "@/widgets/real-pagination/ui/RealPagination";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import {
flexRender,
getCoreRowModel,
useReactTable,
} from "@tanstack/react-table";
import { Loader, PlusIcon } from "lucide-react";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import z from "zod";
const formSchema = z.object({
name: z.string().min(1, { message: "Majburiy maydon" }),
name_ru: z.string().min(1, { message: "Majburiy maydon" }),
});
const 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("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination table={table} totalPages={data?.total_pages} />
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<p className="text-xl">
{types === "create" ? t("Saqlash") : t("Tahrirlash")}
</p>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="space-y-6 p-2"
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Nomi")}</FormLabel>
<FormControl>
<Input placeholder={t("Nomi")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="name_ru"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Nomi (ru)")}</FormLabel>
<FormControl>
<Input placeholder={t("Nomi (ru)")} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end gap-4">
<Button
type="button"
onClick={() => setOpen(false)}
className="border-slate-600 text-white hover:bg-gray-600 bg-gray-600"
>
{t("Bekor qilish")}
</Button>
<Button type="submit">
{isPending || updatePending ? (
<Loader className="animate-spin" />
) : types === "create" ? (
t("Saqlash")
) : (
t("Tahrirlash")
)}
</Button>
</div>
</form>
</Form>
</DialogContent>
</Dialog>
</>
);
};
export default MealTable;

File diff suppressed because it is too large Load Diff

View File

@@ -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 bolishi 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"
>
Qoshish
</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>
);

View 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("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination
table={table}
totalPages={data?.total_pages}
namePage="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;

View File

@@ -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) {
// ✅ Kop rasm holati
const currentFiles = form.getValues(name) || [];
const newFiles = currentFiles.filter(
(_: File, idx: number) => idx !== i,
);
form.setValue(name, newFiles);
const newPreviews = previews.filter(
(_: string, idx: number) => idx !== i,
);
setPreviews(newPreviews);
} else {
// ✅ Bitta rasm holati
form.setValue(name, null);
setPreviews([]);
}
}}
className="absolute top-1 right-1 bg-white/80 rounded-full p-1 shadow hover:bg-white"
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>
)}

View File

@@ -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>

View File

@@ -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>
);

View File

@@ -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>
);

View 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("Ochirildi"), { 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 qoshildi"), { 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("Qoshish")}
</Button>
</div>
<div className="border border-gray-700 rounded-md overflow-hidden mt-4">
<Table key={data?.current_page}>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-gray-400"
>
{t("Ma'lumot topilmadi")}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<RealPagination
table={table}
totalPages={data?.total_pages}
namePage="pageTransport"
namePageSize="pageTransportSize"
/>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<p className="text-xl font-semibold mb-4">
{types === "create"
? t("Yangi transport qoshish")
: 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;