questinaire page added

This commit is contained in:
Samandar Turgunboyev
2025-12-23 16:51:18 +05:00
parent aa90939831
commit 8ccee9b63d
12 changed files with 313 additions and 4 deletions

View File

@@ -11,6 +11,7 @@ import {
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/shared/ui/dialog"; } from "@/shared/ui/dialog";
import Pagination from "@/shared/ui/pagination";
import { DialogDescription } from "@radix-ui/react-dialog"; import { DialogDescription } from "@radix-ui/react-dialog";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
@@ -23,6 +24,8 @@ const FaqList = () => {
const [openDelete, setOpenDelete] = useState<boolean>(false); const [openDelete, setOpenDelete] = useState<boolean>(false);
const [dialogOpen, setDialogOpen] = useState<boolean>(false); const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [editingFaq, setEditingFaq] = useState<FaqItem | null>(null); const [editingFaq, setEditingFaq] = useState<FaqItem | null>(null);
const [page, setPage] = useState<number>(1);
const limit = 20;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { const {
@@ -31,8 +34,8 @@ const FaqList = () => {
isLoading, isLoading,
isFetching, isFetching,
} = useQuery({ } = useQuery({
queryKey: ["faqs"], queryKey: ["faqs", page],
queryFn: async () => faq_api.getFaqs({ page: 1, page_size: 20 }), queryFn: async () => faq_api.getFaqs({ page: page, page_size: limit }),
select(data) { select(data) {
return data.data; return data.data;
}, },
@@ -104,6 +107,12 @@ const FaqList = () => {
setEditingFaq={setEditingFaq} setEditingFaq={setEditingFaq}
/> />
<Pagination
currentPage={page}
setCurrentPage={setPage}
totalPages={faq?.total_pages ?? 1}
/>
<Dialog open={openDelete} onOpenChange={setOpenDelete}> <Dialog open={openDelete} onOpenChange={setOpenDelete}>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>

View File

@@ -108,7 +108,7 @@ const OrderTable = ({
} }
return ( return (
<div className="overflow-auto"> <div className="flex-1 overflow-auto">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>

View File

@@ -115,7 +115,7 @@ const ProductTable = ({
} }
return ( return (
<div className="overflow-auto"> <div className="flex-1 overflow-auto">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>

View File

@@ -0,0 +1,14 @@
import type { QuesList } from "@/features/questionnaire/lib/type";
import httpClient from "@/shared/config/api/httpClient";
import { API_URLS } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
export const questionnaire_api = {
async list(params: {
page: number;
page_size: number;
}): Promise<AxiosResponse<QuesList>> {
const res = await httpClient.get(API_URLS.QuestionnaireList, { params });
return res;
},
};

View File

@@ -0,0 +1,18 @@
export interface QuesList {
total: number;
page: number;
page_size: number;
total_pages: number;
has_next: boolean;
has_previous: boolean;
results: QuesListRes[];
}
export interface QuesListRes {
id: string;
created_at: string;
company_name: string;
full_name: string;
phone_number: string;
file: string;
}

View File

@@ -0,0 +1,88 @@
import type { QuesListRes } from "@/features/questionnaire/lib/type";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
interface Props {
open: boolean;
onClose: () => void;
data: QuesListRes | null;
}
const QuestionnaireDetail = ({ open, onClose, data }: Props) => {
if (!data) return null;
const fileUrl = data.file;
const ext = fileUrl.split(".").pop()?.toLowerCase();
const isImage = ["jpg", "jpeg", "png", "webp"].includes(ext ?? "");
const isPdf = ext === "pdf";
const isOffice = ["doc", "docx", "xls", "xlsx", "odt", "ods"].includes(
ext ?? "",
);
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-3xl">
<DialogHeader>
<DialogTitle>Sorov tafsilotlari</DialogTitle>
</DialogHeader>
<div className="space-y-3 text-sm">
<p>
<b>Kompaniya:</b> {data.company_name}
</p>
<p>
<b>F.I.Sh:</b> {data.full_name}
</p>
<p>
<b>Telefon:</b> {data.phone_number}
</p>
<p>
<b>Yaratilgan:</b> {new Date(data.created_at).toLocaleString()}
</p>
</div>
{/* FILE PREVIEW */}
<div className="mt-4 space-y-2">
<b>Ilova:</b>
{isImage && (
<img src={fileUrl} alt="file" className="max-h-96 rounded border" />
)}
{isPdf && (
<iframe src={fileUrl} className="w-full h-[500px] border rounded" />
)}
{isOffice && (
<div className="flex items-center gap-3">
<a
href={fileUrl}
target="_blank"
className="text-blue-600 underline"
>
Faylni yuklab olish
</a>
<span className="text-muted-foreground text-xs">
(Word / Excel / ODF preview brauzerda ochilmaydi)
</span>
</div>
)}
</div>
<div className="flex justify-end">
<Button variant="outline" onClick={onClose}>
Yopish
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default QuestionnaireDetail;

View File

@@ -0,0 +1,52 @@
import { questionnaire_api } from "@/features/questionnaire/lib/api";
import type { QuesListRes } from "@/features/questionnaire/lib/type";
import QuestionnaireDetail from "@/features/questionnaire/ui/questionnaireDetail";
import QuestionnaireTable from "@/features/questionnaire/ui/questionnaireTable";
import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
const QuestionnaireList = () => {
const [page, setPage] = useState<number>(1);
const [detail, setDetail] = useState<boolean>(false);
const [detailQues, setDetailQues] = useState<QuesListRes | null>(null);
const { data, isLoading, isError, isFetching } = useQuery({
queryKey: ["question_list"],
queryFn: () => questionnaire_api.list({ page, page_size: 20 }),
select(data) {
return data.data;
},
});
return (
<div className="flex flex-col h-full p-10 w-full">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<div className="flex flex-col gap-4 w-full">
<h1 className="text-2xl font-bold">FAQ</h1>
</div>
</div>
<QuestionnaireTable
setDetail={setDetail}
isError={isError}
setDetailQues={setDetailQues}
isLoading={isLoading}
ques={data ? data.results : []}
isFetching={isFetching}
/>
<Pagination
currentPage={page}
setCurrentPage={setPage}
totalPages={data?.total_pages ?? 1}
/>
<QuestionnaireDetail
open={detail}
onClose={() => setDetail(false)}
data={detailQues}
/>
</div>
);
};
export default QuestionnaireList;

View File

@@ -0,0 +1,104 @@
import type { QuesListRes } from "@/features/questionnaire/lib/type";
import formatPhone from "@/shared/lib/formatPhone";
import { Button } from "@/shared/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Eye, Loader2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
isLoading: boolean;
isFetching: boolean;
isError: boolean;
ques: QuesListRes[];
setDetail: Dispatch<SetStateAction<boolean>>;
setDetailQues: Dispatch<SetStateAction<QuesListRes | null>>;
}
const QuestionnaireTable = ({
isFetching,
isLoading,
isError,
setDetail,
setDetailQues,
ques,
}: Props) => {
return (
<div className="flex-1 overflow-auto relative">
{(isLoading || isFetching) && (
<div className="absolute inset-0 flex items-center justify-center bg-white/70 z-10">
<Loader2 className="animate-spin w-6 h-6 text-gray-600" />
</div>
)}
{isError && (
<div className="h-full flex items-center justify-center">
<span className="text-lg font-medium text-red-600">
Ma'lumotlarni olishda xatolik yuz berdi
</span>
</div>
)}
{!isLoading && !isError && (
<Table>
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
<TableHead>Kompaniya</TableHead>
<TableHead>F.I.Sh</TableHead>
<TableHead>Telefon</TableHead>
<TableHead className="text-right">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{ques.length > 0 ? (
ques.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{index + 1}</TableCell>
<TableCell className="font-medium">
{item.company_name}
</TableCell>
<TableCell>{item.full_name}</TableCell>
<TableCell>{formatPhone(item.phone_number)}</TableCell>
<TableCell className="flex justify-end">
<Button
variant={"outline"}
onClick={() => {
setDetail(true);
setDetailQues(item);
}}
className="cursor-pointer"
>
<Eye />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={6}
className="text-center py-6 text-gray-500"
>
Maʼlumot topilmadi
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
);
};
export default QuestionnaireTable;

View File

@@ -0,0 +1,12 @@
import QuestionnaireList from "@/features/questionnaire/ui/questionnaireList";
import SidebarLayout from "@/SidebarLayout";
const Questionnaire = () => {
return (
<SidebarLayout>
<QuestionnaireList />
</SidebarLayout>
);
};
export default Questionnaire;

View File

@@ -5,6 +5,7 @@ import Faq from "@/pages/Faq";
import HomePage from "@/pages/Home"; import HomePage from "@/pages/Home";
import Orders from "@/pages/Orders"; import Orders from "@/pages/Orders";
import Product from "@/pages/Product"; import Product from "@/pages/Product";
import Questionnaire from "@/pages/Questionnaire";
import Units from "@/pages/Units"; import Units from "@/pages/Units";
import Users from "@/pages/Users"; import Users from "@/pages/Users";
import routesConfig from "@/providers/routing/config"; import routesConfig from "@/providers/routing/config";
@@ -53,6 +54,10 @@ const AppRouter = () => {
path: "/dashboard/faq", path: "/dashboard/faq",
element: <Faq />, element: <Faq />,
}, },
{
path: "/dashboard/questionnaire",
element: <Questionnaire />,
},
]); ]);
return routes; return routes;

View File

@@ -31,4 +31,5 @@ export const API_URLS = {
CreateProduct: `${API_V}admin/product/create/`, CreateProduct: `${API_V}admin/product/create/`,
UpdateProduct: (id: string) => `${API_V}admin/product/${id}/update/`, UpdateProduct: (id: string) => `${API_V}admin/product/${id}/update/`,
DeleteProdut: (id: string) => `${API_V}admin/product/${id}/delete/`, DeleteProdut: (id: string) => `${API_V}admin/product/${id}/delete/`,
QuestionnaireList: `${API_V}admin/questionnaire/list/`,
}; };

View File

@@ -1,5 +1,6 @@
import { import {
CircleQuestionMark, CircleQuestionMark,
FileText,
FolderOpen, FolderOpen,
Image, Image,
LayoutDashboard, LayoutDashboard,
@@ -66,6 +67,11 @@ const items = [
url: "/dashboard/faq", url: "/dashboard/faq",
icon: CircleQuestionMark, icon: CircleQuestionMark,
}, },
{
title: "So'rov jo'natganlar",
url: "/dashboard/questionnaire",
icon: FileText,
},
]; ];
export function AppSidebar() { export function AppSidebar() {