questinaire page added
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const OrderTable = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const ProductTable = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
14
src/features/questionnaire/lib/api.ts
Normal file
14
src/features/questionnaire/lib/api.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
18
src/features/questionnaire/lib/type.ts
Normal file
18
src/features/questionnaire/lib/type.ts
Normal 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;
|
||||||
|
}
|
||||||
88
src/features/questionnaire/ui/questionnaireDetail.tsx
Normal file
88
src/features/questionnaire/ui/questionnaireDetail.tsx
Normal 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>So‘rov 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;
|
||||||
52
src/features/questionnaire/ui/questionnaireList.tsx
Normal file
52
src/features/questionnaire/ui/questionnaireList.tsx
Normal 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;
|
||||||
104
src/features/questionnaire/ui/questionnaireTable.tsx
Normal file
104
src/features/questionnaire/ui/questionnaireTable.tsx
Normal 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;
|
||||||
12
src/pages/Questionnaire.tsx
Normal file
12
src/pages/Questionnaire.tsx
Normal 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;
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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/`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user