From a9e99f9755000abeb074ada7ebe82754bb1194e3 Mon Sep 17 00:00:00 2001 From: Samandar Turgunboyev Date: Mon, 27 Oct 2025 12:54:47 +0500 Subject: [PATCH] added tour --- src/pages/faq/lib/api.ts | 57 +++++++ src/pages/faq/lib/type.ts | 50 ++++++ src/pages/faq/ui/Faq.tsx | 221 ++++++++++++-------------- src/pages/faq/ui/FaqCategory.tsx | 259 ++++++++++++++++++++++--------- src/pages/tours/lib/form.ts | 5 +- src/pages/tours/ui/StepOne.tsx | 2 +- src/pages/tours/ui/StepTwo.tsx | 6 +- src/pages/users/ui/User.tsx | 5 +- src/shared/config/api/URLs.ts | 4 + src/shared/lib/formatPrice.ts | 13 +- 10 files changed, 405 insertions(+), 217 deletions(-) create mode 100644 src/pages/faq/lib/api.ts create mode 100644 src/pages/faq/lib/type.ts diff --git a/src/pages/faq/lib/api.ts b/src/pages/faq/lib/api.ts new file mode 100644 index 0000000..bb9d080 --- /dev/null +++ b/src/pages/faq/lib/api.ts @@ -0,0 +1,57 @@ +import type { Faq, FaqCategory, FaqCategoryDetail } from "@/pages/faq/lib/type"; +import httpClient from "@/shared/config/api/httpClient"; +import { FAQ, FAQ_CATEGORIES } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +const getAllFaq = async (params: { + page: number; + page_size: number; +}): Promise> => { + const res = await httpClient.get(FAQ, { params }); + return res; +}; + +const getAllFaqCategory = async (params: { + page: number; + page_size: number; +}): Promise> => { + const res = await httpClient.get(FAQ_CATEGORIES, { params }); + return res; +}; + +const getDetailFaqCategory = async ( + id: number, +): Promise> => { + const res = await httpClient.get(`${FAQ_CATEGORIES}${id}/`); + return res; +}; + +const createFaqCategory = async (body: { name: string; name_ru: string }) => { + const res = await httpClient.post(FAQ_CATEGORIES, body); + return res; +}; + +const updateFaqCategory = async ({ + body, + id, +}: { + id: number; + body: { name: string; name_ru: string }; +}) => { + const res = await httpClient.patch(`${FAQ_CATEGORIES}${id}/`, body); + return res; +}; + +const deleteFaqCategory = async (id: number) => { + const res = await httpClient.delete(`${FAQ_CATEGORIES}${id}/`); + return res; +}; + +export { + createFaqCategory, + deleteFaqCategory, + getAllFaq, + getAllFaqCategory, + getDetailFaqCategory, + updateFaqCategory, +}; diff --git a/src/pages/faq/lib/type.ts b/src/pages/faq/lib/type.ts new file mode 100644 index 0000000..1e7ed5f --- /dev/null +++ b/src/pages/faq/lib/type.ts @@ -0,0 +1,50 @@ +export interface FaqCategory { + status: boolean; + data: { + links: { + previous: string; + next: string; + }; + total_items: number; + total_pages: number; + page_size: number; + current_page: number; + results: { + id: number; + name: string; + }[]; + }; +} + +export interface FaqCategoryDetail { + status: boolean; + data: { + id: number; + name: string; + name_ru: string; + }; +} + +export interface Faq { + status: boolean; + data: { + links: { + previous: string; + next: string; + }; + total_items: number; + total_pages: number; + page_size: number; + current_page: number; + results: { + id: number; + title: string; + title_ru: string; + text: string; + text_ru: string; + category: { + name: string; + }; + }[]; + }; +} diff --git a/src/pages/faq/ui/Faq.tsx b/src/pages/faq/ui/Faq.tsx index 26006a2..f32cf64 100644 --- a/src/pages/faq/ui/Faq.tsx +++ b/src/pages/faq/ui/Faq.tsx @@ -1,5 +1,6 @@ "use client"; +import { getAllFaq, getAllFaqCategory } from "@/pages/faq/lib/api"; import { Button } from "@/shared/ui/button"; import { Dialog, @@ -21,7 +22,6 @@ import { Select, SelectContent, SelectGroup, - SelectItem, SelectLabel, SelectTrigger, SelectValue, @@ -37,63 +37,13 @@ import { import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/shared/ui/tabs"; import { Textarea } from "@/shared/ui/textarea"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useQuery } from "@tanstack/react-query"; import { Pencil, PlusCircle, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import z from "zod"; -type FaqType = { - id: number; - category: string; - question: string; - answer: string; -}; - -const categories = [ - { value: "umumiy", label: "Umumiy" }, - { value: "tolov", label: "To‘lov" }, - { value: "hujjatlar", label: "Hujjatlar" }, - { value: "sugurta", label: "Sug‘urta" }, -]; - -const initialFaqs: FaqType[] = [ - { - id: 1, - category: "umumiy", - question: "Sayohatni bron qilish uchun qanday to‘lov usullari mavjud?", - answer: - "Biz kredit karta, Payme, Click, va naqd to‘lovni qabul qilamiz. To‘lovlar xavfsiz va ishonchli tizim orqali amalga oshiriladi.", - }, - { - id: 2, - category: "umumiy", - question: "Sayohatni bekor qilsam, pul qaytariladimi?", - answer: - "Ha, ammo bu bron qilingan turdagi sayohatga bog‘liq. Ba’zi sayohatlar uchun 24 soat oldin bekor qilsangiz, to‘liq qaytariladi.", - }, - { - id: 3, - category: "hujjatlar", - question: "Pasport muddati tugasa sayohat qilish mumkinmi?", - answer: - "Yo‘q, pasport muddati kamida 6 oy amal qilishi kerak. Aks holda, mamlakatga kirish rad etiladi.", - }, - { - id: 4, - category: "sugurta", - question: "Sayohat davomida sug‘urta kerakmi?", - answer: - "Ha, biz barcha mijozlarga sayohat sug‘urtasini tavsiya qilamiz. Bu favqulodda holatlarda yordam beradi.", - }, - { - id: 5, - category: "tolov", - question: "To‘lovni bosqichma-bosqich amalga oshirish mumkinmi?", - answer: "Ha, ayrim yo‘nalishlar uchun bosqichli to‘lov mavjud.", - }, -]; - const faqForm = z.object({ categories: z.string().min(1, { message: "Majburiy maydon" }), title: z.string().min(1, { message: "Majburiy maydon" }), @@ -101,14 +51,36 @@ const faqForm = z.object({ }); const Faq = () => { - const [faqs, setFaqs] = useState(initialFaqs); - const [activeTab, setActiveTab] = useState("umumiy"); const { t } = useTranslation(); - const [openModal, setOpenModal] = useState(false); - const [editFaq, setEditFaq] = useState(null); - const [deleteId, setDeleteId] = useState(null); + const { data: category } = useQuery({ + queryKey: ["all_faqcategory"], + queryFn: () => { + return getAllFaqCategory({ page: 1, page_size: 99 }); + }, + select(data) { + return data.data.data.results; + }, + }); + const { data: faq } = useQuery({ + queryKey: ["all_faq"], + queryFn: () => { + return getAllFaq({ page: 1, page_size: 99 }); + }, + select(data) { + return data.data.data.results; + }, + }); + const [activeTab, setActiveTab] = useState(""); - const filteredFaqs = faqs.filter((faq) => faq.category === activeTab); + useEffect(() => { + if (category) { + setActiveTab(String(category[0].id)); + } + }, [category]); + + const [deleteId, setDeleteId] = useState(null); + const [editFaq, setEditFaq] = useState(null); + const [openModal, setOpenModal] = useState(false); const form = useForm>({ resolver: zodResolver(faqForm), @@ -123,18 +95,12 @@ const Faq = () => { console.log(value); } - const handleEdit = (faq: FaqType) => { + const handleEdit = (faq: number) => { setEditFaq(faq); - setOpenModal(true); - form.setValue("answer", faq.answer); - form.setValue("title", faq.question); - form.setValue("categories", faq.category); }; const handleDelete = () => { if (deleteId) { - setFaqs((prev) => prev.filter((faq) => faq.id !== deleteId)); - setDeleteId(null); } }; @@ -164,70 +130,73 @@ const Faq = () => { {/* Tabs */} - - - {categories.map((cat) => ( - - {cat.label} + + + {category?.map((cat) => ( + + {cat.name} ))} - - {filteredFaqs.length > 0 ? ( -
- - - - # - {t("Savol")} - {t("Javob")} - - {t("Amallar")} - - - - - {filteredFaqs.map((faq, index) => ( - - - {index + 1} - - - {faq.question} - - - {faq.answer.length > 80 - ? faq.answer.slice(0, 80) + "..." - : faq.answer} - - - - - + {/* Tabs content */} + {category?.map((cat) => ( + + {faq && faq?.length > 0 ? ( +
+
+ + + # + {t("Savol")} + {t("Javob")} + + {t("Amallar")} + - ))} - -
-
- ) : ( -

- {t("Bu bo‘limda savollar yo‘q.")} -

- )} -
+ + + {faq.map((faq, index) => ( + + + {index + 1} + + + {faq.title} + + + {faq.text.length > 80 + ? faq.text.slice(0, 80) + "..." + : faq.text} + + + + + + + ))} + + + + ) : ( +

+ {t("Bu bo‘limda savollar yo‘q.")} +

+ )} + + ))}
@@ -257,11 +226,11 @@ const Faq = () => { {t("Kategoriyalar")} - {categories.map((cat) => ( + {/* {categories.map((cat) => ( {cat.label} - ))} + ))} */} diff --git a/src/pages/faq/ui/FaqCategory.tsx b/src/pages/faq/ui/FaqCategory.tsx index 33f655d..bee5f33 100644 --- a/src/pages/faq/ui/FaqCategory.tsx +++ b/src/pages/faq/ui/FaqCategory.tsx @@ -1,5 +1,12 @@ "use client"; +import { + createFaqCategory, + deleteFaqCategory, + getAllFaqCategory, + getDetailFaqCategory, + updateFaqCategory, +} from "@/pages/faq/lib/api"; import { Button } from "@/shared/ui/button"; import { Dialog, @@ -26,81 +33,145 @@ import { TableRow, } from "@/shared/ui/table"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Pencil, PlusCircle, Trash2 } from "lucide-react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Loader, Pencil, PlusCircle, Trash2 } 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"; -type FaqCategoryType = { - id: number; - name: string; - faqCount: number; -}; - -// fakeData: kategoriya + savol soni -const initialCategories: FaqCategoryType[] = [ - { id: 1, name: "umumiy", faqCount: 8 }, - { id: 2, name: "to‘lov", faqCount: 5 }, - { id: 3, name: "hujjatlar", faqCount: 6 }, - { id: 4, name: "sug‘urta", faqCount: 3 }, -]; - const categoryFormSchema = z.object({ name: z.string().min(1, { message: "Kategoriya nomi majburiy" }), + name_ru: z.string().min(1, { message: "Kategoriya nomi majburiy" }), }); const FaqCategory = () => { - const { t } = useTranslation(); - const [categories, setCategories] = - useState(initialCategories); - const [openModal, setOpenModal] = useState(false); - const [editCategory, setEditCategory] = useState( - null, - ); const [deleteId, setDeleteId] = useState(null); + const [categories, setCategories] = useState(null); + const queryClient = useQueryClient(); + const { data: category } = useQuery({ + queryKey: ["all_faqcategory"], + queryFn: () => { + return getAllFaqCategory({ page: 1, page_size: 99 }); + }, + select(data) { + return data.data.data.results; + }, + }); + const { data: oneCategory } = useQuery({ + queryKey: ["detail_faqcategory", categories], + queryFn: () => { + return getDetailFaqCategory(categories!); + }, + select(data) { + return data.data.data; + }, + enabled: !!categories, + }); + const { mutate: create, isPending: createPending } = useMutation({ + mutationFn: (body: { name: string; name_ru: string }) => { + return createFaqCategory(body); + }, + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["all_faqcategory"] }); + queryClient.refetchQueries({ queryKey: ["detail_faqcategory"] }); + setOpenModal(false); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + + const { mutate: update, isPending: updatePending } = useMutation({ + mutationFn: ({ + body, + id, + }: { + id: number; + body: { name: string; name_ru: string }; + }) => { + return updateFaqCategory({ body, id }); + }, + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["all_faqcategory"] }); + queryClient.refetchQueries({ queryKey: ["detail_faqcategory"] }); + setOpenModal(false); + setCategories(null); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + const { mutate: deleteCategory, isPending: deletePending } = useMutation({ + mutationFn: (id: number) => { + return deleteFaqCategory(id); + }, + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["all_faqcategory"] }); + queryClient.refetchQueries({ queryKey: ["detail_faqcategory"] }); + setDeleteId(null); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + const { t } = useTranslation(); + const [openModal, setOpenModal] = useState(false); const form = useForm>({ resolver: zodResolver(categoryFormSchema), - defaultValues: { name: "" }, + defaultValues: { name: "", name_ru: "" }, }); - const onSubmit = (values: z.infer) => { - if (editCategory) { - setCategories((prev) => - prev.map((cat) => - cat.id === editCategory.id ? { ...cat, name: values.name } : cat, - ), - ); - } else { - const newCategory = { - id: Date.now(), - name: values.name, - faqCount: 0, // yangi kategoriya bo‘lsa 0 ta savol - }; - setCategories((prev) => [...prev, newCategory]); + useEffect(() => { + if (oneCategory && categories) { + form.setValue("name", oneCategory.name); + form.setValue("name_ru", oneCategory.name_ru); } + }, [oneCategory, categories]); - setOpenModal(false); + const onSubmit = (values: z.infer) => { + if (categories !== null) { + update({ + id: categories, + body: { + name: values.name, + name_ru: values.name_ru, + }, + }); + } else { + create({ + name: values.name, + name_ru: values.name_ru, + }); + } }; - const handleEdit = (cat: FaqCategoryType) => { - setEditCategory(cat); - form.setValue("name", cat.name); + const handleEdit = (cat: number) => { + setCategories(cat); setOpenModal(true); }; const handleDelete = () => { if (deleteId) { - setCategories((prev) => prev.filter((cat) => cat.id !== deleteId)); - setDeleteId(null); + deleteCategory(deleteId); } }; useEffect(() => { if (!openModal) { form.reset(); - setEditCategory(null); + setCategories(null); } }, [openModal, form]); @@ -111,7 +182,7 @@ const FaqCategory = () => { - + {category && category.length > 0 ? ( + category.map((cat, index) => ( + + + {index + 1} + + + {cat.name} + + {/* {cat.questionCount} */} + + + + + + )) + ) : ( + + + {t("Hech qanday kategoriya topilmadi")} - ))} + )} @@ -167,7 +250,7 @@ const FaqCategory = () => { - {editCategory ? "Kategoriyani tahrirlash" : "Yangi kategoriya"} + {categories ? "Kategoriyani tahrirlash" : "Yangi kategoriya"} @@ -191,6 +274,24 @@ const FaqCategory = () => { )} /> + ( + + + + + + + + )} + /> + @@ -222,7 +329,11 @@ const FaqCategory = () => { Bekor qilish diff --git a/src/pages/tours/lib/form.ts b/src/pages/tours/lib/form.ts index aee84a1..e98d5a0 100644 --- a/src/pages/tours/lib/form.ts +++ b/src/pages/tours/lib/form.ts @@ -115,17 +115,18 @@ export const TourformSchema = z.object({ tariff: z.number().min(1, { message: "Transport ID majburiy" }), price: z .number() - .min(1000, { message: "Narx kamida 1000 UZS bo'lishi kerak" }), + .min(0, { message: "Narx 0 dan kichik bo‘lishi mumkin emas" }), // 0 ham ruxsat }), ) .min(1, { message: "Kamida bitta transport tanlang." }), + transport: z .array( z.object({ transport: z.number().min(1, { message: "Transport ID majburiy" }), price: z .number() - .min(1000, { message: "Narx kamida 1000 UZS bo'lishi kerak" }), + .min(0, { message: "Narx 0 dan kichik bo‘lishi mumkin emas" }), // 0 ham ruxsat }), ) .min(1, { message: "Kamida bitta transport tanlang." }), diff --git a/src/pages/tours/ui/StepOne.tsx b/src/pages/tours/ui/StepOne.tsx index 7696aed..3af1088 100644 --- a/src/pages/tours/ui/StepOne.tsx +++ b/src/pages/tours/ui/StepOne.tsx @@ -926,7 +926,7 @@ const StepOne = ({ // Local holatda ham yangilaymiz: const updatedPrices = [...tarifdisplayPrice]; - updatedPrices[idx] = raw ? formatPrice(num) : ""; + updatedPrices[idx] = raw ? formatPrice(num) : "0"; setTarifDisplayPrice(updatedPrices); }} className="h-12 !text-md" diff --git a/src/pages/tours/ui/StepTwo.tsx b/src/pages/tours/ui/StepTwo.tsx index ad39a8e..0381ffb 100644 --- a/src/pages/tours/ui/StepTwo.tsx +++ b/src/pages/tours/ui/StepTwo.tsx @@ -43,7 +43,7 @@ const formSchema = z.object({ title: z.string().min(2, { message: "Sarlavha kamida 2 ta belgidan iborat bo'lishi kerak", }), - rating: z.number().min(1).max(5), + rating: z.string().min(1).max(5), mealPlan: z.string().min(1, { message: "Taom rejasi tanlanishi majburiy" }), hotelType: z .array(z.string()) @@ -71,7 +71,7 @@ const StepTwo = ({ resolver: zodResolver(formSchema), defaultValues: { title: "", - rating: 3.0, + rating: "3.0", mealPlan: "", hotelType: [], hotelFeatures: [], @@ -84,7 +84,7 @@ const StepTwo = ({ const tourData = data.data; form.setValue("title", tourData.hotel_name); - form.setValue("rating", Number(tourData.hotel_rating)); + form.setValue("rating", tourData.hotel_rating); form.setValue("mealPlan", tourData.hotel_meals); } }, [isEditMode, data, form]); diff --git a/src/pages/users/ui/User.tsx b/src/pages/users/ui/User.tsx index 17d7f79..bccce0a 100644 --- a/src/pages/users/ui/User.tsx +++ b/src/pages/users/ui/User.tsx @@ -14,7 +14,6 @@ import { Pencil, Phone, Search, - Trash2, Users, } from "lucide-react"; import { useState } from "react"; @@ -205,10 +204,10 @@ export default function UserList() { {t("Tahrirlash")} - + */} diff --git a/src/shared/config/api/URLs.ts b/src/shared/config/api/URLs.ts index 0443884..15584d0 100644 --- a/src/shared/config/api/URLs.ts +++ b/src/shared/config/api/URLs.ts @@ -18,11 +18,15 @@ 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/"; +const FAQ = "dashboard/dashboard-faq/"; +const FAQ_CATEGORIES = "dashboard/dashboard-faq-category/"; export { AUTH_LOGIN, BASE_URL, DOWNLOAD_PDF, + FAQ, + FAQ_CATEGORIES, GET_ALL_AGENCY, GET_ALL_EMPLOYEES, GET_ALL_USERS, diff --git a/src/shared/lib/formatPrice.ts b/src/shared/lib/formatPrice.ts index 4afc4e6..8bb5866 100644 --- a/src/shared/lib/formatPrice.ts +++ b/src/shared/lib/formatPrice.ts @@ -17,18 +17,15 @@ const formatPrice = (amount: number | string, withLabel = false): string => { : " so‘m" : ""; - // Agar qiymat bo‘sh yoki 0 bo‘lsa — hech narsa ko‘rsatmaymiz - if ( - amount === "" || - amount === null || - amount === undefined || - Number(amount) === 0 - ) - return ""; + // Agar qiymat bo‘sh yoki null/undefined bo‘lsa — hech narsa ko‘rsatmaymiz + if (amount === "" || amount === null || amount === undefined) return ""; // Faqat raqamlarni qoldiramiz const numeric = String(amount).replace(/\D/g, ""); + // Agar hech qanday raqam kiritilmagan bo‘lsa, bo‘sh qaytaramiz + if (numeric === "") return ""; + // Raqamni 3 xonadan ajratamiz const formatted = numeric.replace(/\B(?=(\d{3})+(?!\d))/g, " ");