-
Jo'nash sanasi
+
{t("Jo'nash sanasi")}
{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")}
- Marshshrut
+ {t("Marshshrut")}
- Xizmatlar
+ {t("Xizmatlar")}
- Mehmonxona
+ {t("Mehmonxona")}
- Sharhlar
+ {t("Sharhlar")}
@@ -334,7 +337,7 @@ export default function TourDetailPage() {
- Tur haqida ma'lumot
+ {t("Tur haqida ma'lumot")}
@@ -343,7 +346,9 @@ export default function TourDetailPage() {
-
Jo'nash joyi
+
+ {t("Jo'nash joyi")}
+
{tour.departure}
@@ -353,7 +358,9 @@ export default function TourDetailPage() {
-
Yo'nalish
+
+ {t("Yo'nalish")}
+
{tour.destination}
@@ -363,7 +370,7 @@ export default function TourDetailPage() {
-
Tillar
+
{t("Tillar")}
{tour.languages}
@@ -375,7 +382,9 @@ export default function TourDetailPage() {
-
Mehmonxona
+
+ {t("Mehmonxona")}
+
{tour.hotel_info}
@@ -385,7 +394,9 @@ export default function TourDetailPage() {
-
Ovqatlanish
+
+ {t("Ovqatlanish")}
+
{tour.hotel_meals}
@@ -395,7 +406,7 @@ export default function TourDetailPage() {
-
Tarif
+
{t("Tarif")}
{tour.tariff[0]?.name}
@@ -406,7 +417,7 @@ export default function TourDetailPage() {
- Qulayliklar
+ {t("Qulayliklar")}
{tour.ticket_amenities.map((amenity, idx) => (
@@ -430,7 +441,7 @@ export default function TourDetailPage() {
- Sayohat marshshruti
+ {t("Sayohat marshshruti")}
@@ -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")}
{day.title}
@@ -492,7 +503,7 @@ export default function TourDetailPage() {
- Narxga kiritilgan xizmatlar
+ {t("Narxga kiritilgan xizmatlar")}
@@ -526,7 +537,7 @@ export default function TourDetailPage() {
- Mehmonxona va ovqatlanish
+ {t("Mehmonxona va ovqatlanish")}
@@ -542,7 +553,7 @@ export default function TourDetailPage() {
- Ovqatlanish tafsilotlari
+ {t("Ovqatlanish tafsilotlari")}
{tour.ticket_hotel_meals.map((meal, idx) => (
@@ -576,7 +587,7 @@ export default function TourDetailPage() {
- Mijozlar sharhlari
+ {t("Mijozlar sharhlari")}
@@ -586,7 +597,7 @@ export default function TourDetailPage() {
{tour.rating}
- ({tour.ticket_comments.length} sharh)
+ ({tour.ticket_comments.length} {t("sharh")})
@@ -621,9 +632,9 @@ export default function TourDetailPage() {
-
Tur firmasi
+
{t("Tur firmasi")}
- Firma ID: {tour.travel_agency_id}
+ {t("Firma ID")}: {tour.travel_agency_id}
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")}
diff --git a/src/pages/tours/ui/Tours.tsx b/src/pages/tours/ui/Tours.tsx
index 7571445..64806a9 100644
--- a/src/pages/tours/ui/Tours.tsx
+++ b/src/pages/tours/ui/Tours.tsx
@@ -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
([]);
+ const { t } = useTranslation();
const [page, setPage] = useState(1);
- const [totalPages, setTotalPages] = useState(3);
const [deleteId, setDeleteId] = useState(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 (
+
+
+
{t("Ma'lumotlar yuklanmoqda...")}
+
+ );
+ }
+
+ if (isError) {
+ return (
+
+
+
+ {t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
+
+
{
+ 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")}
+
+
+ );
+ }
+
return (
-
Turlar ro'yxati
+
{t("Turlar ro'yxati")}
navigate("/tours/create")} variant="default">
- Yangi tur qo'shish
+ {t("Yangi tur qo'shish")}
@@ -83,18 +99,19 @@ const Tours = () => {
#
- Manzil
- Davomiyligi
- Mehmonxona
- Narx Oralig'i
- Imkoniyatlar
+ {t("Manzil")}
+
+ {t("Davomiyligi")}
+
+ {t("Mehmonxona")}
+ {t("Narxi")}
- Amallar
+ {t("Операции")}
- {tours.map((tour, idx) => (
+ {data?.data.data.results.map((tour, idx) => (
{(page - 1) * 6 + idx + 1}
@@ -102,28 +119,25 @@ const Tours = () => {
- {tour.top_destinations}
+ {tour.destination}
- {tour.top_duration}
+ {tour.duration_days} kun
- {tour.hotel_types}
+ {tour.hotel_name}
- {tour.tickets}
+ {tour.hotel_rating} {t("yulduzli mehmonxona")}
- {tour.min_price} – {tour.max_price}
+ {formatPrice(tour.price, true)}
-
- {tour.hotel_amenities}
-
@@ -148,7 +162,7 @@ const Tours = () => {
size="sm"
onClick={() => navigate(`/tours/${tour.id}`)}
>
- Batafsil
+ {t("Batafsil")}
@@ -158,50 +172,67 @@ const Tours = () => {
- {/* Delete Confirmation Dialog - Faqat bitta */}
setDeleteId(null)}>
- Turni o'chirishni tasdiqlang
+ {t("Turni o'chirishni tasdiqlang")}
- 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.",
+ )}
setDeleteId(null)}>
- Bekor qilish
+ {t("Bekor qilish")}
-
+ confirmDelete(deleteId!)}
+ >
- O'chirish
+ {t("O'chirish")}
-
-
setPage((p) => Math.max(1, p - 1))}
+
+ 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
-
-
- Sahifa {page} / {totalPages}
-
- setPage((p) => Math.min(totalPages, p + 1))}
- disabled={page === totalPages}
+
+
+ {[...Array(data?.data.data.total_pages)].map((_, i) => (
+ 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}
+
+ ))}
+
+ 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
-
+
+
);
diff --git a/src/pages/tours/ui/ToursSetting.tsx b/src/pages/tours/ui/ToursSetting.tsx
index c0b187d..0294934 100644
--- a/src/pages/tours/ui/ToursSetting.tsx
+++ b/src/pages/tours/ui/ToursSetting.tsx
@@ -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("badges");
- const [searchTerm, setSearchTerm] = useState("");
- const [isModalOpen, setIsModalOpen] = useState(false);
- const [modalMode, setModalMode] = useState<"add" | "edit">("add");
- const [currentItem, setCurrentItem] = useState(null);
+ const { t } = useTranslation();
+ const [searchParams] = useSearchParams();
+ const [activeTab, setActiveTab] = useState("badge");
+ const [featureId, setFeatureId] = useState(null);
+ const navigate = useNavigate();
- const [badges, setBadges] = useState([
- { 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([
- { 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([
- { 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([
- { 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([
- { id: 1, name: "3 Yulduz" },
- { id: 2, name: "5 Yulduz" },
- ]);
-
- const [formData, setFormData] = useState>({});
-
- 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
- > => {
- switch (activeTab) {
- case "badges":
- return setBadges as React.Dispatch>;
- case "tariffs":
- return setTariffs as React.Dispatch>;
- case "transports":
- return setTransports as React.Dispatch<
- React.SetStateAction
- >;
- case "mealPlans":
- return setMealPlans as React.Dispatch>;
- case "hotelTypes":
- return setHotelTypes as React.Dispatch<
- React.SetStateAction
- >;
- default:
- return (() => {}) as React.Dispatch>;
- }
- };
-
- 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 (
+
+
+
{t("Ma'lumotlar yuklanmoqda...")}
+
+ );
+ }
- const getFieldValue = (fieldName: string): string | number => {
- return (formData as Record)[fieldName] || "";
+ if (
+ isError ||
+ tarifError ||
+ transportError ||
+ typeError ||
+ featureError ||
+ featureTypeError
+ ) {
+ return (
+
+
+
+ {t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
+
+
{
+ 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")}
+
+
+ );
+ }
+
+ const handleTabChange = (value: string) => {
+ setActiveTab(value);
+ navigate({
+ pathname: window.location.pathname,
+ search: "",
+ });
};
return (
-
-
-
Tur Sozlamalari
-
+
+
{
- setActiveTab(v as TabId);
- setSearchTerm("");
- }}
+ onValueChange={handleTabChange}
+ className="w-full"
>
-
- {tabs.map((tab) => (
-
- {tab.label}
-
- ))}
+
+ {t("Belgilar (Badge)")}
+ {t("Tariflar")}
+ {t("Transport")}
+ {/* {t("Ovqatlanish")} */}
+ {t("Otel turlari")}
+
+ {t("Otel sharoitlari")}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
- setSearchTerm(e.target.value)}
- className="pl-10"
- />
-
-
openModal("add")}
- className="w-full sm:w-auto cursor-pointer"
- >
-
- Yangi qo'shish
-
-
-
-
-
-
-
-
-
-
-
- ID
-
-
- Nomi
-
- {activeTab === "badges" && (
-
- Rang
-
- )}
- {(activeTab === "tariffs" || activeTab === "transports") && (
-
- Narx
-
- )}
-
- Amallar
-
-
-
-
- {filteredData.length === 0 ? (
-
- Ma'lumot topilmadi
-
- ) : (
- filteredData.map((item) => (
-
-
{item.id}
-
- {item.name}
-
- {activeTab === "badges" && (
-
-
-
-
{(item as Badge).color}
-
-
- )}
- {(activeTab === "tariffs" ||
- activeTab === "transports") && (
-
- {(item as Tariff).price}
-
- )}
-
-
- openModal("edit", item)}
- >
-
-
- handleDelete(item.id)}
- >
-
-
-
-
-
- ))
- )}
-
-
-
-
-
-
-
-
-
- {modalMode === "add" ? "Yangi qo'shish" : "Tahrirlash"}
-
-
-
- {getFormFields().map((field) => (
-
-
- {field.label}
- {field.required && (
- *
- )}
-
- {field.type === "textarea" ? (
-
- ))}
-
-
- Bekor qilish
-
-
- Saqlash
-
-
-
-
-
);
diff --git a/src/pages/tours/ui/TransportTable.tsx b/src/pages/tours/ui/TransportTable.tsx
new file mode 100644
index 0000000..1dc02bc
--- /dev/null
+++ b/src/pages/tours/ui/TransportTable.tsx
@@ -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
(null);
+ const [types, setTypes] = useState<"edit" | "create">("create");
+ const [selectedIcon, setSelectedIcon] = useState("Bus");
+ const queryClient = useQueryClient();
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ name: "",
+ name_ru: "",
+ icon_name: "",
+ },
+ });
+
+ useEffect(() => {
+ form.setValue("icon_name", selectedIcon);
+ }, [selectedIcon]);
+
+ const handleEdit = (id: number) => {
+ setTypes("edit");
+ setOpen(true);
+ setEditId(id);
+ };
+
+ const { data: transportDetail } = useQuery({
+ queryKey: ["detail_transport", editId],
+ queryFn: () => hotelTransportDetail({ id: editId! }),
+ enabled: !!editId,
+ });
+
+ useEffect(() => {
+ if (transportDetail) {
+ form.setValue("name", transportDetail.data.data.name);
+ form.setValue("name_ru", transportDetail.data.data.name_ru);
+ form.setValue("icon_name", transportDetail.data.data.icon_name);
+ setSelectedIcon(transportDetail.data.data.icon_name);
+ }
+ }, [transportDetail, editId, form]);
+
+ const { mutate: deleteMutate } = useMutation({
+ mutationFn: ({ id }: { id: number }) => hotelTransportDelete({ id }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["all_transport"] });
+ toast.success(t("O‘chirildi"), { position: "top-center" });
+ },
+ onError: () =>
+ toast.error(t("Xatolik yuz berdi"), { position: "top-center" }),
+ });
+
+ const { mutate: create, isPending } = useMutation({
+ mutationFn: ({
+ body,
+ }: {
+ body: { name: string; name_ru: string; icon_name: string };
+ }) => hotelTranportCreate({ body }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["all_transport"] });
+ setOpen(false);
+ form.reset();
+ toast.success(t("Muvaffaqiyatli qo‘shildi"), { position: "top-center" });
+ },
+ onError: () =>
+ toast.error(t("Xatolik yuz berdi"), { position: "top-center" }),
+ });
+
+ const { mutate: update, isPending: updatePending } = useMutation({
+ mutationFn: ({
+ body,
+ id,
+ }: {
+ id: number;
+ body: { name: string; name_ru: string; icon_name: string };
+ }) => hotelTransportUpdate({ body, id }),
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: ["all_transport"] });
+ setOpen(false);
+ form.reset();
+ toast.success(t("Tahrirlandi"), { position: "top-center" });
+ },
+ onError: () =>
+ toast.error(t("Xatolik yuz berdi"), { position: "top-center" }),
+ });
+
+ function onSubmit(values: z.infer) {
+ 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 (
+ <>
+
+
{
+ setOpen(true);
+ setTypes("create");
+ form.reset();
+ setSelectedIcon("");
+ }}
+ >
+
+ {t("Qo‘shish")}
+
+
+
+
+
+
+ {table.getHeaderGroups().map((headerGroup) => (
+
+ {headerGroup.headers.map((header) => (
+
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext(),
+ )}
+
+ ))}
+
+ ))}
+
+
+
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+
+ {row.getVisibleCells().map((cell) => (
+
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext(),
+ )}
+
+ ))}
+
+ ))
+ ) : (
+
+
+ {t("Ma'lumot topilmadi")}
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ {types === "create"
+ ? t("Yangi transport qo‘shish")
+ : t("Tahrirlash")}
+
+
+
+
+
+ >
+ );
+};
+
+export default TransportTable;
diff --git a/src/pages/users/Edit.tsx b/src/pages/users/Edit.tsx
deleted file mode 100644
index bf5eb08..0000000
--- a/src/pages/users/Edit.tsx
+++ /dev/null
@@ -1,220 +0,0 @@
-import formatPhone from "@/shared/lib/formatPhone";
-import { Button } from "@/shared/ui/button";
-import { Input } from "@/shared/ui/input";
-import { Label } from "@/shared/ui/label";
-import clsx from "clsx";
-import { ArrowLeft, Mail, Phone, Save, User } from "lucide-react";
-import React, { useEffect, useState } from "react";
-import { useNavigate, useParams } from "react-router-dom";
-
-export default function EditUser() {
- const navigate = useNavigate();
- const { id } = useParams();
-
- const [formData, setFormData] = useState({
- username: "",
- email: "",
- phone: "+998",
- });
-
- const [errors, setErrors] = useState>({});
-
- useEffect(() => {
- setFormData({
- username: "john_doe",
- email: "john@example.com",
- phone: "+998901234567",
- });
- }, [id]);
-
- const validateForm = () => {
- const newErrors: Record = {};
-
- if (!formData.username.trim()) {
- newErrors.username = "Username majburiy";
- } else if (formData.username.length < 3) {
- newErrors.username =
- "Username kamida 3 ta belgidan iborat bo'lishi kerak";
- }
-
- if (!formData.email.trim() && !formData.phone.trim()) {
- newErrors.contact = "Email yoki telefon raqami kiritilishi shart";
- }
-
- if (formData.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {
- newErrors.email = "Email formati noto'g'ri";
- }
-
- if (
- formData.phone &&
- !/^\+998\d{9}$/.test(formData.phone.replace(/\s/g, ""))
- ) {
- newErrors.phone = "Telefon raqami formati: +998901234567";
- }
-
- setErrors(newErrors);
- return Object.keys(newErrors).length === 0;
- };
-
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
-
- if (validateForm()) {
- navigate("/");
- }
- };
-
- return (
-
-
- {/* Header */}
-
-
navigate(-1)}
- className="mb-4 -ml-2 text-slate-400 cursor-pointer hover:text-slate-100 hover:bg-slate-800/50"
- >
-
- Orqaga
-
-
-
-
-
-
-
-
Tahrirlash
-
Ma'lumotlarni yangilang
-
-
-
-
- {/* Form Card */}
-
-
-
- );
-}
diff --git a/src/pages/users/User.tsx b/src/pages/users/User.tsx
deleted file mode 100644
index 4b728bd..0000000
--- a/src/pages/users/User.tsx
+++ /dev/null
@@ -1,398 +0,0 @@
-import { Button } from "@/shared/ui/button";
-import {
- AlertTriangle,
- Calendar,
- ChevronLeft,
- ChevronRight,
- Eye,
- Mail,
- Pencil,
- Phone,
- Plus,
- Search,
- Trash2,
- Users,
- X,
-} from "lucide-react";
-import { useState } from "react";
-import { useNavigate } from "react-router-dom";
-
-type User = {
- id: number;
- username: string;
- email?: string;
- phone?: string;
- createdAt: string;
-};
-
-export default function UserList() {
- const [searchQuery, setSearchQuery] = useState("");
- const [currentPage, setCurrentPage] = useState(1);
- const usersPerPage = 6;
- const navigate = useNavigate();
-
- const [users, setUsers] = useState([
- {
- id: 1,
- username: "john_doe",
- email: "john@example.com",
- createdAt: "2024-01-15",
- },
- {
- id: 2,
- username: "jane_smith",
- phone: "+998907654321",
- createdAt: "2024-01-20",
- },
- {
- id: 3,
- username: "ali_karimov",
- phone: "+998909876543",
- createdAt: "2024-02-01",
- },
- {
- id: 4,
- username: "sara_johnson",
- email: "sara@example.com",
- phone: "+998901234567",
- createdAt: "2024-02-10",
- },
- {
- id: 5,
- username: "murod_toshev",
- email: "murod@example.com",
- createdAt: "2024-02-15",
- },
- {
- id: 6,
- username: "aziza_sobirova",
- email: "aziza@example.com",
- createdAt: "2024-03-01",
- },
- {
- id: 7,
- username: "timur_ergashev",
- phone: "+998907777777",
- createdAt: "2024-03-10",
- },
- {
- id: 8,
- username: "odil_akbarov",
- email: "odil@example.com",
- createdAt: "2024-03-12",
- },
- {
- id: 9,
- username: "lola_nazarova",
- phone: "+998909111222",
- createdAt: "2024-04-05",
- },
- {
- id: 10,
- username: "bahrom_tursunov",
- email: "bahrom@example.com",
- createdAt: "2024-04-10",
- },
- ]);
-
- const [confirmDelete, setConfirmDelete] = useState(null);
-
- const handleDelete = (id: number) => {
- setUsers((prev) => prev.filter((u) => u.id !== id));
- setConfirmDelete(null);
- };
-
- const formatPhone = (phone: string) => {
- if (phone.startsWith("+998")) {
- return phone.replace(
- /(\+998)(\d{2})(\d{3})(\d{2})(\d{2})/,
- "$1 $2 $3 $4 $5",
- );
- }
- return phone;
- };
-
- const filteredUsers = users.filter(
- (user) =>
- user.username.toLowerCase().includes(searchQuery.toLowerCase()) ||
- user.email?.toLowerCase().includes(searchQuery.toLowerCase()) ||
- user.phone?.includes(searchQuery),
- );
-
- const totalPages = Math.ceil(filteredUsers.length / usersPerPage);
- const startIndex = (currentPage - 1) * usersPerPage;
- const paginatedUsers = filteredUsers.slice(
- startIndex,
- startIndex + usersPerPage,
- );
-
- const getInitials = (username: string) => username.slice(0, 2).toUpperCase();
-
- const getAvatarGradient = (id: number) => {
- const gradients = ["from-blue-600 to-cyan-500"];
- return gradients[id % gradients.length];
- };
-
- return (
-
-
-
-
-
-
-
-
-
- Foydalanuvchilar
-
-
-
- Jami {users.length} ta foydalanuvchini boshqaring
-
-
-
navigate("/users/create")}
- className="py-5 px-4 bg-gradient-to-br text-white cursor-pointer from-blue-500 to-cyan-500 rounded-xl shadow-lg shadow-cyan-500/20"
- >
-
- Foydalanuvchi Qo'shish
-
-
-
-
-
}
- gradient="from-blue-600 to-blue-400"
- />
-
u.email).length.toString()}
- icon={ }
- gradient="from-cyan-600 to-cyan-400"
- />
- u.phone).length.toString()}
- icon={ }
- gradient="from-purple-600 to-pink-400"
- />
-
-
-
-
-
-
- {
- setSearchQuery(e.target.value);
- setCurrentPage(1);
- }}
- className="w-full pl-14 pr-4 py-3 bg-slate-700/30 border border-slate-600/50 text-white placeholder-slate-400 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
- />
-
-
-
- {paginatedUsers.map((user) => (
-
-
-
-
-
-
- {getInitials(user.username)}
-
-
-
- {user.username}
-
-
- Faol
-
-
-
-
- {user.email && (
-
-
-
- {user.email}
-
-
- )}
- {user.phone && (
-
-
-
- {formatPhone(user.phone)}
-
-
- )}
-
-
-
- {user.createdAt}
-
-
-
-
-
-
-
navigate(`/users/${user.id}/`)}
- className="flex-1 flex cursor-pointer items-center justify-center gap-2 px-4 py-2.5 bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 font-medium rounded-lg transition-all border border-blue-500/30 hover:border-blue-500/50"
- >
-
- Ko'rish
-
-
navigate(`/users/${user.id}/edit`)}
- className="flex-1 flex items-center cursor-pointer justify-center gap-2 px-4 py-2.5 bg-green-500/20 hover:bg-green-500/30 text-green-300 font-medium rounded-lg transition-all border border-green-500/30 hover:border-green-500/50"
- >
-
- Tahrirlash
-
-
setConfirmDelete(user)}
- className="flex-1 flex items-center cursor-pointer justify-center gap-2 px-4 py-2.5 bg-red-500/20 hover:bg-red-500/30 text-red-300 font-medium rounded-lg transition-all border border-red-500/30 hover:border-red-500/50"
- >
-
- O'chirish
-
-
-
-
- ))}
-
-
-
- setCurrentPage((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"
- >
-
-
-
- {[...Array(totalPages)].map((_, i) => (
- setCurrentPage(i + 1)}
- className={`px-4 py-2 rounded-lg border transition-all font-medium ${
- currentPage === 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}
-
- ))}
-
- setCurrentPage((p) => Math.min(p + 1, totalPages))}
- 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"
- >
-
-
-
-
-
- {confirmDelete && (
-
-
-
-
-
-
-
-
- Foydalanuvchini o'chirish
-
-
-
setConfirmDelete(null)}
- className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors"
- >
-
-
-
-
-
-
- Siz{" "}
-
- {confirmDelete.username}
- {" "}
- foydalanuvchini o'chirmoqchimisiz?
-
-
-
-
- Ushbu amalni qaytarib bo'lmaydi.
-
-
-
-
-
- setConfirmDelete(null)}
- className="flex-1 px-4 py-3 border border-slate-600 hover:bg-slate-700/50 text-slate-300 font-medium rounded-lg transition-all"
- >
- Bekor qilish
-
- handleDelete(confirmDelete.id)}
- className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-red-600 to-red-700 hover:from-red-500 hover:to-red-600 text-white font-medium rounded-lg transition-all shadow-lg"
- >
-
- O'chirish
-
-
-
-
- )}
-
- );
-}
-
-function StatCard({
- title,
- value,
- icon,
- gradient,
-}: {
- title: string;
- value: string;
- icon: React.ReactNode;
- gradient: string;
-}) {
- return (
-
- );
-}
diff --git a/src/pages/users/UserDetail.tsx b/src/pages/users/UserDetail.tsx
deleted file mode 100644
index db0ab2a..0000000
--- a/src/pages/users/UserDetail.tsx
+++ /dev/null
@@ -1,689 +0,0 @@
-import { Button } from "@/shared/ui/button";
-import {
- ArrowLeft,
- Bus,
- Calendar,
- Clock,
- CreditCard,
- DollarSign,
- Download,
- Edit,
- Mail,
- MapPin,
- Package,
- Phone,
- Shield,
- Ticket,
- User,
- Users as UsersIcon,
-} from "lucide-react";
-import { useEffect, useState } from "react";
-import { useNavigate, useParams } from "react-router-dom";
-
-type PassportImage = {
- id: number;
- image: string;
-};
-
-type Companion = {
- id: number;
- first_name: string;
- last_name: string;
- birth_date: string;
- phone_number: string;
- gender: "male" | "female";
- participant_pasport_image: PassportImage[];
-};
-
-type Participant = {
- id: number;
- first_name: string;
- last_name: string;
- gender: "male" | "female";
-};
-
-type TicketInfo = {
- id: number;
- title: string;
- service_name: string;
- location_name: string;
-};
-
-type ExtraService = {
- id: number;
- name: string;
-};
-
-type ExtraPaidService = {
- id: number;
- name: string;
- price: number;
-};
-
-type BookingTicket = {
- id: number;
- departure: string;
- destination: string;
- departure_date: string;
- arrival_time: string;
- participant: Participant[];
- ticket: TicketInfo;
- tariff: string;
- transport: string;
- extra_service: ExtraService[];
- extra_paid_service: ExtraPaidService[];
- total_price: number;
- order_status: "pending_payment" | "confirmed" | "cancelled";
-};
-
-type UserData = {
- id: number;
- username: string;
- email?: string;
- phone?: string;
- createdAt: string;
- status: "active" | "inactive";
- companions: Companion[];
- bookings: BookingTicket[];
-};
-
-const UserDetail = () => {
- const navigate = useNavigate();
- const { id } = useParams();
- const [user, setUser] = useState(null);
-
- useEffect(() => {
- // Backend'dan ma'lumot olish
- setUser({
- id: Number(id),
- username: "john_doe",
- email: "john@example.com",
- phone: "+998901234567",
- createdAt: "2024-01-15",
- status: "active",
- companions: [
- {
- id: 1,
- first_name: "Aziza",
- last_name: "Karimova",
- birth_date: "1995-05-20",
- phone_number: "+998901111111",
- gender: "female",
- participant_pasport_image: [
- { id: 1, image: "/images/passport1.jpg" },
- ],
- },
- {
- id: 2,
- first_name: "Sardor",
- last_name: "Toshev",
- birth_date: "1990-08-15",
- phone_number: "+998902222222",
- gender: "male",
- participant_pasport_image: [
- { id: 2, image: "/images/passport2.jpg" },
- ],
- },
- ],
- bookings: [
- {
- id: 1,
- departure: "Toshkent",
- destination: "Samarqand",
- departure_date: "2024-06-20",
- arrival_time: "2024-06-20T18:30:00",
- participant: [
- { id: 1, first_name: "John", last_name: "Doe", gender: "male" },
- ],
- ticket: {
- id: 1,
- title: "Premium Class",
- service_name: "Express Service",
- location_name: "Central Station",
- },
- tariff: "Standard",
- transport: "Bus",
- extra_service: [
- { id: 1, name: "Wi-Fi" },
- { id: 2, name: "Refreshments" },
- ],
- extra_paid_service: [{ id: 1, name: "Extra Luggage", price: 50000 }],
- total_price: 150000,
- order_status: "confirmed",
- },
- {
- id: 2,
- departure: "Samarqand",
- destination: "Buxoro",
- departure_date: "2024-06-25",
- arrival_time: "2024-06-25T15:00:00",
- participant: [
- { id: 2, first_name: "Jane", last_name: "Smith", gender: "female" },
- ],
- ticket: {
- id: 2,
- title: "Economy Class",
- service_name: "Standard Service",
- location_name: "Main Terminal",
- },
- tariff: "Economy",
- transport: "Train",
- extra_service: [{ id: 3, name: "AC" }],
- extra_paid_service: [],
- total_price: 120000,
- order_status: "confirmed",
- },
- {
- id: 3,
- departure: "Samarqand",
- destination: "Buxoro",
- departure_date: "2024-06-25",
- arrival_time: "2024-06-25T15:00:00",
- participant: [
- { id: 2, first_name: "Jane", last_name: "Smith", gender: "female" },
- ],
- ticket: {
- id: 2,
- title: "Economy Class",
- service_name: "Standard Service",
- location_name: "Main Terminal",
- },
- tariff: "Economy",
- transport: "Train",
- extra_service: [{ id: 3, name: "AC" }],
- extra_paid_service: [],
- total_price: 120000,
- order_status: "confirmed",
- },
- ],
- });
- }, [id]);
-
- const formatPhone = (phone: string) => {
- if (phone.startsWith("+998")) {
- return phone.replace(
- /(\+998)(\d{2})(\d{3})(\d{2})(\d{2})/,
- "$1 $2 $3 $4 $5",
- );
- }
- return phone;
- };
-
- const formatPrice = (price: number) => {
- return price.toLocaleString("uz-UZ") + " so'm";
- };
-
- const getStatusBadge = (status: string) => {
- const badges = {
- confirmed:
- "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30",
- pending_payment:
- "bg-amber-500/20 text-amber-400 border border-amber-500/30",
- cancelled: "bg-red-500/20 text-red-400 border border-red-500/30",
- };
- const labels = {
- confirmed: "Tasdiqlangan",
- pending_payment: "To'lov kutilmoqda",
- cancelled: "Bekor qilingan",
- };
- return {
- class: badges[status as keyof typeof badges],
- label: labels[status as keyof typeof labels],
- };
- };
-
- const handleDownloadPDF = (bookingId: number) => {
- console.log("Downloading PDF for booking:", bookingId);
- };
-
- if (!user) {
- return (
-
- );
- }
-
- return (
-
-
- {/* Header */}
-
-
navigate("/")}
- className="mb-4 -ml-2 text-slate-400 hover:text-slate-100 hover:bg-slate-800/50 cursor-pointer"
- >
-
- Orqaga
-
-
-
-
-
-
-
-
-
- {user.username}
-
-
- {user.status === "active" ? "Faol" : "Nofaol"}
-
-
-
-
-
navigate(`/users/${id}/edit`)}
- className="bg-gradient-to-r cursor-pointer from-blue-600 to-indigo-700 hover:from-blue-700 hover:to-indigo-800 text-white shadow-lg shadow-blue-500/20"
- >
-
- Tahrirlash
-
-
-
-
- {/* Main Content */}
-
- {/* Left Column - Main Info */}
-
- {/* Contact Information */}
-
-
-
- Aloqa ma'lumotlari
-
-
- {user.email && (
-
-
-
-
-
-
- Email
-
-
{user.email}
-
-
- )}
-
- {user.phone && (
-
-
-
-
- Telefon
-
-
- {formatPhone(user.phone)}
-
-
-
- )}
-
-
-
- {/* Account Information */}
-
-
-
- Hisob ma'lumotlari
-
-
-
-
-
-
-
-
- Username
-
-
- {user.username}
-
-
-
-
-
-
-
-
-
-
- Yaratilgan sana
-
-
- {user.createdAt}
-
-
-
-
-
-
- {/* Booking Tickets */}
-
-
-
- Sotib olingan chiptalar
-
- {user.bookings.length} ta
-
-
-
- {user.bookings.length > 0 ? (
- user.bookings.map((booking) => {
- const statusBadge = getStatusBadge(booking.order_status);
- return (
-
- {/* Header */}
-
-
-
-
- {booking.departure} → {booking.destination}
-
-
-
- {statusBadge.label}
-
-
-
- {/* Ticket Info */}
-
-
-
- Chipta turi
-
-
- {booking.ticket.title}
-
-
-
-
Xizmat
-
- {booking.ticket.service_name}
-
-
-
-
Manzil
-
- {booking.ticket.location_name}
-
-
-
-
Transport
-
-
- {booking.transport}
-
-
-
-
- {/* Dates */}
-
-
-
-
-
Jo'nash
-
- {booking.departure_date}
-
-
-
-
-
-
-
Yetish
-
- {booking.arrival_time.split("T")[1].slice(0, 5)}
-
-
-
-
-
- {/* Participants */}
-
-
- Yo'lovchilar:
-
-
- {booking.participant.map((p) => (
-
- {p.first_name} {p.last_name}
-
- ))}
-
-
-
- {/* Services */}
- {booking.extra_service.length > 0 && (
-
-
- Qo'shimcha xizmatlar:
-
-
- {booking.extra_service.map((service) => (
-
-
- {service.name}
-
- ))}
-
-
- )}
-
- {/* Paid Services */}
- {booking.extra_paid_service.length > 0 && (
-
-
- Pullik xizmatlar:
-
-
- {booking.extra_paid_service.map((service) => (
-
-
- {service.name}
-
-
- {formatPrice(service.price)}
-
-
- ))}
-
-
- )}
-
- {/* Total & Actions */}
-
-
-
-
-
- Jami narx
-
-
- {formatPrice(booking.total_price)}
-
-
-
-
handleDownloadPDF(booking.id)}
- variant="outline"
- className="flex items-center gap-2 bg-slate-800/50 hover:bg-slate-700/50 text-slate-300 border-slate-700/50 hover:border-slate-600/50"
- >
-
- PDF yuklab olish
-
-
-
- );
- })
- ) : (
-
- Hozircha chiptalar mavjud emas
-
- )}
-
-
-
- {/* Companions */}
-
-
-
- Hamrohlar
-
- {user.companions.length} ta
-
-
-
- {user.companions.length > 0 ? (
- user.companions.map((companion) => (
-
-
-
-
-
-
-
-
- {companion.first_name} {companion.last_name}
-
-
-
- {companion.gender === "male" ? "Erkak" : "Ayol"}
-
-
-
-
-
-
- Tug'ilgan sana
-
-
- {companion.birth_date}
-
-
-
-
Telefon
-
- {formatPhone(companion.phone_number)}
-
-
-
- {companion.participant_pasport_image.length > 0 && (
-
-
- Passport rasmlari:
-
-
- {companion.participant_pasport_image.map(
- (img) => (
-
-
-
- ),
- )}
-
-
- )}
-
-
-
- ))
- ) : (
-
- Hozircha hamrohlar qo'shilmagan
-
- )}
-
-
-
-
- {/* Right Column - Stats */}
-
-
-
-
- Statistika
-
-
-
- Chiptalar
-
- {user.bookings.length} ta
-
-
-
- Hamrohlar
-
- {user.companions.length} ta
-
-
-
- Status
-
- {user.status === "active" ? "Faol" : "Nofaol"}
-
-
-
- ID
-
- #{user.id}
-
-
-
-
-
-
-
Qo'shimcha ma'lumot
-
- Bu foydalanuvchi hozirda tizimda faol holatda. Barcha
- ma'lumotlar to'liq va tasdiqlangan.
-
-
-
-
-
-
- );
-};
-
-export default UserDetail;
diff --git a/src/pages/users/lib/api.ts b/src/pages/users/lib/api.ts
new file mode 100644
index 0000000..1c45731
--- /dev/null
+++ b/src/pages/users/lib/api.ts
@@ -0,0 +1,64 @@
+import type { UsersData, UsersDetaiData } from "@/pages/users/lib/type";
+import httpClient from "@/shared/config/api/httpClient";
+import {
+ DOWNLOAD_PDF,
+ GET_ALL_USERS,
+ UPDATE_USER,
+} from "@/shared/config/api/URLs";
+import type { AxiosResponse } from "axios";
+
+const getAllUsers = async ({
+ page,
+ page_size,
+ search,
+}: {
+ page: number;
+ page_size: number;
+ search: string;
+}): Promise> => {
+ const response = await httpClient.get(GET_ALL_USERS, {
+ params: {
+ page,
+ page_size,
+ search,
+ },
+ });
+ return response;
+};
+
+const getUserDetail = async ({
+ id,
+}: {
+ id: number;
+}): Promise> => {
+ const response = await httpClient.get(`${GET_ALL_USERS}${id}`);
+ return response;
+};
+
+const downloadPdf = async (body: {
+ order_id: number | null;
+ lang: string;
+}): Promise> => {
+ const response = await httpClient.post(DOWNLOAD_PDF, body, {
+ responseType: "blob",
+ });
+ return response;
+};
+
+const updateUser = async ({
+ body,
+ id,
+}: {
+ body: {
+ first_name: string;
+ last_name: string;
+ email: string | null;
+ phone: string | null;
+ };
+ id: number;
+}) => {
+ const response = await httpClient.patch(`${UPDATE_USER}${id}/`, body);
+ return response;
+};
+
+export { downloadPdf, getAllUsers, getUserDetail, updateUser };
diff --git a/src/pages/users/lib/type.ts b/src/pages/users/lib/type.ts
new file mode 100644
index 0000000..e7fc455
--- /dev/null
+++ b/src/pages/users/lib/type.ts
@@ -0,0 +1,99 @@
+export interface UsersData {
+ status: boolean;
+ data: {
+ links: {
+ previous: string;
+ next: string;
+ };
+ total_items: string;
+ total_pages: number;
+ page_size: string;
+ current_page: string;
+ results: {
+ users: [
+ {
+ id: number;
+ phone: string;
+ first_name: string;
+ last_name: string;
+ email: string;
+ avatar: string;
+ validated_at: string;
+ },
+ ];
+ user_register_phone: string;
+ user_register_email: string;
+ };
+ };
+}
+
+export interface UsersDetaiData {
+ status: boolean;
+ data: {
+ id: number;
+ phone: string;
+ email: string;
+ first_name: string;
+ last_name: string;
+ avatar: string;
+ validated_at: string;
+ orders: [
+ {
+ id: 0;
+ departure: string;
+ transport: string;
+ destination: string;
+ ticket: 0;
+ travel_agency_id: string;
+ tariff: string;
+ departure_date: string;
+ arrival_time: string;
+ location_name: string;
+ participant: [
+ {
+ first_name: string;
+ last_name: string;
+ },
+ ];
+ extra_service: [
+ {
+ id: number;
+ name: string;
+ },
+ ];
+ extra_paid_service: [
+ {
+ id: number;
+ name: string;
+ price: number;
+ },
+ ];
+ total_price: number;
+ order_status:
+ | "pending_payment"
+ | "pending_confirmation"
+ | "cancelled"
+ | "confirmed"
+ | "completed";
+ },
+ ];
+ participant: [
+ {
+ id: number;
+ first_name: string;
+ last_name: string;
+ birth_date: string;
+ phone_number: string;
+ gender: "male" | "female";
+ participant_pasport_image: [
+ {
+ id: number;
+ image: string;
+ },
+ ];
+ },
+ ];
+ ticket_count: string;
+ participant_count: string;
+ };
+}
diff --git a/src/pages/users/Create.tsx b/src/pages/users/ui/Create.tsx
similarity index 92%
rename from src/pages/users/Create.tsx
rename to src/pages/users/ui/Create.tsx
index 6045812..ca14139 100644
--- a/src/pages/users/Create.tsx
+++ b/src/pages/users/ui/Create.tsx
@@ -14,11 +14,12 @@ import {
User,
} from "lucide-react";
import React, { useState } from "react";
+import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
export default function CreateUser() {
const navigate = useNavigate();
-
+ const { t } = useTranslation();
const [formData, setFormData] = useState({
username: "",
email: "",
@@ -62,8 +63,8 @@ export default function CreateUser() {
if (!formData.password) {
newErrors.password = "Parol majburiy";
- } else if (formData.password.length < 6) {
- newErrors.password = "Parol kamida 6 ta belgidan iborat bo'lishi kerak";
+ } else if (formData.password.length < 8) {
+ newErrors.password = "Parol kamida 8 ta belgidan iborat bo'lishi kerak";
}
if (formData.password !== formData.confirmPassword) {
@@ -99,7 +100,7 @@ export default function CreateUser() {
className="mb-4 -ml-2 text-slate-400 hover:text-slate-100 hover:bg-slate-800/50 cursor-pointer"
>
- Orqaga
+ {t("Orqaga")}
@@ -108,10 +109,10 @@ export default function CreateUser() {
- Yangi foydalanuvchi
+ {t("Yangi foydalanuvchi")}
- Ma'lumotlarni to'ldiring va saqlang
+ {t("Ma'lumotlarni to'ldiring va saqlang")}
@@ -127,7 +128,7 @@ export default function CreateUser() {
htmlFor="username"
className="text-slate-300 font-medium"
>
- Username
*
+ {t("Ismi")}
@@ -149,8 +150,7 @@ export default function CreateUser() {
{errors.username && (
-
- {errors.username}
+ {t(errors.username)}
)}
@@ -158,7 +158,7 @@ export default function CreateUser() {
{/* Email */}
- Email
+ {t("Email")}
@@ -181,8 +181,7 @@ export default function CreateUser() {
{errors.email && (
-
- {errors.email}
+ {t(errors.email)}
)}
@@ -190,7 +189,7 @@ export default function CreateUser() {
{/* Phone */}
- Telefon raqami
+ {t("Telefon raqami")}
@@ -213,8 +212,7 @@ export default function CreateUser() {
{errors.phone && (
-
- {errors.phone}
+ {t(errors.phone)}
)}
@@ -235,7 +233,7 @@ export default function CreateUser() {
htmlFor="password"
className="text-slate-300 font-medium"
>
- Parol *
+ {t("Parol")}
@@ -271,8 +269,7 @@ export default function CreateUser() {
{errors.password && (
-
- {errors.password}
+ {t(errors.password)}
)}
@@ -283,7 +280,7 @@ export default function CreateUser() {
htmlFor="confirmPassword"
className="text-slate-300 font-medium"
>
- Parolni tasdiqlang *
+ {t("Parolni tasdiqlang")}
@@ -322,8 +319,7 @@ export default function CreateUser() {
{errors.confirmPassword && (
-
- {errors.confirmPassword}
+ {t(errors.confirmPassword)}
)}
@@ -336,13 +332,13 @@ export default function CreateUser() {
onClick={() => navigate("/")}
className="flex-1 h-14 rounded-xl cursor-pointer border-2 border-slate-700/50 bg-slate-800/50 hover:bg-slate-700/50 text-slate-300 hover:text-slate-100 hover:border-slate-600/50 font-medium transition-all duration-200"
>
- Bekor qilish
+ {t("Bekor qilish")}
- Saqlash
+ {t("Saqlash")}
diff --git a/src/pages/users/ui/Edit.tsx b/src/pages/users/ui/Edit.tsx
new file mode 100644
index 0000000..9a5e39c
--- /dev/null
+++ b/src/pages/users/ui/Edit.tsx
@@ -0,0 +1,263 @@
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { getUserDetail, updateUser } from "@/pages/users/lib/api";
+import { useMutation, useQuery } from "@tanstack/react-query";
+
+import formatPhone from "@/shared/lib/formatPhone";
+import onlyNumber from "@/shared/lib/onlyNumber";
+import { Button } from "@/shared/ui/button";
+import {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/shared/ui/form";
+import { Input } from "@/shared/ui/input";
+import { ArrowLeft, Mail, Phone, Save, User } from "lucide-react";
+import { useEffect } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate, useParams } from "react-router-dom";
+
+const formSchema = z
+ .object({
+ first_name: z
+ .string()
+ .min(3, "Ism kamida 3 ta belgidan iborat bo‘lishi kerak")
+ .nonempty("Ism majburiy"),
+ last_name: z
+ .string()
+ .min(3, "Familiya kamida 3 ta belgidan iborat bo‘lishi kerak")
+ .nonempty("Familiya majburiy"),
+ email: z.string().email("Email formati noto‘g‘ri").or(z.literal("")),
+ phone: z.string().min(9, "Telefon raqam to‘liq emas").or(z.literal("+998")),
+ })
+ .refine((data) => data.email || data.phone, {
+ message: "Email yoki telefon raqami kiritilishi shart",
+ path: ["contact"],
+ });
+
+export default function EditUser() {
+ const navigate = useNavigate();
+ const { id } = useParams();
+ const { t } = useTranslation();
+
+ const { data } = useQuery({
+ queryKey: ["user_detail", id],
+ queryFn: () => getUserDetail({ id: Number(id) }),
+ });
+
+ const form = useForm>({
+ resolver: zodResolver(formSchema),
+ defaultValues: {
+ first_name: "",
+ last_name: "",
+ email: "",
+ phone: "+998",
+ },
+ });
+
+ useEffect(() => {
+ if (data) {
+ const u = data.data.data;
+ form.reset({
+ first_name: u.first_name,
+ last_name: u.last_name,
+ email: u.email || "",
+ phone: formatPhone(u.phone),
+ });
+ }
+ }, [data, form]);
+
+ const { mutate, isPending } = useMutation({
+ mutationFn: ({
+ body,
+ id,
+ }: {
+ body: {
+ first_name: string;
+ last_name: string;
+ email: string | null;
+ phone: string | null;
+ };
+ id: number;
+ }) => updateUser({ body, id }),
+ onSuccess: () => navigate("/user"),
+ });
+
+ const onSubmit = (values: z.infer) => {
+ if (data) {
+ mutate({
+ body: {
+ first_name: values.first_name,
+ last_name: values.last_name,
+ email: values.email.length === 0 ? null : values.email,
+ phone: values.phone.length === 0 ? null : onlyNumber(values.phone),
+ },
+ id: data.data.data.id,
+ });
+ }
+ };
+
+ return (
+
+
+ {/* Header */}
+
+
navigate(-1)}
+ className="mb-4 -ml-2 text-slate-400 cursor-pointer hover:text-slate-100 hover:bg-slate-800/50"
+ >
+
+ {t("Orqaga")}
+
+
+
+
+
+
+
+
+ {t("Tahrirlash")}
+
+
+ {t("Ma'lumotlarni yangilang")}
+
+
+
+
+
+ {/* Form */}
+
+
+
+ );
+}
diff --git a/src/pages/users/ui/User.tsx b/src/pages/users/ui/User.tsx
new file mode 100644
index 0000000..17d7f79
--- /dev/null
+++ b/src/pages/users/ui/User.tsx
@@ -0,0 +1,350 @@
+import { getAllUsers } from "@/pages/users/lib/api";
+import formatPhone from "@/shared/lib/formatPhone";
+import { Avatar, AvatarFallback, AvatarImage } from "@/shared/ui/avatar";
+import { Button } from "@/shared/ui/button";
+import { useQuery } from "@tanstack/react-query";
+import {
+ AlertTriangle,
+ Calendar,
+ ChevronLeft,
+ ChevronRight,
+ Eye,
+ Loader2,
+ Mail,
+ Pencil,
+ Phone,
+ Search,
+ Trash2,
+ Users,
+} from "lucide-react";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { useNavigate } from "react-router-dom";
+
+export default function UserList() {
+ const [searchQuery, setSearchQuery] = useState("");
+ const [currentPage, setCurrentPage] = useState(1);
+ const usersPerPage = 6;
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+
+ const { data, isLoading, isError, refetch } = useQuery({
+ queryKey: ["user_all", currentPage, searchQuery],
+ queryFn: () =>
+ getAllUsers({
+ page: currentPage,
+ page_size: usersPerPage,
+ search: searchQuery,
+ }),
+ });
+
+ const getAvatarGradient = (id: number) => {
+ const gradients = ["from-blue-600 to-cyan-500"];
+ return gradients[id % gradients.length];
+ };
+
+ if (isError) {
+ return (
+
+
+
+ {t("Ma'lumotlarni yuklashda xatolik yuz berdi.")}
+
+
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")}
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {t("Foydalanuvchilar")}
+
+
+
+ {t("Jami")} {data?.data.data.total_items}{" "}
+ {t("ta foydalanuvchini boshqaring")}
+
+
+ {/*
navigate("/users/create")}
+ className="py-5 px-4 bg-gradient-to-br text-white cursor-pointer from-blue-500 to-cyan-500 rounded-xl shadow-lg shadow-cyan-500/20"
+ >
+
+ {t("Foydalanuvchi Qo'shish")}
+ */}
+
+
+
+ }
+ gradient="from-blue-600 to-blue-400"
+ />
+ }
+ gradient="from-cyan-600 to-cyan-400"
+ />
+ }
+ gradient="from-purple-600 to-pink-400"
+ />
+
+
+
+
+
+
+ {
+ setSearchQuery(e.target.value);
+ setCurrentPage(1);
+ }}
+ className="w-full pl-14 pr-4 py-3 bg-slate-700/30 border border-slate-600/50 text-white placeholder-slate-400 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
+ />
+
+
+
+ {isLoading ? (
+
+
+
{t("Ma'lumotlar yuklanmoqda...")}
+
+ ) : (
+ <>
+
+
+ {data &&
+ data.data.data.results.users.map((e) => (
+
+
+
+
+
+
+
+
+ {e.first_name.slice(0, 1).toUpperCase()}
+ {e.last_name.slice(0, 1).toUpperCase()}
+
+
+
+
+ {e.first_name} {e.last_name}
+
+
+
+
+ {e.email && (
+
+
+
+ {e.email}
+
+
+ )}
+ {e.phone && (
+
+
+
+ {formatPhone(e.phone)}
+
+
+ )}
+
+
+
+ {e.validated_at}
+
+
+
+
+
+
+
navigate(`/users/${e.id}/`)}
+ className="flex-1 flex cursor-pointer items-center justify-center gap-2 px-4 py-2.5 bg-blue-500/20 hover:bg-blue-500/30 text-blue-300 font-medium rounded-lg transition-all border border-blue-500/30 hover:border-blue-500/50"
+ >
+
+ {t("Ko'rish")}
+
+
navigate(`/users/${e.id}/edit`)}
+ className="flex-1 flex items-center cursor-pointer justify-center gap-2 px-4 py-2.5 bg-green-500/20 hover:bg-green-500/30 text-green-300 font-medium rounded-lg transition-all border border-green-500/30 hover:border-green-500/50"
+ >
+
+ {t("Tahrirlash")}
+
+
+
+ {t("O'chirish")}
+
+
+
+
+ ))}
+
+
+
+ setCurrentPage((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"
+ >
+
+
+
+ {[...Array(data?.data.data.total_pages)].map((_, i) => (
+ setCurrentPage(i + 1)}
+ className={`px-4 py-2 rounded-lg border transition-all font-medium ${
+ currentPage === 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}
+
+ ))}
+
+
+ setCurrentPage((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"
+ >
+
+
+
+
+
+ {/* {confirmDelete && (
+
+
+
+
+
+
+
+
+ {t("Foydalanuvchini o'chirish")}
+
+
+
setConfirmDelete(null)}
+ className="p-2 hover:bg-slate-700/50 rounded-lg transition-colors"
+ >
+
+
+
+
+
+
+ {t("Siz")}{" "}
+
+ {confirmDelete.username}
+ {" "}
+ {t("foydalanuvchini o'chirmoqchimisiz?")}
+
+
+
+
+ {t("Ushbu amalni qaytarib bo'lmaydi")}
+
+
+
+
+
+ setConfirmDelete(null)}
+ className="flex-1 px-4 py-3 border border-slate-600 hover:bg-slate-700/50 text-slate-300 font-medium rounded-lg transition-all"
+ >
+ {t("Bekor qilish")}
+
+ handleDelete(confirmDelete.id)}
+ className="flex-1 flex items-center justify-center gap-2 px-4 py-3 bg-gradient-to-r from-red-600 to-red-700 hover:from-red-500 hover:to-red-600 text-white font-medium rounded-lg transition-all shadow-lg"
+ >
+
+ {t("O'chirish")}
+
+
+
+
+ )} */}
+ >
+ )}
+
+ );
+}
+
+function StatCard({
+ title,
+ value,
+ icon,
+ gradient,
+}: {
+ title: string;
+ value: string;
+ icon: React.ReactNode;
+ gradient: string;
+}) {
+ return (
+
+ );
+}
diff --git a/src/pages/users/ui/UserDetail.tsx b/src/pages/users/ui/UserDetail.tsx
new file mode 100644
index 0000000..0a7991e
--- /dev/null
+++ b/src/pages/users/ui/UserDetail.tsx
@@ -0,0 +1,558 @@
+import { downloadPdf, getUserDetail } from "@/pages/users/lib/api";
+import i18n from "@/shared/config/i18n";
+import formatPhone from "@/shared/lib/formatPhone";
+import formatPrice from "@/shared/lib/formatPrice";
+import { Button } from "@/shared/ui/button";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import {
+ ArrowLeft,
+ Calendar,
+ Clock,
+ DollarSign,
+ Download,
+ Edit,
+ Loader2,
+ Mail,
+ MapPin,
+ Package,
+ Phone,
+ Shield,
+ Ticket,
+ User,
+ Users as UsersIcon,
+} from "lucide-react";
+import { useTranslation } from "react-i18next";
+import { useNavigate, useParams } from "react-router-dom";
+import { toast } from "sonner";
+
+const UserDetail = () => {
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const locale = i18n.language;
+ const { id } = useParams();
+
+ const { data, isLoading } = useQuery({
+ queryKey: ["user_detail", id],
+ queryFn: () => getUserDetail({ id: Number(id) }),
+ });
+
+ const { mutate } = useMutation({
+ mutationFn: ({ id }: { id: number }) =>
+ downloadPdf({ lang: locale, order_id: id }),
+ onSuccess: (res) => {
+ const blob = new Blob([res.data], { type: "application/pdf" });
+ const url = window.URL.createObjectURL(blob);
+ const link = document.createElement("a");
+ link.href = url;
+ link.download = `ticket-order-${id}.pdf`;
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+ },
+ onError: () => {
+ toast.error("Xatolik yuz berdi");
+ },
+ });
+
+ const getStatusBadge = (status: string) => {
+ const badges = {
+ confirmed:
+ "bg-emerald-500/20 text-emerald-400 border border-emerald-500/30",
+ pending_payment:
+ "bg-amber-500/20 text-amber-400 border border-amber-500/30",
+ cancelled: "bg-red-500/20 text-red-400 border border-red-500/30",
+ };
+ const labels = {
+ confirmed: "Tasdiqlangan",
+ pending_payment: "To'lov kutilmoqda",
+ cancelled: "Bekor qilingan",
+ };
+ return {
+ class: badges[status as keyof typeof badges],
+ label: labels[status as keyof typeof labels],
+ };
+ };
+
+ const handleDownloadPDF = (bookingId: number) => {
+ mutate({
+ id: bookingId,
+ });
+ };
+
+ if (isLoading) {
+ return (
+
+
+
{t("Ma'lumotlar yuklanmoqda...")}
+
+ );
+ }
+
+ return (
+
+ {data && (
+ <>
+
+
+
navigate("/")}
+ className="mb-4 -ml-2 text-slate-400 hover:text-slate-100 hover:bg-slate-800/50 cursor-pointer"
+ >
+
+ {t("Orqaga")}
+
+
+
+
+
+
+
+
+
+ {data?.data.data.first_name} {data?.data.data.last_name}
+
+ {/*
+ {user.status === "active" ? t("Faol") : t("Nofaol")}
+ */}
+
+
+
+
navigate(`/users/${id}/edit`)}
+ className="bg-gradient-to-r cursor-pointer from-blue-600 to-indigo-700 hover:from-blue-700 hover:to-indigo-800 text-white shadow-lg shadow-blue-500/20"
+ >
+
+ {t("Tahrirlash")}
+
+
+
+
+
+
+
+
+
+ {t("Aloqa ma'lumotlari")}
+
+
+ {data?.data.data.email && (
+
+
+
+
+
+
+ {t("Email")}
+
+
+ {data?.data.data.email}
+
+
+
+ )}
+
+ {data?.data.data.phone && (
+
+
+
+
+ {t("Telefon raqami")}
+
+
+ {formatPhone(data?.data.data.phone)}
+
+
+
+ )}
+
+
+
+
+
+
+ {t("Hisob ma'lumotlari")}
+
+
+
+
+
+
+
+
+ {t("Ismi")}
+
+
+ {data?.data.data.first_name}{" "}
+ {data?.data.data.last_name}
+
+
+
+
+
+
+
+
+
+
+ {t("Yaratilgan sana")}
+
+
+ {data?.data.data.validated_at}
+
+
+
+
+
+
+
+
+
+ {t("Sotib olingan chiptalar")}
+
+ {data?.data.data.orders.length} ta
+
+
+
+ {data?.data.data.orders.length > 0 ? (
+ data?.data.data.orders.map((booking) => {
+ const statusBadge = getStatusBadge(
+ booking.order_status,
+ );
+ return (
+
+
+
+
+
+ {booking.departure} → {booking.destination}
+
+
+
+ {statusBadge.label}
+
+
+
+
+
+
+ {t("Chipta turi")}
+
+
+ {booking.tariff}
+
+
+ {booking.extra_service.length > 0 && (
+
+
+ {t("Xizmat")}
+
+ <>
+ {booking.extra_service.map((e) => (
+
+ {e.name}
+
+ ))}
+ >
+
+ )}
+
+
+ {t("Manzil")}
+
+
+ {booking.location_name}
+
+
+
+
+ {t("Transport")}
+
+
+ {booking.transport}
+
+
+
+
+
+
+
+
+
+ {t("Jo'nash")}
+
+
+ {booking.departure_date}
+
+
+
+
+
+
+
+ {t("Yetish")}
+
+
+ {booking.arrival_time}
+
+
+
+
+
+
+
+ {t("Yo'lovchilar")}:
+
+
+ {booking.participant.map((p) => (
+
+ {p.first_name} {p.last_name}
+
+ ))}
+
+
+
+ {booking.extra_service.length > 0 && (
+
+
+ {t("Qo'shimcha xizmatlar")}:
+
+
+ {booking.extra_service.map((service) => (
+
+
+ {service.name}
+
+ ))}
+
+
+ )}
+
+ {booking.extra_paid_service.length > 0 && (
+
+
+ {t("Pullik xizmatlar")}:
+
+
+ {booking.extra_paid_service.map((service) => (
+
+
+ {service.name}
+
+
+ {formatPrice(service.price, true)}
+
+
+ ))}
+
+
+ )}
+
+
+
+
+
+
+ {t("Jami narx")}
+
+
+ {formatPrice(booking.total_price, true)}
+
+
+
+
handleDownloadPDF(booking.id)}
+ variant="outline"
+ className="flex items-center gap-2 bg-slate-800/50 hover:bg-slate-700/50 text-slate-300 border-slate-700/50 hover:border-slate-600/50"
+ >
+
+ {t("PDF yuklab olish")}
+
+
+
+ );
+ })
+ ) : (
+
+ {t("Hozircha chiptalar mavjud emas")}
+
+ )}
+
+
+
+
+
+
+ {t("Hamrohlar")}
+
+ {data.data.data.participant.length} ta
+
+
+
+ {data.data.data.participant.length > 0 ? (
+ data.data.data.participant.map((companion) => (
+
+
+
+
+
+
+
+
+ {companion.first_name} {companion.last_name}
+
+
+
+ {companion.gender === "male"
+ ? t("Erkak")
+ : t("Ayol")}
+
+
+
+
+
+
+ {t("Tug'ilgan sana")}
+
+
+ {companion.birth_date}
+
+
+
+
+ {t("Telefon raqami")}
+
+
+ {formatPhone(companion.phone_number)}
+
+
+
+ {companion.participant_pasport_image.length >
+ 0 && (
+
+
+ {t("Passport rasmlari")}:
+
+
+ {companion.participant_pasport_image.map(
+ (img) => (
+
+
+
+ ),
+ )}
+
+
+ )}
+
+
+
+ ))
+ ) : (
+
+ {t("Hozircha hamrohlar qo'shilmagan")}
+
+ )}
+
+
+
+
+
+
+
+
+ {t("Statistika")}
+
+
+
+
+ {t("Chiptalar")}
+
+
+ {data.data.data.orders.length}
+
+
+
+
+ {t("Hamrohlar")}
+
+
+ {data.data.data.participant_count}
+
+
+ {/*
+
+ {t("Status")}
+
+
*/}
+
+ {t("ID")}
+
+ #{data.data.data.id}
+
+
+
+
+
+
+
+ {t("Qo'shimcha ma'lumot")}
+
+
+ {t(
+ "Bu foydalanuvchi hozirda tizimda faol holatda. Barcha ma'lumotlar to'liq va tasdiqlangan.",
+ )}
+
+
+
+
+
+ >
+ )}
+
+ );
+};
+
+export default UserDetail;
diff --git a/src/shared/config/api/URLs.ts b/src/shared/config/api/URLs.ts
index 5ff9d1c..0443884 100644
--- a/src/shared/config/api/URLs.ts
+++ b/src/shared/config/api/URLs.ts
@@ -1,6 +1,41 @@
const BASE_URL =
- import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com';
+ import.meta.env.VITE_API_URL || "https://simple-travel.felixits.uz/api/v1/";
-const ENDP_POSTS = '/posts/';
+const AUTH_LOGIN = "auth/token/phone/";
+const GET_ME = "auth/me/";
+const GET_ALL_USERS = "dashboard/users/";
+const DOWNLOAD_PDF = "get-order-pdf/";
+const UPDATE_USER = "/dashboard/users/";
+const GET_ALL_AGENCY = "dashboard/tour-agency/";
+const GET_ALL_EMPLOYEES = "dashboard/employees/";
+const GET_TICKET = "dashboard/dashboard-tickets/";
+const HOTEL_BADGE = "dashboard/dashboard-tickets-settings-badge/";
+const HOTEL_FEATURES = "dashboard/dashboard-ticket-hotel-feature-type/";
+const HOTEL_FEATURES_TYPE = "dashboard/dashboard-ticket-hotel-feature/";
+const HOTEL_TARIF = "dashboard/dashboard-tickets-settings-tariff/";
+const TOUR_TRANSPORT = "dashboard/dashboard-tickets-settings-transport/";
+const HPTEL_TYPES = "dashboard/dashboard-tickets-settings-hotel-type/";
+const NEWS = "dashboard/dashboard-post/";
+const NEWS_CATEGORY = "dashboard/dashboard-category/";
+const HOTEL = "dashboard/dashboard-hotel/";
-export { BASE_URL, ENDP_POSTS };
+export {
+ AUTH_LOGIN,
+ BASE_URL,
+ DOWNLOAD_PDF,
+ GET_ALL_AGENCY,
+ GET_ALL_EMPLOYEES,
+ GET_ALL_USERS,
+ GET_ME,
+ GET_TICKET,
+ HOTEL,
+ HOTEL_BADGE,
+ HOTEL_FEATURES,
+ HOTEL_FEATURES_TYPE,
+ HOTEL_TARIF,
+ HPTEL_TYPES,
+ NEWS,
+ NEWS_CATEGORY,
+ TOUR_TRANSPORT,
+ UPDATE_USER,
+};
diff --git a/src/shared/config/api/auth/api.ts b/src/shared/config/api/auth/api.ts
new file mode 100644
index 0000000..39914da
--- /dev/null
+++ b/src/shared/config/api/auth/api.ts
@@ -0,0 +1,22 @@
+import type { LoginData, MeData } from "@/shared/config/api/auth/auth.model";
+import httpClient from "@/shared/config/api/httpClient";
+import { AUTH_LOGIN, GET_ME } from "@/shared/config/api/URLs";
+import type { AxiosResponse } from "axios";
+
+const authLogin = async ({
+ phone,
+ password,
+}: {
+ phone: string;
+ password: string;
+}): Promise> => {
+ const response = await httpClient.post(AUTH_LOGIN, { phone, password });
+ return response;
+};
+
+const getMe = async (): Promise> => {
+ const response = await httpClient.get(GET_ME);
+ return response;
+};
+
+export { authLogin, getMe };
diff --git a/src/shared/config/api/auth/auth.model.ts b/src/shared/config/api/auth/auth.model.ts
new file mode 100644
index 0000000..788d469
--- /dev/null
+++ b/src/shared/config/api/auth/auth.model.ts
@@ -0,0 +1,32 @@
+export interface LoginData {
+ access: string;
+ refresh: string;
+}
+
+export interface MeData {
+ status: boolean;
+ data: {
+ id: number;
+ last_login: string;
+ is_superuser: boolean;
+ first_name: string;
+ last_name: string;
+ is_staff: boolean;
+ is_active: boolean;
+ date_joined: string;
+ phone: string;
+ email: string;
+ username: string;
+ avatar: string;
+ validated_at: string;
+ role:
+ | "superuser"
+ | "admin"
+ | "moderator"
+ | "tour_admin"
+ | "buxgalter"
+ | "operator"
+ | "user";
+ travel_agency: number;
+ };
+}
diff --git a/src/shared/config/api/httpClient.ts b/src/shared/config/api/httpClient.ts
index 1b28c43..eff0778 100644
--- a/src/shared/config/api/httpClient.ts
+++ b/src/shared/config/api/httpClient.ts
@@ -1,7 +1,28 @@
import i18n from "@/shared/config/i18n";
-import axios from "axios";
+import {
+ getAuthToken,
+ getRefAuthToken,
+ removeAuthToken,
+ removeRefAuthToken,
+ setAuthToken,
+} from "@/shared/lib/authCookies";
+import axios, { AxiosError } from "axios";
import { BASE_URL } from "./URLs";
+let isRefreshing = false;
+let failedQueue: {
+ resolve: (token: string) => void;
+ reject: (error: any) => void;
+}[] = [];
+
+const processQueue = (error: any, token: string | null = null) => {
+ failedQueue.forEach((prom) => {
+ if (error) prom.reject(error);
+ else if (token) prom.resolve(token);
+ });
+ failedQueue = [];
+};
+
const httpClient = axios.create({
baseURL: BASE_URL,
timeout: 10000,
@@ -9,13 +30,16 @@ const httpClient = axios.create({
httpClient.interceptors.request.use(
async (config) => {
- // Language configs
- const language = i18n.language;
- config.headers["Accept-Language"] = language;
- // const accessToken = localStorage.getItem('accessToken');
- // if (accessToken) {
- // config.headers['Authorization'] = `Bearer ${accessToken}`;
- // }
+ // Faqat GET so'rovlarida Accept-Language headerini qo'shish
+ if (config.method?.toLowerCase() === "get") {
+ const language = i18n.language;
+ config.headers["Accept-Language"] = language;
+ }
+
+ const accessToken = getAuthToken();
+ if (accessToken) {
+ config.headers["Authorization"] = `Bearer ${accessToken}`;
+ }
return config;
},
@@ -24,7 +48,62 @@ httpClient.interceptors.request.use(
httpClient.interceptors.response.use(
(response) => response,
- (error) => {
+ async (error: AxiosError) => {
+ const originalRequest = error.config as any;
+
+ if (error.response?.status === 401 && !originalRequest._retry) {
+ if (isRefreshing) {
+ return new Promise((resolve, reject) => {
+ failedQueue.push({ resolve, reject });
+ })
+ .then((token) => {
+ originalRequest.headers["Authorization"] = `Bearer ${token}`;
+ return httpClient(originalRequest);
+ })
+ .catch((err) => Promise.reject(err));
+ }
+
+ originalRequest._retry = true;
+ isRefreshing = true;
+
+ const refreshToken = getRefAuthToken();
+ if (!refreshToken) {
+ removeAuthToken();
+ removeRefAuthToken();
+ window.location.href = "/login";
+ return Promise.reject(error);
+ }
+
+ try {
+ const response = await axios.post(`${BASE_URL}auth/token/refresh/`, {
+ refresh: refreshToken,
+ });
+
+ const newAccessToken = response.data.access;
+ setAuthToken(newAccessToken);
+
+ httpClient.defaults.headers["Authorization"] =
+ `Bearer ${newAccessToken}`;
+ processQueue(null, newAccessToken);
+
+ originalRequest.headers["Authorization"] = `Bearer ${newAccessToken}`;
+ return httpClient(originalRequest);
+ } catch (refreshError: any) {
+ processQueue(refreshError, null);
+ removeAuthToken();
+ removeRefAuthToken();
+ const status = refreshError.response?.status;
+
+ if ([401].includes(status)) {
+ window.location.href = "/login";
+ }
+
+ return Promise.reject(refreshError);
+ } finally {
+ isRefreshing = false;
+ }
+ }
+
console.error("API error:", error);
return Promise.reject(error);
},
diff --git a/src/shared/config/api/test/test.request.ts b/src/shared/config/api/test/test.request.ts
index a8d9bcb..8b13789 100644
--- a/src/shared/config/api/test/test.request.ts
+++ b/src/shared/config/api/test/test.request.ts
@@ -1,14 +1 @@
-import httpClient from '@/shared/config/api/httpClient';
-import type { TestApiType } from '@/shared/config/api/test/test.model';
-import type { ReqWithPagination } from '@/shared/config/api/types';
-import { ENDP_POSTS } from '@/shared/config/api/URLs';
-import type { AxiosResponse } from 'axios';
-const getPosts = async (
- pagination?: ReqWithPagination,
-): Promise> => {
- const response = await httpClient.get(ENDP_POSTS, { params: pagination });
- return response;
-};
-
-export { getPosts };
diff --git a/src/shared/config/i18n/locales/ru/translation.json b/src/shared/config/i18n/locales/ru/translation.json
index 32b1bb9..42bbdeb 100644
--- a/src/shared/config/i18n/locales/ru/translation.json
+++ b/src/shared/config/i18n/locales/ru/translation.json
@@ -1,13 +1,332 @@
{
- "welcome": "Добро пожаловать на наш сайт",
- "language": "Язык",
+ "Admin Panelga Kirish": "Вход в Админ Панель",
+ "Telefon raqam": "Номер телефона",
+ "Parol": "Пароль",
+ "Parolingizni kiriting": "Введите пароль",
+ "Kirish": "Войти",
+ "Admin Panel": "Админ Панель",
"Foydalanuvchilar": "Пользователи",
"Tur firmalar": "Турфирмы",
"Xodimlar": "Сотрудники",
"Byudjet": "Бюджет",
"Turlar": "Туры",
+ "Tur sozlamalari": "Настройки туров",
"Bronlar": "Бронирования",
"Yangiliklar": "Новости",
- "Yordam Arizalar": "Заявки на помощь",
- "Tur sozlamalari": "Настройки тура"
+ "Kategoriya": "Категория",
+ "FAQ": "FAQ",
+ "Savollar ro‘yxati": "Список вопросов",
+ "Savollar kategoriyasi": "Категории вопросов",
+ "Arizalar": "Заявки",
+ "Agentlik arizalari": "Заявки агентств",
+ "Yordam arizalari": "Заявки на помощь",
+ "Sayt sozlamalari": "Настройки сайта",
+ "Sayt SEOsi": "SEO сайта",
+ "Offerta": "Оферта",
+ "Yordam pagelari": "Страницы помощи",
+ "Jami": "Всего",
+ "ta foydalanuvchini boshqaring": "управляйте пользователями",
+ "Foydalanuvchi Qo'shish": "Добавить пользователя",
+ "Jami foydalanuvchilar": "Всего пользователей",
+ "Email bilan ro'yxatlangan": "Зарегистрированы по email",
+ "Telefon bilan ro'yxatlangan": "Зарегистрированы по телефону",
+ "Username, email yoki telefon raqami bo'yicha qidirish": "Поиск по имени пользователя, email или номеру телефона...",
+ "Faol": "Активен",
+ "Ko'rish": "Просмотр",
+ "Tahrirlash": "Редактировать",
+ "O'chirish": "Удалить",
+ "Foydalanuvchini o'chirish": "Удалить пользователя",
+ "Siz": "Вы",
+ "foydalanuvchini o'chirmoqchimisiz?": "хотите удалить пользователя?",
+ "Ushbu amalni qaytarib bo'lmaydi": "Это действие невозможно отменить.",
+ "Bekor qilish": "Отмена",
+ "Orqaga": "Назад",
+ "Yangi foydalanuvchi": "Новый пользователь",
+ "Ma'lumotlarni to'ldiring va saqlang": "Заполните данные и сохраните",
+ "Ismi": "Имя",
+ "Email": "Email",
+ "Telefon raqami": "Номер телефона",
+ "Parolni tasdiqlang": "Подтвердите пароль",
+ "Saqlash": "Сохранить",
+ "Username majburiy": "Имя пользователя обязательно",
+ "Username kamida 3 ta belgidan iborat bo'lishi kerak": "Имя пользователя должно содержать не менее 3 символов",
+ "Email yoki telefon raqamlardan kamida bittasi kiritilishi kerak": "Должен быть введен как минимум один из email или номера телефона",
+ "Email formati noto'g'ri": "Неверный формат email",
+ "Telefon raqami formati: +998901234567": "Формат номера телефона: +998901234567",
+ "Parol majburiy": "Пароль обязателен",
+ "Parol kamida 8 ta belgidan iborat bo'lishi kerak": "Пароль должен содержать не менее 8 символов",
+ "Parollar mos kelmaydi": "Пароли не совпадают",
+ "Ma'lumotlarni yangilang": "Обновите данные",
+ "Yangilash": "Обновить",
+ "Nofaol": "Неактивен",
+ "Aloqa ma'lumotlari": "Контактная информация",
+ "Hisob ma'lumotlari": "Данные учетной записи",
+ "Yaratilgan sana": "Дата создания",
+ "Sotib olingan chiptalar": "Купленные билеты",
+ "Chipta turi": "Тип билета",
+ "Xizmat": "Сервис",
+ "Manzil": "Адрес",
+ "Transport": "Транспорт",
+ "Jo'nash": "Отправление",
+ "Yetish": "Прибытие",
+ "Yo'lovchilar": "Пассажиры",
+ "Qo'shimcha xizmatlar": "Дополнительные услуги",
+ "Pullik xizmatlar": "Платные услуги",
+ "Jami narx": "Общая цена",
+ "PDF yuklab olish": "Скачать PDF",
+ "Hozircha chiptalar mavjud emas": "Пока что билетов нет",
+ "Hamrohlar": "Партнёры",
+ "Erkak": "Мужчина",
+ "Ayol": "Женщина",
+ "Tug'ilgan sana": "Дата рождения",
+ "Passport rasmlari": "Изображения паспорта",
+ "Hozircha hamrohlar qo'shilmagan": "Пока что партнёры не добавлены",
+ "Statistika": "Статистика",
+ "Chiptalar": "Билеты",
+ "Status": "Статус",
+ "ID": "ID",
+ "Qo'shimcha ma'lumot": "Дополнительная информация",
+ "Bu foydalanuvchi hozirda tizimda faol holatda. Barcha ma'lumotlar to'liq va tasdiqlangan.": "Этот пользователь в настоящее время активен в системе. Все данные полные и подтвержденные.",
+ "Tur firmalari": "Турфирмы",
+ "Tur firmalaringizni boshqaring va ularning faoliyatini kuzatib boring.": "Управляйте своими турфирмами и отслеживайте их деятельность.",
+ "Jami firmalar": "Всего фирм",
+ "Faol firmalar": "Активные фирмы",
+ "Jami turlar": "Всего туров",
+ "Umumiy daromad": "Общий доход",
+ "Komissiya": "Комиссия",
+ "Jami tur": "Всего туров",
+ "Sotilgan tur": "Проданные туры",
+ "Daromad": "Доход",
+ "Egasi": "Владелец",
+ "Qo'shilgan turlar soni": "Количество добавленных туров",
+ "Sotilgan turlar soni": "Количество проданных туров",
+ "Jami sotilgan turlar": "Всего проданных туров",
+ "Ulush foizi": "Процент доли",
+ "Har bir sotuvdan": "С каждого проданного тура",
+ "so'm daromad": "сум дохода",
+ "Qo'shilgan turlar": "Добавленные туры",
+ "Firma tomonidan qo'shilgan barcha turlar ro'yxati": "Список всех туров, добавленных фирмой",
+ "Sotilgan": "Продано",
+ "ta xodim": "сотрудников",
+ "Xodim qo'shish": "Добавить сотрудника",
+ "Operator": "Оператор",
+ "Bugalter": "Бухгалтер",
+ "Manager": "Менеджер",
+ "Xodimni tahrirlash": "Редактировать сотрудника",
+ "Familiyasi": "Фамилия",
+ "Role": "Роль",
+ "Qo'shish": "Добавить",
+ "Role tanlang": "Выберите роль",
+ "Sayohat moliyasi boshqaruv paneli": "Панель управления финансовыми аспектами путешествий",
+ "Bronlar, to'lovlar va agentlik moliyalari boshqaruvi": "Управление бронированиями, платежами и финансами агентств",
+ "Bandlovlar va to‘lovlar": "Бронирования и платежи",
+ "Agentlik hisobotlari": "Отчеты агентств",
+ "Barcha bandlovlar": "Все бронирования",
+ "To'langan": "Оплачено",
+ "Kutilmoqda": "Ожидается",
+ "Bekor qilindi": "Отменено",
+ "Qaytarilgan": "Возвращено",
+ "Jami daromad": "Общий доход",
+ "Yakunlangan bandlovlardan": "От завершенных бронирований",
+ "Kutilayotgan to‘lovlar": "Ожидаемые платежи",
+ "Tasdiqlash kutilmoqda": "Ожидает подтверждения",
+ "Tasdiqlangan bandlovlar": "Подтвержденные бронирования",
+ "Kutilayotgan bandlovlar": "Ожидаемые бронирования",
+ "Oxirgi bandlovlar": "Последние бронирования",
+ "Sayohat sanasi": "Дата путешествия",
+ "Miqdor": "Сумма",
+ "Paid": "Оплачено",
+ "Pending": "В ожидании",
+ "Cancelled": "Отменено",
+ "Foydalanuvchi moliyaviy tafsilotlari": "Финансовые детали пользователя",
+ "uchun batafsil moliyaviy sharh": "для подробного финансового обзора",
+ "Total Spent": "Всего потрачено",
+ "Total Bookings": "Всего бронирований",
+ "All completed bookings": "Все завершенные бронирования",
+ "Pending Payments": "Ожидаемые платежи",
+ "Awaiting confirmation": "Ожидает подтверждения",
+ "All time bookings": "Бронирования за все время",
+ "Member Level": "Уровень участника",
+ "Loyalty status": "Статус лояльности",
+ "Booking History": "История бронирований",
+ "User Details": "Детали пользователя",
+ "Booking Ref": "Номер бронирования",
+ "Destination": "Направление",
+ "Travel Dates": "Даты путешествия",
+ "Travelers": "Путешественники",
+ "Amount": "Сумма",
+ "Booked on": "Забронировано",
+ "Personal Information": "Личная информация",
+ "Full Name": "Полное имя",
+ "Phone Number": "Номер телефона",
+ "Email Address": "Адрес электронной почты",
+ "Member Since": "Участник с",
+ "Travel Statistics": "Статистика путешествий",
+ "Favorite Destination": "Любимое направление",
+ "bookings": "бронирований",
+ "Preferred Agency": "Предпочитаемое агентство",
+ "out of": "из",
+ "Average Booking Value": "Средняя стоимость бронирования",
+ "Turlar ro'yxati": "Список туров",
+ "Yangi tur qo'shish": "Добавить новый тур",
+ "Davomiyligi": "Продолжительность",
+ "Narx Oralig'i": "Ценовой диапазон",
+ "Mehmonxona": "Отель",
+ "Imkoniyatlar": "Удобства",
+ "Amallar": "Операции",
+ "kun": "дней",
+ "yulduzli mehmonxona": "звездочный отель",
+ "Bilet turi": "Тип билета",
+ "Batafsil": "Подробнее",
+ "Turni o'chirishni tasdiqlang": "Подтвердите удаление тура",
+ "Haqiqatan ham bu turni o'chirib tashlamoqchimisiz? Bu amalni ortga qaytarib bo'lmaydi.": "Вы действительно хотите удалить этот тур? Это действие невозможно отменить.",
+ "Tur ma'lumotlari": "Информация о туре",
+ "Sarlavha": "Заголовок",
+ "Narx": "Цена",
+ "Ketish joyi": "Место отправления",
+ "Borish joyi": "Место прибытия",
+ "Manzil nomi": "Название места",
+ "Yo‘lovchilar soni": "Количество пассажиров",
+ "Ketish sanasi": "Дата отправления",
+ "Sana tanlang": "Выберите дату",
+ "Ketish vaqti": "Время отправления",
+ "Qaytish sanasi": "Дата возвращения",
+ "Qaytish vaqti": "Время возвращения",
+ "Tillar": "Языки",
+ "Har bir tilni vergul (,) bilan ajrating": "Разделяйте каждый язык запятой (,)",
+ "Tur davomiyligi": "Продолжительность тура",
+ "Belgilar (Badge)": "Значки (Badge)",
+ "Belgilarni tanlang": "Выберите значки",
+ "Tariflar": "Тарифы",
+ "Tarfilarni tanlang": "Выберите тарифы",
+ "Transportlar": "Транспорт",
+ "Transportlarni tanlang": "Выберите транспорт",
+ "Banner rasmi": "Изображение баннера",
+ "Drag or select files": "Выберите файлы",
+ "Drop files here or click to browse": "Перетащите файлы сюда или нажмите, чтобы просмотреть",
+ "Qo‘shimcha rasmlar": "Дополнительные изображения",
+ "Rasmlarni tanlang": "Выберите изображения",
+ "Bir nechta rasm yuklashingiz mumkin": "Вы можете загрузить несколько изображений",
+ "Qulayliklar": "Удобства",
+ "Ikonka tanlang": "Выберите иконку",
+ "Yuklanmoqda...": "Загрузка...",
+ "Qulaylik nomi (masalan: Wi-Fi)": "Название удобства (например: Wi-Fi)",
+ "Qo‘shish": "Добавить",
+ "Mehmonxona haqida": "О отеле",
+ "Mehmonxona xizmatlari": "Услуги отеля",
+ "Yangi xizmat qo‘shish": "Добавить новую услугу",
+ "Xizmat nomi": "Название услуги",
+ "Xizmat tavsifi": "Описание услуги",
+ "Mehmonxona taomlari haqida": "О еде в отеле",
+ "Mehmonxona taomlari": "Еда в отеле",
+ "Mehmonxona taomlari ro'yxati": "Список еды в отеле",
+ "Mehmonxona nomi": "Название отеля",
+ "Mehmonxona raytingi": "Рейтинг отеля",
+ "Meal Plan": "План питания",
+ "Taom rejasini tanlang": "Выберите план питания",
+ "Mehmonxona turi": "Тип отеля",
+ "Mehmonxona turini tanlang": "Выберите тип отеля",
+ "Sarlavha kamida 2 ta belgidan iborat bo‘lishi kerak": "Заголовок должен содержать не менее 2 символов",
+ "Narx kamida 1000 UZS bo‘lishi kerak.": "Цена должна быть не менее 1000 UZS.",
+ "Kamida 1 yo‘lovchi bo‘lishi kerak.": "Должен быть как минимум 1 пассажир.",
+ "Ketish joyi eng kamida 2 ta belgidan iborat bo‘lishi kerak.": "Место отправления должно содержать не менее 2 символов.",
+ "Borish joyi eng kamida 2 ta belgidan iborat bo‘lishi kerak.": "Место прибытия должно содержать не менее 2 символов.",
+ "Eng kamida 2 ta belgidan iborat bo‘lishi kerak.": "Должно содержать не менее 2 символов.",
+ "Majburiy maydon": "Обязательное поле",
+ "Kamida 1kun bo'lishi kerak": "Должно быть не менее 1 дня",
+ "Kamida bitta belgi tanlang.": "Выберите как минимум один значок.",
+ "Banner rasmi majburiy": "Изображение баннера обязательно.",
+ "Kamida bitta rasm yuklang.": "Загрузите как минимум одно изображение.",
+ "Qulaylik nomi majburiy": "Название удобства обязательно",
+ "Icon nomi majburiy": "Название иконки обязательно",
+ "Kamida bitta qulaylik kiriting.": "Введите как минимум одно удобство.",
+ "Kamida bitta xizmat kiriting.": "Введите как минимум одну услугу.",
+ "Taom rejasi tanlanishi majburiy": "Выбор плана питания обязателен",
+ "Mehmonxona turi tanlanishi majburiy": "Выбор типа отеля обязателен",
+ "Tur Sozlamalari": "Настройки туров",
+ "Ovqatlanish": "Питание",
+ "Otel turlari": "Типы отелей",
+ "Qidirish...": "Поиск...",
+ "Yangi qo'shish": "Добавить новый",
+ "Nomi": "Название",
+ "Rang": "Цвет",
+ "Ma'lumot topilmadi": "Данные не найдены",
+ "Tarif nomi": "Название тарифа",
+ "Transport nomi": "Название транспорта",
+ "Tur nomi": "Название тура",
+ "Narxi": "Цена",
+ "kishi": "человека",
+ "Jo'nash sanasi": "Дата отправления",
+ "Umumiy": "Общий",
+ "Marshshrut": "Маршрут",
+ "Xizmatlar": "Услуги",
+ "Sharhlar": "Отзывы",
+ "Tur haqida ma'lumot": "Информация о виде",
+ "Jo'nash joyi": "Место отправления",
+ "Yo'nalish": "Направление",
+ "Tarif": "Тариф",
+ "Sayohat marshshruti": "Маршрут путешествия",
+ "Narxga kiritilgan xizmatlar": "Услуги включены в цену",
+ "Mehmonxona va ovqatlanish": "Гостиницы и питание",
+ "Ovqatlanish tafsilotlari": "Детали питания",
+ "Mijozlar sharhlari": "Отзывы клиентов",
+ "sharh": "комментарий",
+ "Tur firmasi": "Туристическая фирма",
+ "Firma ID": "Фирменный ID",
+ "Firma sahifasiga o'tish": "Перейти на страницу компании",
+ "Bronlar Paneli": "Панель бронирования",
+ "Foydalanuvchi": "Пользователь",
+ "Tour (Agent)": "Тип (Агент)",
+ "Total / Paid": "Итого / Оплачено",
+ "Details": "Подробности",
+ "ta yangilik mavjud": "есть новость об этом",
+ "Yangilik qo'shish": "Добавить обновление",
+ "Hozircha yangilik yo'q": "Пока нет новостей",
+ "Birinchi yangilikni qo'shishni boshlang": "Начните добавлять первую новость",
+ "Yangilikni o'chirishni tasdiqlang": "Подтвердите удаление новости",
+ "Yangilikni tahrirlash": "Редактировать новости",
+ "Yangi yangilik qo‘shish": "Добавить новость",
+ "Yangilik sarlavhasi": "Заголовок новости",
+ "Yangilik ma'lumotlari": "Новостная информация",
+ "Yangilik nomi": "Название новости",
+ "Yangilik haqida": "О новостях",
+ "Kategoriya tanlang": "Выберите категорию",
+ "Kategoriyalar": "Категории",
+ "Keyingisi": "Следующий",
+ "Yangiliklar ro‘yxati": "Список новостей",
+ "Kamida 2 ta belgidan iborat bo‘lishi kerak.": "Должно содержать не менее 2 символов.",
+ "News Categories": "Категории новостей",
+ "Yangi qo‘shish": "Добавить новое",
+ "Kategoriya nomi": "Название категории",
+ "Yangiliklar soni": "Количество новостей",
+ "Harakatlar": "Действия",
+ "Hech qanday kategoriya topilmadi": "Нет категорий",
+ "Kategoriya tahrirlash": "Редактировать категорию",
+ "Yangi kategoriya qo‘shish": "Добавить новую категорию",
+ "FAQ (Savol va javoblar)": "FAQ (Вопросы и ответы)",
+ "Savol": "Вопрос",
+ "Javob": "Ответ",
+ "Bu bo‘limda savollar yo‘q.": "В этом разделе нет вопросов.",
+ "FAQni tahrirlash": "Редактировать FAQ",
+ "Yangi FAQ qo‘shish": "Добавить новый FAQ",
+ "Haqiqatan ham o‘chirmoqchimisiz?": "Вы уверены, что хотите удалить?",
+ "FAQ Kategoriyalar": "FAQ Категории",
+ "Siz muvaffaqiyatli akkountga kirdingiz": "Вы успешно вошли в аккаунт",
+ "Xatolik yuz berdi": "Произошла ошибка",
+ "Ruxsat darajangiz ushbu amalni bajarishga yetarli emas.": "Уровень вашего доступа недостаточен для выполнения этого действия.",
+ "Kirishga huquq yo‘q!": "Нет права входить!",
+ "Ma'lumotlar yuklanmoqda...": "Загрузка данных...",
+ "Ma'lumotlarni yuklashda xatolik yuz berdi.": "При загрузке данных произошла ошибка.",
+ "Qayta urinish": "Повторить попытку",
+ "Umumiy ma'lumot": "Общие сведения",
+ "Agentlik haqida batafsil ma'lumot": "Подробнее об агентстве",
+ "Ma'lumot yo'q": "Нет данных",
+ "Veb-sayt": "Веб-сайт",
+ "ID raqami": "ID номер",
+ "Nomi (ru)": "Название (ru)",
+ "Otel sharoitlari": "гостиничные условия",
+ "Breakfast Only": "Только завтрак",
+ "Half Board": "Полупансион (завтрак и обед или ужин)",
+ "Full Board": "Полный пансион (завтрак, обед и ужин)",
+ "All Inclusive": "Всё включено (питание, напитки и услуги полностью)"
}
diff --git a/src/shared/config/i18n/locales/uz/translation.json b/src/shared/config/i18n/locales/uz/translation.json
index c7b9fa3..380cef8 100644
--- a/src/shared/config/i18n/locales/uz/translation.json
+++ b/src/shared/config/i18n/locales/uz/translation.json
@@ -1,13 +1,332 @@
{
- "welcome": "Uzbek. Bizning saytga xush kelibsiz",
- "language": "Til",
+ "Admin Panelga Kirish": "Admin Panelga Kirish",
+ "Telefon raqam": "Telefon raqam",
+ "Parol": "Parol",
+ "Parolingizni kiriting": "Parolingizni kiriting",
+ "Kirish": "Kirish",
+ "Admin Panel": "Admin Panel",
"Foydalanuvchilar": "Foydalanuvchilar",
"Tur firmalar": "Tur firmalar",
"Xodimlar": "Xodimlar",
"Byudjet": "Byudjet",
"Turlar": "Turlar",
+ "Tur sozlamalari": "Tur sozlamalari",
"Bronlar": "Bronlar",
"Yangiliklar": "Yangiliklar",
- "Yordam Arizalar": "Yordam Arizalar",
- "Tur sozlamalari": "Tur sozlamalari"
+ "Kategoriya": "Kategoriya",
+ "FAQ": "FAQ",
+ "Savollar ro‘yxati": "Savollar ro‘yxati",
+ "Savollar kategoriyasi": "Savollar kategoriyasi",
+ "Arizalar": "Arizalar",
+ "Agentlik arizalari": "Agentlik arizalari",
+ "Yordam arizalari": "Yordam arizalari",
+ "Sayt sozlamalari": "Sayt sozlamalari",
+ "Sayt SEOsi": "Sayt SEOsi",
+ "Offerta": "Offerta",
+ "Yordam pagelari": "Yordam pagelari",
+ "Jami": "Jami",
+ "ta foydalanuvchini boshqaring": "ta foydalanuvchini boshqaring",
+ "Foydalanuvchi Qo'shish": "Foydalanuvchi Qo'shish",
+ "Jami foydalanuvchilar": "Jami foydalanuvchilar",
+ "Email bilan ro'yxatlangan": "Email bilan ro'yxatlangan",
+ "Telefon bilan ro'yxatlangan": "Telefon bilan ro'yxatlangan",
+ "Username, email yoki telefon raqami bo'yicha qidirish": "Username, email yoki telefon raqami bo'yicha qidirish...",
+ "Faol": "Faol",
+ "Ko'rish": "Ko'rish",
+ "Tahrirlash": "Tahrirlash",
+ "O'chirish": "O'chirish",
+ "FAQ Kategoriyalar": "FAQ Kategoriyalar",
+ "Foydalanuvchini o'chirish": "Foydalanuvchini o'chirish",
+ "Siz": "Siz",
+ "foydalanuvchini o'chirmoqchimisiz?": "foydalanuvchini o'chirmoqchimisiz?",
+ "Ushbu amalni qaytarib bo'lmaydi": "Ushbu amalni qaytarib bo'lmaydi.",
+ "Bekor qilish": "Bekor qilish",
+ "Orqaga": "Orqaga",
+ "Yangi foydalanuvchi": "Yangi foydalanuvchi",
+ "Ma'lumotlarni to'ldiring va saqlang": "Ma'lumotlarni to'ldiring va saqlang",
+ "Ismi": "Ismi",
+ "Email": "Email",
+ "Telefon raqami": "Telefon raqami",
+ "Parolni tasdiqlang": "Parolni tasdiqlang",
+ "Saqlash": "Saqlash",
+ "Username majburiy": "Username majburiy",
+ "Username kamida 3 ta belgidan iborat bo'lishi kerak": "Username kamida 3 ta belgidan iborat bo'lishi kerak",
+ "Email yoki telefon raqamlardan kamida bittasi kiritilishi kerak": "Email yoki telefon raqamlardan kamida bittasi kiritilishi kerak",
+ "Email formati noto'g'ri": "Email formati noto'g'ri",
+ "Telefon raqami formati: +998901234567": "Telefon raqami formati: +998901234567",
+ "Parol majburiy": "Parol majburiy",
+ "Parol kamida 8 ta belgidan iborat bo'lishi kerak": "Parol kamida 8 ta belgidan iborat bo'lishi kerak",
+ "Parollar mos kelmaydi": "Parollar mos kelmaydi",
+ "Ma'lumotlarni yangilang": "Ma'lumotlarni yangilang",
+ "Yangilash": "Yangilash",
+ "Nofaol": "Nofaol",
+ "Aloqa ma'lumotlari": "Aloqa ma'lumotlari",
+ "Hisob ma'lumotlari": "Hisob ma'lumotlari",
+ "Yaratilgan sana": "Yaratilgan sana",
+ "Sotib olingan chiptalar": "Sotib olingan chiptalar",
+ "Chipta turi": "Chipta turi",
+ "Xizmat": "Xizmat",
+ "Manzil": "Manzil",
+ "Transport": "Transport",
+ "Jo'nash": "Jo'nash",
+ "Yetish": "Yetish",
+ "Yo'lovchilar": "Yo'lovchilar",
+ "Qo'shimcha xizmatlar": "Qo'shimcha xizmatlar",
+ "Pullik xizmatlar": "Pullik xizmatlar",
+ "Jami narx": "Jami narx",
+ "PDF yuklab olish": "PDF yuklab olish",
+ "Hozircha chiptalar mavjud emas": "Hozircha chiptalar mavjud emas",
+ "Hamrohlar": "Hamrohlar",
+ "Erkak": "Erkak",
+ "Ayol": "Ayol",
+ "Tug'ilgan sana": "Tug'ilgan sana",
+ "Passport rasmlari": "Passport rasmlari",
+ "Hozircha hamrohlar qo'shilmagan": "Hozircha hamrohlar qo'shilmagan",
+ "Statistika": "Statistika",
+ "Chiptalar": "Chiptalar",
+ "Status": "Status",
+ "ID": "ID",
+ "Qo'shimcha ma'lumot": "Qo'shimcha ma'lumot",
+ "Bu foydalanuvchi hozirda tizimda faol holatda. Barcha ma'lumotlar to'liq va tasdiqlangan.": "Bu foydalanuvchi hozirda tizimda faol holatda. Barcha ma'lumotlar to'liq va tasdiqlangan.",
+ "Tur firmalari": "Tur firmalari",
+ "Tur firmalaringizni boshqaring va ularning faoliyatini kuzatib boring.": "Tur firmalaringizni boshqaring va ularning faoliyatini kuzatib boring.",
+ "Jami firmalar": "Jami firmalar",
+ "Faol firmalar": "Faol firmalar",
+ "Jami turlar": "Jami turlar",
+ "Umumiy daromad": "Umumiy daromad",
+ "Komissiya": "Komissiya",
+ "Jami tur": "Jami tur",
+ "Sotilgan tur": "Sotilgan tur",
+ "Daromad": "Daromad",
+ "Egasi": "Egasi",
+ "Qo'shilgan turlar soni": "Qo'shilgan turlar soni",
+ "Sotilgan turlar soni": "Sotilgan turlar soni",
+ "Jami sotilgan turlar": "Jami sotilgan turlar",
+ "Ulush foizi": "Ulush foizi",
+ "Har bir sotuvdan": "Har bir sotuvdan",
+ "so'm daromad": "so'm daromad",
+ "Qo'shilgan turlar": "Qo'shilgan turlar",
+ "Firma tomonidan qo'shilgan barcha turlar ro'yxati": "Firma tomonidan qo'shilgan barcha turlar ro'yxati",
+ "Sotilgan": "Sotilgan",
+ "ta xodim": "ta xodim",
+ "Xodim qo'shish": "Xodim qo'shish",
+ "Operator": "Operator",
+ "Bugalter": "Bugalter",
+ "Manager": "Manager",
+ "Xodimni tahrirlash": "Xodimni tahrirlash",
+ "Familiyasi": "Familiyasi",
+ "Role": "Role",
+ "Qo'shish": "Qo'shish",
+ "Role tanlang": "Role tanlang",
+ "Sayohat moliyasi boshqaruv paneli": "Sayohat moliyasi boshqaruv paneli",
+ "Bronlar, to'lovlar va agentlik moliyalari boshqaruvi": "Bronlar, to'lovlar va agentlik moliyalari boshqaruvi",
+ "Bandlovlar va to‘lovlar": "Bandlovlar va to‘lovlar",
+ "Agentlik hisobotlari": "Agentlik hisobotlari",
+ "Barcha bandlovlar": "Barcha bandlovlar",
+ "To'langan": "To'langan",
+ "Bekor qilindi": "Bekor qilindi",
+ "Kutilmoqda": "Kutilmoqda",
+ "Qaytarilgan": "Qaytarilgan",
+ "Jami daromad": "Jami daromad",
+ "Yakunlangan bandlovlardan": "Yakunlangan bandlovlardan",
+ "Kutilayotgan to‘lovlar": "Kutilayotgan to‘lovlar",
+ "Tasdiqlash kutilmoqda": "Tasdiqlash kutilmoqda",
+ "Tasdiqlangan bandlovlar": "Tasdiqlangan bandlovlar",
+ "Kutilayotgan bandlovlar": "Kutilayotgan bandlovlar",
+ "Oxirgi bandlovlar": "Oxirgi bandlovlar",
+ "Sayohat sanasi": "Sayohat sanasi",
+ "Miqdor": "Miqdor",
+ "Paid": "To'langan",
+ "Pending": "Kutilmoqda",
+ "Cancelled": "Bekor qilindi",
+ "Foydalanuvchi moliyaviy tafsilotlari": "Foydalanuvchi moliyaviy tafsilotlari",
+ "uchun batafsil moliyaviy sharh": "uchun batafsil moliyaviy sharh",
+ "Total Spent": "Jami sarflangan summa",
+ "Total Bookings": "Jami bandlovlar",
+ "All completed bookings": "Barcha yakunlangan bandlovlar",
+ "Pending Payments": "Kutilayotgan to'lovlar",
+ "Awaiting confirmation": "Tasdiqlash kutilmoqda",
+ "All time bookings": "Barcha vaqtlar bandlovlari",
+ "Member Level": "A'zo darajasi",
+ "Loyalty status": "Sodiqlik holati",
+ "Booking History": "Bandlovlar tarixi",
+ "User Details": "Foydalanuvchi tafsilotlari",
+ "Booking Ref": "Bandlov",
+ "Destination": "Manzil",
+ "Travel Dates": "Sayohat sanalari",
+ "Travelers": "Sayohatchilar",
+ "Amount": "Miqdor",
+ "Booked on": "Band qilingan sana",
+ "Personal Information": "Shaxsiy ma'lumotlar",
+ "Full Name": "To'liq ism",
+ "Phone Number": "Telefon raqami",
+ "Email Address": "Email manzili",
+ "Member Since": "A'zo bo'lgan sana",
+ "Travel Statistics": "Sayohat statistikasi",
+ "Favorite Destination": "Sevimli manzil",
+ "bookings": "bandlovlar",
+ "Preferred Agency": "Afzal ko'rilgan agentlik",
+ "out of": "ichidan",
+ "Average Booking Value": "O'rtacha bandlov qiymati",
+ "Turlar ro'yxati": "Turlar ro'yxati",
+ "Yangi tur qo'shish": "Yangi tur qo'shish",
+ "Davomiyligi": "Davomiyligi",
+ "Narx Oralig'i": "Narx Oralig'i",
+ "Mehmonxona": "Mehmonxona",
+ "Imkoniyatlar": "Imkoniyatlar",
+ "Amallar": "Amallar",
+ "kun": "kun",
+ "Otel sharoitlari": "Otel sharoitlari",
+ "yulduzli mehmonxona": "yulduzli mehmonxona",
+ "Bilet turi": "Bilet turi",
+ "Batafsil": "Batafsil",
+ "Turni o'chirishni tasdiqlang": "Turni o'chirishni tasdiqlang",
+ "Haqiqatan ham bu turni o'chirib tashlamoqchimisiz? Bu amalni ortga qaytarib bo'lmaydi.": "Haqiqatan ham bu turni o'chirib tashlamoqchimisiz? Bu amalni ortga qaytarib bo'lmaydi.",
+ "Tur ma'lumotlari": "Tur ma'lumotlari",
+ "Sarlavha": "Sarlavha",
+ "Narx": "Narx",
+ "Ketish joyi": "Ketish joyi",
+ "Borish joyi": "Borish joyi",
+ "Manzil nomi": "Manzil nomi",
+ "Yo‘lovchilar soni": "Yo‘lovchilar soni",
+ "Ketish sanasi": "Ketish sanasi",
+ "Sana tanlang": "Sana tanlang",
+ "Ketish vaqti": "Ketish vaqti",
+ "Qaytish sanasi": "Qaytish sanasi",
+ "Qaytish vaqti": "Qaytish vaqti",
+ "Tillar": "Tillar",
+ "Har bir tilni vergul (,) bilan ajrating": "Har bir tilni vergul (,) bilan ajrating",
+ "Tur davomiyligi": "Tur davomiyligi",
+ "Belgilar (Badge)": "Belgilar (Badge)",
+ "Belgilarni tanlang": "Belgilarni tanlang",
+ "Tariflar": "Tariflar",
+ "Tarfilarni tanlang": "Tarfilarni tanlang",
+ "Transportlar": "Transportlar",
+ "Transportlarni tanlang": "Transportlarni tanlang",
+ "Banner rasmi": "Banner rasmi",
+ "Drag or select files": "Faylni tanlang",
+ "Drop files here or click to browse": "Fayllarni shu yerga tashlang yoki ko'rib chiqish uchun bosing",
+ "Qo‘shimcha rasmlar": "Qo‘shimcha rasmlar",
+ "Rasmlarni tanlang": "Rasmlarni tanlang",
+ "Bir nechta rasm yuklashingiz mumkin": "Bir nechta rasm yuklashingiz mumkin",
+ "Qulayliklar": "Qulayliklar",
+ "Ikonka tanlang": "Ikonka tanlang",
+ "Yuklanmoqda...": "Yuklanmoqda...",
+ "Qulaylik nomi (masalan: Wi-Fi)": "Qulaylik nomi (masalan: Wi-Fi)",
+ "Qo‘shish": "Qo‘shish",
+ "Mehmonxona haqida": "Mehmonxona haqida",
+ "Mehmonxona xizmatlari": "Mehmonxona xizmatlari",
+ "Yangi xizmat qo‘shish": "Yangi xizmat qo‘shish",
+ "Xizmat nomi": "Xizmat nomi",
+ "Xizmat tavsifi": "Xizmat tavsifi",
+ "Mehmonxona taomlari haqida": "Mehmonxona taomlari haqida",
+ "Mehmonxona taomlari": "Mehmonxona taomlari",
+ "Mehmonxona taomlari ro'yxati": "Mehmonxona taomlari ro'yxati",
+ "Mehmonxona nomi": "Mehmonxona nomi",
+ "Mehmonxona raytingi": "Mehmonxona raytingi",
+ "Meal Plan": "Taom rejasi",
+ "Taom rejasini tanlang": "Taom rejasini tanlang",
+ "Mehmonxona turi": "Mehmonxona turi",
+ "Mehmonxona turini tanlang": "Mehmonxona turini tanlang",
+ "Narx kamida 1000 UZS bo‘lishi kerak.": "Narx kamida 1000 UZS bo‘lishi kerak.",
+ "Kamida 1 yo‘lovchi bo‘lishi kerak.": "Kamida 1 yo‘lovchi bo‘lishi kerak.",
+ "Sarlavha kamida 2 ta belgidan iborat bo‘lishi kerak": "Sarlavha kamida 2 ta belgidan iborat bo‘lishi kerak",
+ "Ketish joyi eng kamida 2 ta belgidan iborat bo‘lishi kerak.": "Ketish joyi eng kamida 2 ta belgidan iborat bo‘lishi kerak.",
+ "Borish joyi eng kamida 2 ta belgidan iborat bo‘lishi kerak.": "Borish joyi eng kamida 2 ta belgidan iborat bo‘lishi kerak.",
+ "Eng kamida 2 ta belgidan iborat bo‘lishi kerak.": "Eng kamida 2 ta belgidan iborat bo‘lishi kerak.",
+ "Majburiy maydon": "Majburiy maydon",
+ "Kamida 1kun bo'lishi kerak": "Kamida 1kun bo'lishi kerak",
+ "Kamida bitta belgi tanlang.": "Kamida bitta belgi tanlang.",
+ "Banner rasmi majburiy": "Banner rasmi majburiy.",
+ "Kamida bitta rasm yuklang.": "Kamida bitta rasm yuklang.",
+ "Qulaylik nomi majburiy": "Qulaylik nomi majburiy.",
+ "Icon nomi majburiy": "Icon nomi majburiy.",
+ "Kamida bitta qulaylik kiriting.": "Kamida bitta qulaylik kiriting.",
+ "Kamida bitta xizmat kiriting.": "Kamida bitta xizmat kiriting.",
+ "Taom rejasi tanlanishi majburiy": "Taom rejasi tanlanishi majburiy.",
+ "Mehmonxona turi tanlanishi majburiy": "Mehmonxona turi tanlanishi majburiy.",
+ "Tur Sozlamalari": "Tur Sozlamalari",
+ "Ovqatlanish": "Ovqatlanish",
+ "Otel turlari": "Otel turlari",
+ "Qidirish...": "Qidirish...",
+ "Yangi qo'shish": "Yangi qo'shish",
+ "Nomi": "Nomi",
+ "Rang": "Rang",
+ "Ma'lumot topilmadi": "Ma'lumot topilmadi",
+ "Tarif nomi": "Tarif nomi",
+ "Transport nomi": "Transport nomi",
+ "Tur nomi": "Tur nomi",
+ "Narxi": "Narxi",
+ "kishi": "kishi",
+ "Jo'nash sanasi": "Jo'nash sanasi",
+ "Umumiy": "Umumiy",
+ "Marshshrut": "Marshshrut",
+ "Xizmatlar": "Xizmatlar",
+ "Sharhlar": "Sharhlar",
+ "Tur haqida ma'lumot": "Tur haqida ma'lumot",
+ "Jo'nash joyi": "Jo'nash joyi",
+ "Yo'nalish": "Yo'nalish",
+ "Tarif": "Tarif",
+ "Sayohat marshshruti": "Sayohat marshshruti",
+ "Narxga kiritilgan xizmatlar": "Narxga kiritilgan xizmatlar",
+ "Mehmonxona va ovqatlanish": "Mehmonxona va ovqatlanish",
+ "Ovqatlanish tafsilotlari": "Ovqatlanish tafsilotlari",
+ "Mijozlar sharhlari": "Mijozlar sharhlari",
+ "sharh": "sharh",
+ "Tur firmasi": "Tur firmasi",
+ "Firma ID": "Firma ID",
+ "Firma sahifasiga o'tish": "Firma sahifasiga o'tish",
+ "Bronlar Paneli": "Bronlar Paneli",
+ "Foydalanuvchi": "Foydalanuvchi",
+ "Tour (Agent)": "Tur (Agent)",
+ "Total / Paid": "Total / Paid",
+ "Details": "Batafsil",
+ "ta yangilik mavjud": "ta yangilik mavjud",
+ "Yangilik qo'shish": "Yangilik qo'shish",
+ "Hozircha yangilik yo'q": "'Hozircha yangilik yo'q",
+ "Birinchi yangilikni qo'shishni boshlang": "Birinchi yangilikni qo'shishni boshlang",
+ "Yangilikni o'chirishni tasdiqlang": "Yangilikni o'chirishni tasdiqlang",
+ "Yangilikni tahrirlash": "Yangilikni tahrirlash",
+ "Yangi yangilik qo‘shish": "Yangi yangilik qo‘shish",
+ "Yangilik sarlavhasi": "Yangilik sarlavhasi",
+ "Yangilik ma'lumotlari": "Yangilik ma'lumotlari",
+ "Yangilik nomi": "Yangilik nomi",
+ "Yangilik haqida": "Yangilik haqida",
+ "Kategoriya tanlang": "Kategoriya tanlang",
+ "Kategoriyalar": "Kategoriyalar",
+ "Keyingisi": "Keyingisi",
+ "Yangiliklar ro‘yxati": "Yangiliklar ro‘yxati",
+ "Kamida 2 ta belgidan iborat bo‘lishi kerak.": "Kamida 2 ta belgidan iborat bo‘lishi kerak.",
+ "News Categories": "Yangiliklar turkumlari",
+ "Yangi qo‘shish": "Yangi qo‘shish",
+ "Kategoriya nomi": "Kategoriya nomi",
+ "Yangiliklar soni": "Yangiliklar soni",
+ "Harakatlar": "Harakatlar",
+ "Hech qanday kategoriya topilmadi": "Hech qanday kategoriya topilmadi",
+ "Kategoriya tahrirlash": "Kategoriya tahrirlash",
+ "Yangi kategoriya qo‘shish": "Yangi kategoriya qo‘shish",
+ "FAQ (Savol va javoblar)": "FAQ (Savol va javoblar)",
+ "Savol": "Savol",
+ "Javob": "Javob",
+ "Bu bo‘limda savollar yo‘q.": "Bu bo‘limda savollar yo‘q.",
+ "FAQni tahrirlash": "FAQni tahrirlash",
+ "Yangi FAQ qo‘shish": "Yangi FAQ qo‘shish",
+ "Haqiqatan ham o‘chirmoqchimisiz?": "Haqiqatan ham o‘chirmoqchimisiz?",
+ "Siz muvaffaqiyatli akkountga kirdingiz": "Siz muvaffaqiyatli akkountga kirdingiz",
+ "Ma'lumotlar yuklanmoqda...": "Ma'lumotlar yuklanmoqda...",
+ "Xatolik yuz berdi": "Xatolik yuz berdi",
+ "Ruxsat darajangiz ushbu amalni bajarishga yetarli emas.": "Ruxsat darajangiz ushbu amalni bajarishga yetarli emas.",
+ "Kirishga huquq yo‘q!": "Kirishga huquq yo‘q!",
+ "Ma'lumotlarni yuklashda xatolik yuz berdi.": "Ma'lumotlarni yuklashda xatolik yuz berdi.",
+ "Qayta urinish": "Qayta urinish",
+ "Umumiy ma'lumot": "Umumiy ma'lumot",
+ "Agentlik haqida batafsil ma'lumot": "Agentlik haqida batafsil ma'lumot",
+ "Ma'lumot yo'q": "Ma'lumot yo'q",
+ "Veb-sayt": "Veb-sayt",
+ "ID raqami": "ID raqami",
+ "Nomi (ru)": "Nomi (ru)",
+ "Breakfast Only": "Faqat nonushta",
+ "Half Board": "Yarim pansion (nonushta va tushlik yoki kechki ovqat)",
+ "Full Board": "To‘liq pansion (nonushta, tushlik va kechki ovqat)",
+ "All Inclusive": "To‘liq pansion (nonushta, tushlik va kechki ovqat)"
}
diff --git a/src/shared/hooks/user.ts b/src/shared/hooks/user.ts
new file mode 100644
index 0000000..823b5c7
--- /dev/null
+++ b/src/shared/hooks/user.ts
@@ -0,0 +1,42 @@
+import { create } from "zustand";
+
+type User = {
+ id: number;
+ last_login: string;
+ is_superuser: boolean;
+ first_name: string;
+ last_name: string;
+ is_staff: boolean;
+ is_active: boolean;
+ date_joined: string;
+ phone: string;
+ email: string;
+ username: string;
+ avatar: string;
+ validated_at: string;
+ role:
+ | "superuser"
+ | "admin"
+ | "moderator"
+ | "tour_admin"
+ | "buxgalter"
+ | "operator"
+ | "user";
+ travel_agency: number;
+} | null;
+
+interface UserStore {
+ user: User;
+ setUser: (user: User) => void;
+ clearUser: () => void;
+}
+
+const useUserStore = create((set) => ({
+ user: null,
+
+ setUser: (user) => set({ user }),
+
+ clearUser: () => set({ user: null }),
+}));
+
+export default useUserStore;
diff --git a/src/shared/lib/authCookies.ts b/src/shared/lib/authCookies.ts
new file mode 100644
index 0000000..2806195
--- /dev/null
+++ b/src/shared/lib/authCookies.ts
@@ -0,0 +1,57 @@
+import Cookies from "js-cookie";
+
+const TOKEN_KEY = "auth_token"; // cookie nomi
+const REF_TOKEN_KEY = "ref_auth_token"; // cookie nomi
+const EXPIRE_DAYS = 1; // token necha kun saqlansin
+const REF_EXPIRE_DAYS = 30; // token necha kun saqlansin
+
+/**
+ * Tokenni cookie'ga saqlaydi
+ * @param token - foydalanuvchining JWT tokeni
+ */
+export const setAuthToken = (token: string) => {
+ Cookies.set(TOKEN_KEY, token, {
+ expires: EXPIRE_DAYS,
+ secure: true, // faqat https da ishlaydi
+ sameSite: "strict", // CSRF xavfsizligi
+ });
+};
+
+export const setAuthRefToken = (token: string) => {
+ Cookies.set(REF_TOKEN_KEY, token, {
+ expires: REF_EXPIRE_DAYS,
+ secure: true, // faqat https da ishlaydi
+ sameSite: "strict", // CSRF xavfsizligi
+ });
+};
+
+/**
+ * Cookie'dan tokenni oladi
+ * @returns string | undefined
+ */
+export const getAuthToken = (): string | undefined => {
+ return Cookies.get(TOKEN_KEY);
+};
+
+export const getRefAuthToken = (): string | undefined => {
+ return Cookies.get(REF_TOKEN_KEY);
+};
+
+/**
+ * Tokenni cookie'dan o‘chiradi
+ */
+export const removeAuthToken = () => {
+ Cookies.remove(TOKEN_KEY);
+};
+
+export const removeRefAuthToken = () => {
+ Cookies.remove(REF_TOKEN_KEY);
+};
+
+/**
+ * Foydalanuvchi tizimga kirgan yoki yo‘qligini tekshiradi
+ * @returns boolean
+ */
+export const isAuthenticated = (): boolean => {
+ return !!Cookies.get(TOKEN_KEY);
+};
diff --git a/src/shared/lib/formatPrice.ts b/src/shared/lib/formatPrice.ts
index 6b87196..4afc4e6 100644
--- a/src/shared/lib/formatPrice.ts
+++ b/src/shared/lib/formatPrice.ts
@@ -13,7 +13,7 @@ const formatPrice = (amount: number | string, withLabel = false): string => {
? locale === LanguageRoutes.RU
? " сум"
: locale === LanguageRoutes.UZ
- ? " сўм"
+ ? " so‘m"
: " so‘m"
: "";
diff --git a/src/shared/lib/onlyNumber.ts b/src/shared/lib/onlyNumber.ts
new file mode 100644
index 0000000..bfff8dd
--- /dev/null
+++ b/src/shared/lib/onlyNumber.ts
@@ -0,0 +1,6 @@
+const onlyNumber = (digits: string | number) => {
+ const phone = digits.toString();
+ return phone.replace(/\D/g, "");
+};
+
+export default onlyNumber;
diff --git a/src/shared/ui/avatar.tsx b/src/shared/ui/avatar.tsx
new file mode 100644
index 0000000..8a6dbf4
--- /dev/null
+++ b/src/shared/ui/avatar.tsx
@@ -0,0 +1,51 @@
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/shared/lib/utils"
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/src/shared/ui/dialog.tsx b/src/shared/ui/dialog.tsx
index 213edd9..69fede7 100644
--- a/src/shared/ui/dialog.tsx
+++ b/src/shared/ui/dialog.tsx
@@ -1,8 +1,8 @@
"use client";
-import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { XIcon } from "lucide-react";
+import * as React from "react";
import { cn } from "@/shared/lib/utils";
diff --git a/src/shared/ui/form.tsx b/src/shared/ui/form.tsx
index 3d91ea4..9c1cd0e 100644
--- a/src/shared/ui/form.tsx
+++ b/src/shared/ui/form.tsx
@@ -1,8 +1,8 @@
"use client";
-import * as React from "react";
import * as LabelPrimitive from "@radix-ui/react-label";
import { Slot } from "@radix-ui/react-slot";
+import * as React from "react";
import {
Controller,
FormProvider,
@@ -15,6 +15,7 @@ import {
import { cn } from "@/shared/lib/utils";
import { Label } from "@/shared/ui/label";
+import { useTranslation } from "react-i18next";
const Form = FormProvider;
@@ -138,6 +139,7 @@ function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
const { error, formMessageId } = useFormField();
+ const { t } = useTranslation();
const body = error ? String(error?.message ?? "") : props.children;
if (!body) {
@@ -151,18 +153,18 @@ function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
className={cn("text-destructive text-sm", className)}
{...props}
>
- {body}
+ {t(error?.message ? error.message : "")}
);
}
export {
- useFormField,
Form,
- FormItem,
- FormLabel,
FormControl,
FormDescription,
- FormMessage,
FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+ useFormField,
};
diff --git a/src/shared/ui/iocnSelect.tsx b/src/shared/ui/iocnSelect.tsx
index 6108fcb..ba1cec1 100644
--- a/src/shared/ui/iocnSelect.tsx
+++ b/src/shared/ui/iocnSelect.tsx
@@ -20,6 +20,7 @@ import React, {
type ComponentType,
type LazyExoticComponent,
} from "react";
+import { useTranslation } from "react-i18next";
// 🔹 Lazy icon faqat tanlangan icon uchun
const LazyIcon: React.FC<{ name: string }> = ({ name }) => {
@@ -49,6 +50,7 @@ const IconSelect: React.FC = ({
setSelectedIcon,
}) => {
const [icons, setIcons] = useState([]);
+ const { t } = useTranslation();
const [visibleIcons, setVisibleIcons] = useState([]);
const [chunkSize] = useState(100);
const [index, setIndex] = useState(1);
@@ -116,14 +118,14 @@ const IconSelect: React.FC = ({
onOpenChange={handleOpenChange}
>
-
+
{selectedIcon ? (
{selectedIcon}
) : (
- "Ikonka tanlang"
+ t("Ikonka tanlang")
)}
@@ -151,7 +153,9 @@ const IconSelect: React.FC = ({
{!searchTerm && isOpen && (
{visibleIcons.length < icons.length && (
- Yuklanmoqda...
+
+ {t("Yuklanmoqda...")}
+
)}
)}
diff --git a/src/shared/ui/radio-group.tsx b/src/shared/ui/radio-group.tsx
new file mode 100644
index 0000000..2c75a33
--- /dev/null
+++ b/src/shared/ui/radio-group.tsx
@@ -0,0 +1,43 @@
+import * as React from "react"
+import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
+import { CircleIcon } from "lucide-react"
+
+import { cn } from "@/shared/lib/utils"
+
+function RadioGroup({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function RadioGroupItem({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+ )
+}
+
+export { RadioGroup, RadioGroupItem }
diff --git a/src/shared/ui/sonner.tsx b/src/shared/ui/sonner.tsx
new file mode 100644
index 0000000..9f46e06
--- /dev/null
+++ b/src/shared/ui/sonner.tsx
@@ -0,0 +1,38 @@
+import {
+ CircleCheckIcon,
+ InfoIcon,
+ Loader2Icon,
+ OctagonXIcon,
+ TriangleAlertIcon,
+} from "lucide-react"
+import { useTheme } from "next-themes"
+import { Toaster as Sonner, type ToasterProps } from "sonner"
+
+const Toaster = ({ ...props }: ToasterProps) => {
+ const { theme = "system" } = useTheme()
+
+ return (
+ ,
+ info: ,
+ warning: ,
+ error: ,
+ loading: ,
+ }}
+ style={
+ {
+ "--normal-bg": "var(--popover)",
+ "--normal-text": "var(--popover-foreground)",
+ "--normal-border": "var(--border)",
+ "--border-radius": "var(--radius)",
+ } as React.CSSProperties
+ }
+ {...props}
+ />
+ )
+}
+
+export { Toaster }
diff --git a/src/widgets/lang-toggle/ui/lang-toggle.tsx b/src/widgets/lang-toggle/ui/lang-toggle.tsx
index 29ab8f9..1308837 100644
--- a/src/widgets/lang-toggle/ui/lang-toggle.tsx
+++ b/src/widgets/lang-toggle/ui/lang-toggle.tsx
@@ -1,3 +1,4 @@
+import httpClient from "@/shared/config/api/httpClient";
import { LanguageRoutes } from "@/shared/config/i18n/type";
import { Button } from "@/shared/ui/button";
import {
@@ -7,12 +8,16 @@ import {
DropdownMenuTrigger,
} from "@/shared/ui/dropdown-menu";
import { languages } from "@/widgets/lang-toggle/lib/data";
+import { useQueryClient } from "@tanstack/react-query";
import { GlobeIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
const LangToggle = () => {
const { i18n } = useTranslation();
+ const queryClient = useQueryClient();
const changeLanguage = (lng: LanguageRoutes) => {
+ httpClient.defaults.headers.common["Accept-Language"] = lng;
+ queryClient.refetchQueries();
i18n.changeLanguage(lng);
};
diff --git a/src/widgets/real-pagination/ui/RealPagination.tsx b/src/widgets/real-pagination/ui/RealPagination.tsx
new file mode 100644
index 0000000..34af79a
--- /dev/null
+++ b/src/widgets/real-pagination/ui/RealPagination.tsx
@@ -0,0 +1,109 @@
+"use client";
+
+import type { Table } from "@tanstack/react-table";
+import { ChevronLeft, ChevronRight } from "lucide-react";
+import { useCallback, useEffect } from "react";
+import { useSearchParams } from "react-router-dom";
+
+interface Props {
+ table: Table;
+ totalPages?: number;
+ namePage?: string;
+ namePageSize?: string;
+}
+
+const RealPagination = ({
+ table,
+ totalPages = 1,
+ namePage,
+ namePageSize,
+}: Props) => {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const currentPage = table.getState().pagination.pageIndex + 1;
+
+ const updateUrl = useCallback(
+ (page: number, pageSize: number) => {
+ const newParams = new URLSearchParams(searchParams);
+ newParams.set(namePage ? namePage : "page", page.toString());
+ newParams.set(
+ namePageSize ? namePageSize : "pageSize",
+ pageSize.toString(),
+ );
+ setSearchParams(newParams);
+ },
+ [searchParams, setSearchParams],
+ );
+
+ useEffect(() => {
+ const urlPage = parseInt(searchParams.get("page") || "1", 10);
+ const urlPageSize = parseInt(searchParams.get("pageSize") || "10", 10);
+
+ if (urlPage - 1 !== table.getState().pagination.pageIndex) {
+ table.setPageIndex(urlPage - 1);
+ }
+ if (urlPageSize !== table.getState().pagination.pageSize) {
+ table.setPageSize(urlPageSize);
+ }
+ }, [searchParams, table]);
+
+ const handlePrev = () => {
+ if (currentPage > 1) {
+ table.setPageIndex(currentPage - 2);
+ updateUrl(currentPage - 1, table.getState().pagination.pageSize);
+ }
+ };
+
+ const handleNext = () => {
+ if (currentPage < totalPages) {
+ table.setPageIndex(currentPage);
+ updateUrl(currentPage + 1, table.getState().pagination.pageSize);
+ }
+ };
+
+ const handlePageClick = (page: number) => {
+ table.setPageIndex(page - 1);
+ updateUrl(page, table.getState().pagination.pageSize);
+ };
+
+ return (
+
+ {/* Previous Button */}
+
+
+
+
+ {/* Page Numbers */}
+ {[...Array(totalPages)].map((_, i) => (
+ handlePageClick(i + 1)}
+ className={`px-4 py-2 rounded-lg border transition-all font-medium ${
+ currentPage === 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}
+
+ ))}
+
+ {/* Next Button */}
+
+
+
+
+ );
+};
+
+export default RealPagination;
diff --git a/src/widgets/sidebar/ui/Sidebar.tsx b/src/widgets/sidebar/ui/Sidebar.tsx
index 73b1048..997551f 100644
--- a/src/widgets/sidebar/ui/Sidebar.tsx
+++ b/src/widgets/sidebar/ui/Sidebar.tsx
@@ -13,7 +13,6 @@ import LangToggle from "@/widgets/lang-toggle/ui/lang-toggle";
import {
Briefcase,
Building2,
- CalendarCheck2,
ChevronDown,
ChevronRight,
HelpCircle,
@@ -29,44 +28,83 @@ import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom";
-type Role = "admin" | "manager" | "user";
+type Role =
+ | "superuser"
+ | "admin"
+ | "moderator"
+ | "tour_admin"
+ | "buxgalter"
+ | "operator"
+ | "user";
interface SidebarProps {
role: Role;
}
-/** --- MENYU TUZILMASI --- **/
const MENU_ITEMS = [
- { label: "Foydalanuvchilar", icon: Users, path: "/user", roles: ["admin"] },
+ {
+ label: "Foydalanuvchilar",
+ icon: Users,
+ path: "/user",
+ roles: ["moderator", "admin", "superuser", "moderator"],
+ },
{
label: "Tur firmalar",
icon: Building2,
path: "/agencies",
- roles: ["admin", "manager"],
+ roles: ["moderator", "admin", "superuser", "moderator"],
+ },
+ {
+ label: "Xodimlar",
+ icon: Briefcase,
+ path: "/employees",
+ roles: ["moderator", "admin", "superuser"],
+ },
+ {
+ label: "Bronlar",
+ icon: Wallet,
+ path: "/finance",
+ roles: ["moderator", "admin", "superuser", "buxgalter"],
},
- { label: "Xodimlar", icon: Briefcase, path: "/employees", roles: ["admin"] },
- { label: "Byudjet", icon: Wallet, path: "/finance", roles: ["admin"] },
{
label: "Turlar",
icon: Plane,
path: "/tours",
- roles: ["admin", "manager"],
+ roles: [
+ "moderator",
+ "admin",
+ "superuser",
+ "tour_admin",
+ "operator",
+ "buxgalter",
+ ],
children: [
{ label: "Turlar", path: "/tours" },
- { label: "Tur sozlamalari", path: "/tours/setting" },
+ {
+ label: "Tur sozlamalari",
+ path: "/tours/setting",
+ roles: ["moderator", "admin", "superuser"],
+ },
],
},
- {
- label: "Bronlar",
- icon: CalendarCheck2,
- path: "/bookings",
- roles: ["admin", "manager", "user"],
- },
+ // {
+ // label: "Bronlar",
+ // icon: CalendarCheck2,
+ // path: "/bookings",
+ // roles: [
+ // "moderator",
+ // "admin",
+ // "superuser",
+ // "tour_admin",
+ // "operator",
+ // "buxgalter",
+ // ],
+ // },
{
label: "Yangiliklar",
icon: Newspaper,
path: "/news",
- roles: ["admin", "manager"],
+ roles: ["moderator", "admin", "superuser"],
children: [
{ label: "Yangiliklar", path: "/news" },
{ label: "Kategoriya", path: "/news/categories" },
@@ -76,7 +114,7 @@ const MENU_ITEMS = [
label: "FAQ",
icon: MessageSquare,
path: "/faq",
- roles: ["admin"],
+ roles: ["moderator", "admin", "superuser"],
children: [
{ label: "Savollar ro‘yxati", path: "/faq" },
{ label: "Savollar kategoriyasi", path: "/faq/categories" },
@@ -86,17 +124,32 @@ const MENU_ITEMS = [
label: "Arizalar",
icon: HelpCircle,
path: "/support",
- roles: ["admin", "manager"],
+ roles: ["moderator", "admin", "superuser", "tour_admin", "operator"],
children: [
- { label: "Agentlik arizalari", path: "/support/tours", roles: ["admin"] },
- { label: "Yordam arizalari", path: "/support/user", roles: ["admin"] },
+ {
+ label: "Agentlik arizalari",
+ path: "/support/tours",
+ roles: ["moderator", "admin", "superuser", "operator"],
+ },
+ {
+ label: "Yordam arizalari",
+ path: "/support/user",
+ roles: [
+ "moderator",
+ "admin",
+ "superuser",
+ "tour_admin",
+ "operator",
+ "buxgalter",
+ ],
+ },
],
},
{
- label: "Tur sozlamalari",
+ label: "Sayt sozlamalari",
icon: Settings,
path: "/tour-settings",
- roles: ["admin"],
+ roles: ["moderator", "admin", "superuser"],
children: [
{ label: "Sayt SEOsi", path: "/site-seo/" },
{ label: "Offerta", path: "/site-pages/" },
@@ -201,7 +254,7 @@ export function Sidebar({ role }: SidebarProps) {
return (
-
+
diff --git a/vite.config.ts b/vite.config.ts
index 92d8455..b9d0a86 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -4,10 +4,12 @@ import path from "path";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
-// https://vite.dev/config/
export default defineConfig({
plugins: [react(), tailwindcss(), tsconfigPaths()],
resolve: {
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
},
+ server: {
+ allowedHosts: ["71ad80caca04.ngrok-free.app"],
+ },
});