update api request and response
This commit is contained in:
17
src/features/distributed/lib/api.ts
Normal file
17
src/features/distributed/lib/api.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { DistributedList } from "@/features/distributed/lib/data";
|
||||||
|
import httpClient from "@/shared/config/api/httpClient";
|
||||||
|
import { API_URLS } from "@/shared/config/api/URLs";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
export const distributed_api = {
|
||||||
|
async list(params: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
product?: string;
|
||||||
|
user?: string;
|
||||||
|
date?: string;
|
||||||
|
}): Promise<AxiosResponse<DistributedList>> {
|
||||||
|
const res = await httpClient.get(API_URLS.DISTRIBUTED, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
29
src/features/distributed/lib/data.ts
Normal file
29
src/features/distributed/lib/data.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
export interface DistributedList {
|
||||||
|
status_code: number;
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
count: number;
|
||||||
|
next: null | string;
|
||||||
|
previous: null | string;
|
||||||
|
results: DistributedListData[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributedListData {
|
||||||
|
id: number;
|
||||||
|
product: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
quantity: number;
|
||||||
|
employee_name: string;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
};
|
||||||
|
created_at: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
199
src/features/distributed/ui/DistributedList.tsx
Normal file
199
src/features/distributed/ui/DistributedList.tsx
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
import { distributed_api } from "@/features/distributed/lib/api";
|
||||||
|
import type { DistributedListData } from "@/features/distributed/lib/data";
|
||||||
|
import { DistributedDetail } from "@/features/distributed/ui/SpecificationDetail ";
|
||||||
|
import formatDate from "@/shared/lib/formatDate";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Calendar } from "@/shared/ui/calendar";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import Pagination from "@/shared/ui/pagination";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/shared/ui/table";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { ChevronDownIcon, Eye, Loader2 } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const DistributedList = () => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [nameFilter, setNameFilter] = useState<string>("");
|
||||||
|
const limit = 20;
|
||||||
|
const [disctritFilter, setDisctritFilter] = useState<string>("");
|
||||||
|
const [openDate, setOpenDate] = useState<boolean>(false);
|
||||||
|
const [dateFilter, setDateFilter] = useState<Date | undefined>(undefined);
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [supportDetail, setSupportDetail] =
|
||||||
|
useState<DistributedListData | null>(null);
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
"distributed_list",
|
||||||
|
currentPage,
|
||||||
|
nameFilter,
|
||||||
|
disctritFilter,
|
||||||
|
dateFilter,
|
||||||
|
],
|
||||||
|
queryFn: () =>
|
||||||
|
distributed_api.list({
|
||||||
|
limit,
|
||||||
|
offset: (currentPage - 1) * limit,
|
||||||
|
product: nameFilter,
|
||||||
|
user: disctritFilter,
|
||||||
|
date: dateFilter && formatDate.format(dateFilter, "YYYY-MM-DD"),
|
||||||
|
}),
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPages = data ? Math.ceil(data?.count / limit) : 1;
|
||||||
|
|
||||||
|
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">
|
||||||
|
<h1 className="text-2xl font-bold">Yordam so'rovlari ro'yxati</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Foydalanuvchi nomi"
|
||||||
|
className="h-12"
|
||||||
|
value={disctritFilter}
|
||||||
|
onChange={(e) => setDisctritFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Mahsulot nomi"
|
||||||
|
className="h-12"
|
||||||
|
value={nameFilter}
|
||||||
|
onChange={(e) => setNameFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Popover open={openDate} onOpenChange={setOpenDate}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
id="date"
|
||||||
|
className="w-48 justify-between font-normal h-12"
|
||||||
|
>
|
||||||
|
{dateFilter ? dateFilter.toDateString() : "Sana"}
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-auto overflow-hidden p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={dateFilter}
|
||||||
|
captionLayout="dropdown"
|
||||||
|
onSelect={(date) => {
|
||||||
|
setDateFilter(date);
|
||||||
|
setOpenDate(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="p-2 border-t bg-white">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
setDateFilter(undefined);
|
||||||
|
setOpenDate(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tozalash
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="h-full flex items-center justify-center bg-white/70 z-10">
|
||||||
|
<span className="text-lg font-medium">
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<div className="h-full flex items-center justify-center z-10">
|
||||||
|
<span className="text-lg font-medium text-red-600">
|
||||||
|
Ma'lumotlarni olishda xatolik yuz berdi.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && !isError && (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="text-center">
|
||||||
|
<TableHead className="text-start">ID</TableHead>
|
||||||
|
<TableHead className="text-start">Kim qo'shgan</TableHead>
|
||||||
|
<TableHead className="text-start">Xaridorning ismi</TableHead>
|
||||||
|
<TableHead className="text-start">Mahsulot nomi</TableHead>
|
||||||
|
<TableHead className="text-start">Nechta berilgan</TableHead>
|
||||||
|
<TableHead className="text-start">Topshirilgan sana</TableHead>
|
||||||
|
<TableHead className="text-start">Amallar</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data && data.results.length > 0 ? (
|
||||||
|
data?.results.map((plan) => (
|
||||||
|
<TableRow key={plan.id} className="text-start">
|
||||||
|
<TableCell>{plan.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{plan.user.first_name} {plan.user.last_name}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{plan.employee_name}</TableCell>
|
||||||
|
<TableCell>{plan.product.name}</TableCell>
|
||||||
|
<TableCell>{plan.quantity}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{formatDate.format(plan.date, "YYYY-MM-DD")}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
className="bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
setSupportDetail(plan);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Eye />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center py-4 text-lg">
|
||||||
|
Farmasevtika topilmadi.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
setCurrentPage={setCurrentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DistributedDetail
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
specification={supportDetail}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DistributedList;
|
||||||
92
src/features/distributed/ui/SpecificationDetail .tsx
Normal file
92
src/features/distributed/ui/SpecificationDetail .tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import type { DistributedListData } from "@/features/distributed/lib/data";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
specification: DistributedListData | null;
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DistributedDetail = ({ specification, open, setOpen }: Props) => {
|
||||||
|
if (!specification) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader className="border-b pb-4">
|
||||||
|
<DialogTitle className="text-2xl font-bold text-gray-800">
|
||||||
|
Tafsilot
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 mt-6">
|
||||||
|
{/* Asosiy ma'lumotlar - Grid */}
|
||||||
|
<div className="grid grid-cols-1">
|
||||||
|
{/* Xaridor */}
|
||||||
|
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
|
||||||
|
<p className="text-sm text-blue-600 font-medium mb-1">
|
||||||
|
Xaridorning ismi
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-800">
|
||||||
|
{specification.employee_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Foydalanuvchi */}
|
||||||
|
<div className="bg-gradient-to-br mt-5 from-purple-50 to-purple-100 rounded-lg p-4 border border-purple-200 md:col-span-2">
|
||||||
|
<p className="text-sm text-purple-600 font-medium mb-1">
|
||||||
|
Mas'ul xodim
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-800">
|
||||||
|
{specification.user.first_name} {specification.user.last_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-br mt-5 from-green-50 to-green-100 rounded-lg p-4 border border-green-200 md:col-span-2">
|
||||||
|
<p className="text-sm text-green-600 font-medium mb-1">
|
||||||
|
Topshirilgan sanasi
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-800">
|
||||||
|
{specification.date}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dorilar ro'yxati */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
||||||
|
<h3 className="text-lg font-bold text-gray-800 mb-4 flex items-center">
|
||||||
|
Topshirilgan dori
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-white rounded-lg p-4 border border-gray-200 hover:border-indigo-300 transition-colors">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<p className="font-semibold text-gray-800">
|
||||||
|
{specification.product.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||||
|
<span>
|
||||||
|
Miqdor: <strong>{specification.quantity} ta</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -17,6 +17,7 @@ export const doctor_api = {
|
|||||||
work_place?: string;
|
work_place?: string;
|
||||||
sphere?: string;
|
sphere?: string;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
user_id?: number;
|
||||||
}): Promise<AxiosResponse<DoctorListRes>> {
|
}): Promise<AxiosResponse<DoctorListRes>> {
|
||||||
const res = await httpClient.get(`${API_URLS.DOCTOR}list/`, { params });
|
const res = await httpClient.get(`${API_URLS.DOCTOR}list/`, { params });
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -106,16 +106,19 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
return data.data.data;
|
return data.data.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const user_id = form.watch("user");
|
||||||
|
|
||||||
const { data: object, isLoading: isObjectLoading } = useQuery({
|
const { data: object, isLoading: isObjectLoading } = useQuery({
|
||||||
queryKey: ["object_list", searchUser, selectDiscrit],
|
queryKey: ["object_list", searchUser, selectDiscrit, user_id],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const params: {
|
const params: {
|
||||||
name?: string;
|
name?: string;
|
||||||
district?: string;
|
district?: string;
|
||||||
|
user_id?: number;
|
||||||
} = {
|
} = {
|
||||||
name: searchUser,
|
name: searchUser,
|
||||||
district: selectDiscrit,
|
district: selectDiscrit,
|
||||||
|
user_id: Number(user_id),
|
||||||
};
|
};
|
||||||
|
|
||||||
return object_api.list(params);
|
return object_api.list(params);
|
||||||
@@ -125,8 +128,6 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const user_id = form.watch("user");
|
|
||||||
|
|
||||||
const { data: discrit, isLoading: discritLoading } = useQuery({
|
const { data: discrit, isLoading: discritLoading } = useQuery({
|
||||||
queryKey: ["discrit_list", searchDiscrit, user_id],
|
queryKey: ["discrit_list", searchDiscrit, user_id],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export const object_api = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
district?: string;
|
district?: string;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
user_id?: number;
|
||||||
}): Promise<AxiosResponse<ObjectListRes>> {
|
}): Promise<AxiosResponse<ObjectListRes>> {
|
||||||
const res = await httpClient.get(`${API_URLS.OBJECT}list/`, { params });
|
const res = await httpClient.get(`${API_URLS.OBJECT}list/`, { params });
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const pharmacies_api = {
|
|||||||
place?: string;
|
place?: string;
|
||||||
district?: string;
|
district?: string;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
user_id?: number;
|
||||||
}): Promise<AxiosResponse<PharmaciesListRes>> {
|
}): Promise<AxiosResponse<PharmaciesListRes>> {
|
||||||
const res = await httpClient.get(`${API_URLS.PHARMACIES}list/`, { params });
|
const res = await httpClient.get(`${API_URLS.PHARMACIES}list/`, { params });
|
||||||
return res;
|
return res;
|
||||||
|
|||||||
@@ -102,16 +102,19 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
return data.data.data;
|
return data.data.data;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const user_id = form.watch("user");
|
||||||
|
|
||||||
const { data: object, isLoading: isObjectLoading } = useQuery({
|
const { data: object, isLoading: isObjectLoading } = useQuery({
|
||||||
queryKey: ["object_list", searchUser, selectDiscrit],
|
queryKey: ["object_list", searchUser, selectDiscrit, user_id],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
const params: {
|
const params: {
|
||||||
name?: string;
|
name?: string;
|
||||||
district?: string;
|
district?: string;
|
||||||
|
user_id?: number;
|
||||||
} = {
|
} = {
|
||||||
name: searchUser,
|
name: searchUser,
|
||||||
district: selectDiscrit,
|
district: selectDiscrit,
|
||||||
|
user_id: Number(user_id),
|
||||||
};
|
};
|
||||||
|
|
||||||
return object_api.list(params);
|
return object_api.list(params);
|
||||||
@@ -121,8 +124,6 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const user_id = form.watch("user");
|
|
||||||
|
|
||||||
const { data: discrit, isLoading: discritLoading } = useQuery({
|
const { data: discrit, isLoading: discritLoading } = useQuery({
|
||||||
queryKey: ["discrit_list", searchDiscrit, user_id],
|
queryKey: ["discrit_list", searchDiscrit, user_id],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
|
|||||||
@@ -26,12 +26,27 @@ export interface PlanListData {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
comment: null | string;
|
||||||
|
doctor: {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
} | null;
|
||||||
|
pharmacy: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
user: {
|
user: {
|
||||||
id: number;
|
id: number;
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
};
|
};
|
||||||
is_done: true;
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
extra_location: {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
};
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +55,18 @@ export interface PlanCreateReq {
|
|||||||
description: string;
|
description: string;
|
||||||
date: string;
|
date: string;
|
||||||
user_id: number;
|
user_id: number;
|
||||||
|
doctor_id: number | null;
|
||||||
|
pharmacy_id: number | null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
extra_location: { longitude: number; latitude: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlanUpdateReq {
|
export interface PlanUpdateReq {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
date: string;
|
date: string;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
extra_location: { longitude: number; latitude: number };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,4 +5,10 @@ export const createPlanFormData = z.object({
|
|||||||
description: z.string().min(1, { message: "Majburiy maydon" }),
|
description: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
date: z.string().min(1, { message: "Majburiy maydon" }),
|
date: z.string().min(1, { message: "Majburiy maydon" }),
|
||||||
|
doctor_id: z.string().optional(),
|
||||||
|
pharmacy_id: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// longitude: number;
|
||||||
|
// latitude: number;
|
||||||
|
// extra_location: { longitude: number; latitude: number };
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { doctor_api } from "@/features/doctors/lib/api";
|
||||||
|
import { pharmacies_api } from "@/features/pharmacies/lib/api";
|
||||||
import { plans_api } from "@/features/plans/lib/api";
|
import { plans_api } from "@/features/plans/lib/api";
|
||||||
import type {
|
import type {
|
||||||
PlanCreateReq,
|
PlanCreateReq,
|
||||||
@@ -53,8 +55,16 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
date: initialValues ? initialValues?.date : "",
|
date: initialValues ? initialValues?.date : "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [type, setType] = useState<"DOCTOR" | "PHARM">("DOCTOR");
|
||||||
|
const [long, setLong] = useState<number>(41.233);
|
||||||
|
const [lat, setLat] = useState<number>(63.233);
|
||||||
const [searchUser, setSearchUser] = useState<string>("");
|
const [searchUser, setSearchUser] = useState<string>("");
|
||||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||||
|
const [searchDoctor, setSearchDoctor] = useState<string>("");
|
||||||
|
const [openDoctor, setOpenDoctor] = useState<boolean>(false);
|
||||||
|
const [searchPharm, setSearchPharm] = useState<string>("");
|
||||||
|
const [openPharm, setOpenPharm] = useState<boolean>(false);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
@@ -79,6 +89,50 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const user_id = form.watch("user");
|
||||||
|
|
||||||
|
const { data: doctor, isLoading: isDoctorLoading } = useQuery({
|
||||||
|
queryKey: ["doctor_list", searchDoctor, user_id],
|
||||||
|
queryFn: () => {
|
||||||
|
const params: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
user_id?: number;
|
||||||
|
full_name?: string;
|
||||||
|
} = {
|
||||||
|
limit: 8,
|
||||||
|
full_name: searchDoctor,
|
||||||
|
user_id: Number(user_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
return doctor_api.list(params);
|
||||||
|
},
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: pharm, isLoading: isPharmLoading } = useQuery({
|
||||||
|
queryKey: ["pharm_list", searchPharm, user_id],
|
||||||
|
queryFn: () => {
|
||||||
|
const params: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
user_id?: number;
|
||||||
|
name?: string;
|
||||||
|
} = {
|
||||||
|
limit: 8,
|
||||||
|
name: searchPharm,
|
||||||
|
user_id: Number(user_id),
|
||||||
|
};
|
||||||
|
|
||||||
|
return pharmacies_api.list(params);
|
||||||
|
},
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const { mutate, isPending } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: (body: PlanCreateReq) => plans_api.create(body),
|
mutationFn: (body: PlanCreateReq) => plans_api.create(body),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -117,18 +171,32 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
function onSubmit(data: z.infer<typeof createPlanFormData>) {
|
function onSubmit(data: z.infer<typeof createPlanFormData>) {
|
||||||
if (initialValues) {
|
if (initialValues) {
|
||||||
edit({
|
edit({
|
||||||
id: initialValues.id,
|
|
||||||
body: {
|
body: {
|
||||||
date: formatDate.format(data.date, "YYYY-MM-DD"),
|
date: formatDate.format(data.date, "YYYY-MM-DD"),
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
extra_location: {
|
||||||
|
latitude: initialValues.latitude,
|
||||||
|
longitude: initialValues.longitude,
|
||||||
|
},
|
||||||
|
latitude: initialValues.latitude,
|
||||||
|
longitude: initialValues.longitude,
|
||||||
title: data.name,
|
title: data.name,
|
||||||
},
|
},
|
||||||
|
id: initialValues.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
mutate({
|
mutate({
|
||||||
date: formatDate.format(data.date, "YYYY-MM-DD"),
|
date: formatDate.format(data.date, "YYYY-MM-DD"),
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
extra_location: {
|
||||||
|
latitude: lat,
|
||||||
|
longitude: long,
|
||||||
|
},
|
||||||
|
latitude: lat,
|
||||||
|
longitude: long,
|
||||||
title: data.name,
|
title: data.name,
|
||||||
|
doctor_id: data.doctor_id ? Number(data.doctor_id) : null,
|
||||||
|
pharmacy_id: data.pharmacy_id ? Number(data.pharmacy_id) : null,
|
||||||
user_id: Number(data.user),
|
user_id: Number(data.user),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -223,6 +291,213 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setType("DOCTOR")}
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer h-10 text-sm",
|
||||||
|
type === "DOCTOR"
|
||||||
|
? "bg-blue-700 hover:bg-blue-700 text-white"
|
||||||
|
: "bg-gray-300 hover:bg-gray-300 text-black/80",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Shifokorga birlashtirish
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => setType("PHARM")}
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"cursor-pointer h-10 text-sm",
|
||||||
|
type === "PHARM"
|
||||||
|
? "bg-blue-700 hover:bg-blue-700 text-white"
|
||||||
|
: "bg-gray-300 hover:bg-gray-300 text-black/80",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Dorixonaga birlashtirish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{type === "DOCTOR" && (
|
||||||
|
<FormField
|
||||||
|
name="doctor_id"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedUser = doctor?.results.find(
|
||||||
|
(u) => String(u.id) === field.value,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<Label className="text-md">Shifokorlar</Label>
|
||||||
|
|
||||||
|
<Popover open={openDoctor} onOpenChange={setOpenDoctor}>
|
||||||
|
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={openDoctor}
|
||||||
|
className={cn(
|
||||||
|
"w-full h-12 justify-between",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedUser
|
||||||
|
? `${selectedUser.first_name} ${selectedUser.last_name}`
|
||||||
|
: "Shifokorni tanlang"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
className="w-[--radix-popover-trigger-width] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Qidirish..."
|
||||||
|
className="h-9"
|
||||||
|
value={searchDoctor}
|
||||||
|
onValueChange={setSearchDoctor}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommandList>
|
||||||
|
{isDoctorLoading ? (
|
||||||
|
<div className="py-6 text-center text-sm">
|
||||||
|
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : doctor && doctor.results.length > 0 ? (
|
||||||
|
<CommandGroup>
|
||||||
|
{doctor.results.map((u) => (
|
||||||
|
<CommandItem
|
||||||
|
key={u.id}
|
||||||
|
value={`${u.id}`}
|
||||||
|
onSelect={() => {
|
||||||
|
field.onChange(String(u.id));
|
||||||
|
setOpenDoctor(false);
|
||||||
|
setLat(u.latitude);
|
||||||
|
setLong(u.longitude);
|
||||||
|
form.setValue("pharmacy_id", undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
field.value === String(u.id)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{u.first_name} {u.last_name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
) : (
|
||||||
|
<CommandEmpty>Shifokor topilmadi</CommandEmpty>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{type === "PHARM" && (
|
||||||
|
<FormField
|
||||||
|
name="pharmacy_id"
|
||||||
|
control={form.control}
|
||||||
|
render={({ field }) => {
|
||||||
|
const selectedUser = pharm?.results.find(
|
||||||
|
(u) => String(u.id) === field.value,
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<Label className="text-md">Dorixonalar</Label>
|
||||||
|
|
||||||
|
<Popover open={openPharm} onOpenChange={setOpenPharm}>
|
||||||
|
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={openPharm}
|
||||||
|
className={cn(
|
||||||
|
"w-full h-12 justify-between",
|
||||||
|
!field.value && "text-muted-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{selectedUser
|
||||||
|
? `${selectedUser.name}`
|
||||||
|
: "Dorixonani tanlang"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
className="w-[--radix-popover-trigger-width] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Qidirish..."
|
||||||
|
className="h-9"
|
||||||
|
value={searchPharm}
|
||||||
|
onValueChange={setSearchPharm}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommandList>
|
||||||
|
{isPharmLoading ? (
|
||||||
|
<div className="py-6 text-center text-sm">
|
||||||
|
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||||
|
</div>
|
||||||
|
) : pharm && pharm.results.length > 0 ? (
|
||||||
|
<CommandGroup>
|
||||||
|
{pharm.results.map((u) => (
|
||||||
|
<CommandItem
|
||||||
|
key={u.id}
|
||||||
|
value={`${u.id}`}
|
||||||
|
onSelect={() => {
|
||||||
|
field.onChange(String(u.id));
|
||||||
|
setOpenPharm(false);
|
||||||
|
setLat(u.latitude);
|
||||||
|
setLong(u.longitude);
|
||||||
|
form.setValue("doctor_id", undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Check
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
field.value === String(u.id)
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{u.name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
) : (
|
||||||
|
<CommandEmpty>Dorixona topilmadi</CommandEmpty>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -307,7 +582,9 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||||
disabled={isPending || editPending || initialValues?.is_done}
|
disabled={
|
||||||
|
isPending || editPending || initialValues?.comment ? true : false
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{isPending || editPending ? (
|
{isPending || editPending ? (
|
||||||
<Loader2 className="animate-spin" />
|
<Loader2 className="animate-spin" />
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ const PalanTable = ({
|
|||||||
<TableHead className="text-start">Reja nomi</TableHead>
|
<TableHead className="text-start">Reja nomi</TableHead>
|
||||||
<TableHead className="text-start">Tavsifi</TableHead>
|
<TableHead className="text-start">Tavsifi</TableHead>
|
||||||
<TableHead className="text-start">Kimga tegishli</TableHead>
|
<TableHead className="text-start">Kimga tegishli</TableHead>
|
||||||
|
<TableHead className="text-start">Shifokor biriktirgan</TableHead>
|
||||||
|
<TableHead className="text-start">
|
||||||
|
Dorixonaga biriktirgan
|
||||||
|
</TableHead>
|
||||||
<TableHead className="text-start">Status</TableHead>
|
<TableHead className="text-start">Status</TableHead>
|
||||||
<TableHead className="text-right">Harakatlar</TableHead>
|
<TableHead className="text-right">Harakatlar</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -72,12 +76,20 @@ const PalanTable = ({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{plan.user.first_name + " " + plan.user.last_name}
|
{plan.user.first_name + " " + plan.user.last_name}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{plan.doctor
|
||||||
|
? plan.doctor.first_name + " " + plan.doctor.last_name
|
||||||
|
: "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{plan.pharmacy ? plan.pharmacy.name : "-"}
|
||||||
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className={clsx(
|
className={clsx(
|
||||||
plan.is_done ? "text-green-500" : "text-red-500",
|
plan.comment ? "text-green-500" : "text-red-500",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{plan.is_done ? "Bajarilgan" : "Bajarilmagan"}
|
{plan.comment ? "Bajarilgan" : "Bajarilmagan"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="flex gap-2 justify-end">
|
<TableCell className="flex gap-2 justify-end">
|
||||||
<Button
|
<Button
|
||||||
@@ -94,7 +106,7 @@ const PalanTable = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={plan.is_done}
|
disabled={plan.comment ? true : false}
|
||||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setEditingPlan(plan);
|
setEditingPlan(plan);
|
||||||
@@ -107,7 +119,7 @@ const PalanTable = ({
|
|||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
disabled={plan.is_done}
|
disabled={plan.comment ? true : false}
|
||||||
onClick={() => handleDelete(plan)}
|
onClick={() => handleDelete(plan)}
|
||||||
>
|
>
|
||||||
<Trash className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
|
|||||||
@@ -8,6 +8,13 @@ import {
|
|||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
|
|
||||||
import { Badge } from "@/shared/ui/badge";
|
import { Badge } from "@/shared/ui/badge";
|
||||||
|
import {
|
||||||
|
Circle,
|
||||||
|
Map,
|
||||||
|
Placemark,
|
||||||
|
YMaps,
|
||||||
|
ZoomControl,
|
||||||
|
} from "@pbe/react-yandex-maps";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { type Dispatch, type SetStateAction } from "react";
|
import { type Dispatch, type SetStateAction } from "react";
|
||||||
|
|
||||||
@@ -22,7 +29,7 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={detail} onOpenChange={setDetail}>
|
<Dialog open={detail} onOpenChange={setDetail}>
|
||||||
<DialogContent className="sm:max-w-lg">
|
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-xl font-semibold">
|
<DialogTitle className="text-xl font-semibold">
|
||||||
Reja haqida batafsil
|
Reja haqida batafsil
|
||||||
@@ -30,19 +37,47 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<DialogDescription className="space-y-4 mt-2 text-md">
|
<DialogDescription className="space-y-4 mt-2 text-md">
|
||||||
{/* Reja nomi */}
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-gray-900">Reja nomi:</p>
|
<p className="font-semibold text-gray-900">Reja nomi:</p>
|
||||||
<p>{plan.title}</p>
|
<p>{plan.title}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reja tavsifi */}
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-gray-900">Tavsifi:</p>
|
<p className="font-semibold text-gray-900">Tavsifi:</p>
|
||||||
<p>{plan.description}</p>
|
<p>{plan.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Kimga tegishli */}
|
{plan.comment && (
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-900">Qanday bajarildi:</p>
|
||||||
|
<p>{plan.comment}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plan.doctor && (
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-900">
|
||||||
|
Shikorga biriktirgan
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>Shifokot ismi: </span>
|
||||||
|
{plan.doctor.first_name} {plan.doctor.last_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{plan.pharmacy && (
|
||||||
|
<div>
|
||||||
|
<p className="font-semibold text-gray-900">
|
||||||
|
Dorixonaga biriktirgan
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<span>Dorixona nomi: </span>
|
||||||
|
{plan.pharmacy.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-gray-900">Kimga tegishli:</p>
|
<p className="font-semibold text-gray-900">Kimga tegishli:</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -50,21 +85,56 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reja statusi */}
|
|
||||||
<div>
|
<div>
|
||||||
<p className="font-semibold text-gray-900">Reja statusi:</p>
|
<p className="font-semibold text-gray-900">Reja statusi:</p>
|
||||||
|
|
||||||
<Badge
|
<Badge
|
||||||
className={clsx(
|
className={clsx(
|
||||||
plan.is_done
|
plan.comment
|
||||||
? "bg-green-100 text-green-700"
|
? "bg-green-100 text-green-700"
|
||||||
: "bg-yellow-100 text-yellow-700",
|
: "bg-yellow-100 text-yellow-700",
|
||||||
"text-sm px-4 py-2 mt-2",
|
"text-sm px-4 py-2 mt-2",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{plan.is_done ? "Bajarilgan" : "Bajarilmagan"}
|
{plan.comment ? "Bajarilgan" : "Bajarilmagan"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{plan.doctor
|
||||||
|
? "Shifokor manzili:"
|
||||||
|
: plan.pharmacy && "Dorixona manzili:"}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<YMaps query={{ lang: "en_RU" }}>
|
||||||
|
<div className="h-[300px] w-full rounded-md overflow-hidden">
|
||||||
|
<Map
|
||||||
|
state={{
|
||||||
|
center: [plan.latitude, plan.longitude],
|
||||||
|
zoom: 12,
|
||||||
|
}}
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
>
|
||||||
|
<ZoomControl
|
||||||
|
options={{
|
||||||
|
position: { right: "10px", bottom: "70px" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Placemark geometry={[plan.latitude, plan.longitude]} />
|
||||||
|
|
||||||
|
<Circle
|
||||||
|
geometry={[[plan.latitude, plan.longitude], 300]}
|
||||||
|
options={{
|
||||||
|
fillColor: "rgba(255, 100, 0, 0.3)",
|
||||||
|
strokeColor: "rgba(255, 100, 0, 0.8)",
|
||||||
|
strokeWidth: 2,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Map>
|
||||||
|
</div>
|
||||||
|
</YMaps>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
18
src/features/support/lib/api.ts
Normal file
18
src/features/support/lib/api.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import type { SupportListRes } from "@/features/support/lib/data";
|
||||||
|
import httpClient from "@/shared/config/api/httpClient";
|
||||||
|
import { API_URLS } from "@/shared/config/api/URLs";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
export const support_api = {
|
||||||
|
async list(params: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
problem?: string;
|
||||||
|
district?: string;
|
||||||
|
user?: string;
|
||||||
|
date?: string;
|
||||||
|
}): Promise<AxiosResponse<SupportListRes>> {
|
||||||
|
const res = await httpClient.get(API_URLS.SUPPORT, { params });
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
27
src/features/support/lib/data.ts
Normal file
27
src/features/support/lib/data.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export interface SupportListRes {
|
||||||
|
status_code: number;
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
count: number;
|
||||||
|
next: null | string;
|
||||||
|
previous: null | string;
|
||||||
|
results: SupportListData[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export interface SupportListData {
|
||||||
|
id: number;
|
||||||
|
problem: string;
|
||||||
|
date: string;
|
||||||
|
type: "PROBLEM" | "HELP";
|
||||||
|
district: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
user: {
|
||||||
|
id: number;
|
||||||
|
first_name: string;
|
||||||
|
last_name: string;
|
||||||
|
};
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
70
src/features/support/ui/SupportDetail.tsx
Normal file
70
src/features/support/ui/SupportDetail.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import type { SupportListData } from "@/features/support/lib/data";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
|
const SupportDetail = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
supportDetail,
|
||||||
|
}: {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
supportDetail: SupportListData | null;
|
||||||
|
}) => {
|
||||||
|
if (supportDetail === null) return null;
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Batafsil</DialogTitle>
|
||||||
|
<DialogDescription className="mt-5 flex flex-col gap-2">
|
||||||
|
<p className="text-black text-lg font-medium">
|
||||||
|
<span className="text-foreground">Kim jo'natgan:</span>{" "}
|
||||||
|
{supportDetail?.user.first_name} {supportDetail?.user.last_name}
|
||||||
|
</p>
|
||||||
|
{supportDetail?.district && (
|
||||||
|
<p className="text-black text-lg font-medium">
|
||||||
|
<span className="text-foreground">Tuman nomi:</span>{" "}
|
||||||
|
{supportDetail?.district?.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<p className="text-black text-lg font-medium">
|
||||||
|
<span className="text-foreground">Jo'natilgan vaqti:</span>{" "}
|
||||||
|
{supportDetail?.date}
|
||||||
|
</p>
|
||||||
|
<p className="text-black text-lg font-medium">
|
||||||
|
<span className="text-foreground">Xabar turi:</span>{" "}
|
||||||
|
{supportDetail?.type === "PROBLEM"
|
||||||
|
? "Muommo hal qilish"
|
||||||
|
: "Yordam so'rash"}
|
||||||
|
</p>
|
||||||
|
<p className="text-black text-lg font-medium">
|
||||||
|
<span className="text-foreground">Xabar tavsifi:</span>{" "}
|
||||||
|
{supportDetail?.problem}
|
||||||
|
</p>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter className="mt-5">
|
||||||
|
<Button
|
||||||
|
className="bg-blue-500 hover:bg-blue-500 cursor-pointer w-32 h-12 text-md"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yopish
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SupportDetail;
|
||||||
225
src/features/support/ui/SupportList.tsx
Normal file
225
src/features/support/ui/SupportList.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { support_api } from "@/features/support/lib/api";
|
||||||
|
import type { SupportListData } from "@/features/support/lib/data";
|
||||||
|
import SupportDetail from "@/features/support/ui/SupportDetail";
|
||||||
|
import formatDate from "@/shared/lib/formatDate";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Calendar } from "@/shared/ui/calendar";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import Pagination from "@/shared/ui/pagination";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/shared/ui/table";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { ChevronDownIcon, Eye, Loader2 } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
const SupportList = () => {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [nameFilter, setNameFilter] = useState<string>("");
|
||||||
|
const [disctritFilter, setDisctritFilter] = useState<string | null>(null);
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [openDate, setOpenDate] = useState<boolean>(false);
|
||||||
|
const [supportDetail, setSupportDetail] = useState<SupportListData | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const [dateFilter, setDateFilter] = useState<Date | undefined>(undefined);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (disctritFilter?.length === 0) {
|
||||||
|
setDisctritFilter(null);
|
||||||
|
}
|
||||||
|
}, [disctritFilter]);
|
||||||
|
|
||||||
|
const limit = 20;
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
"factory_list",
|
||||||
|
currentPage,
|
||||||
|
nameFilter,
|
||||||
|
disctritFilter,
|
||||||
|
dateFilter,
|
||||||
|
],
|
||||||
|
queryFn: () => {
|
||||||
|
const params: {
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
problem?: string;
|
||||||
|
district?: string;
|
||||||
|
user?: string;
|
||||||
|
date?: string;
|
||||||
|
} = {
|
||||||
|
limit: limit,
|
||||||
|
offset: (currentPage - 1) * limit,
|
||||||
|
user: nameFilter,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (disctritFilter !== null) {
|
||||||
|
params.district = disctritFilter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dateFilter) {
|
||||||
|
params.date = formatDate.format(dateFilter, "YYYY-MM-DD");
|
||||||
|
}
|
||||||
|
|
||||||
|
return support_api.list(params);
|
||||||
|
},
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const totalPages = data ? Math.ceil(data?.count / limit) : 1;
|
||||||
|
|
||||||
|
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">
|
||||||
|
<h1 className="text-2xl font-bold">Yordam so'rovlari ro'yxati</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Foydalanuvchi nomi"
|
||||||
|
className="h-12"
|
||||||
|
value={nameFilter}
|
||||||
|
onChange={(e) => setNameFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Tuman nomi"
|
||||||
|
className="h-12"
|
||||||
|
value={disctritFilter ?? ""}
|
||||||
|
onChange={(e) => setDisctritFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Popover open={openDate} onOpenChange={setOpenDate}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
id="date"
|
||||||
|
className="w-48 justify-between font-normal h-12"
|
||||||
|
>
|
||||||
|
{dateFilter ? dateFilter.toDateString() : "Sana"}
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-auto overflow-hidden p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={dateFilter}
|
||||||
|
captionLayout="dropdown"
|
||||||
|
onSelect={(date) => {
|
||||||
|
setDateFilter(date);
|
||||||
|
setOpenDate(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="p-2 border-t bg-white">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => {
|
||||||
|
setDateFilter(undefined);
|
||||||
|
setOpenDate(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Tozalash
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
{isLoading && (
|
||||||
|
<div className="h-full flex items-center justify-center bg-white/70 z-10">
|
||||||
|
<span className="text-lg font-medium">
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<div className="h-full flex items-center justify-center z-10">
|
||||||
|
<span className="text-lg font-medium text-red-600">
|
||||||
|
Ma'lumotlarni olishda xatolik yuz berdi.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!isLoading && !isError && (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="text-center">
|
||||||
|
<TableHead className="text-start">ID</TableHead>
|
||||||
|
<TableHead className="text-start">Kim jo'natgan</TableHead>
|
||||||
|
<TableHead className="text-start">Habar haqida</TableHead>
|
||||||
|
<TableHead className="text-start">Habar turi</TableHead>
|
||||||
|
<TableHead className="text-start">Tuman</TableHead>
|
||||||
|
<TableHead className="text-start">Jo'natilgan sanasi</TableHead>
|
||||||
|
<TableHead className="text-start">Amallar</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{data && data.results.length > 0 ? (
|
||||||
|
data?.results.map((plan) => (
|
||||||
|
<TableRow key={plan.id} className="text-start">
|
||||||
|
<TableCell>{plan.id}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{plan.user.first_name} {plan.user.last_name}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{plan.problem.slice(0, 50)}...</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{plan.type === "PROBLEM"
|
||||||
|
? "Muommo hal qilish"
|
||||||
|
: "Yordam so'rash"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{plan.district ? plan.district.name : "-"}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>{plan.date}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Button
|
||||||
|
className="bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setOpen(true);
|
||||||
|
setSupportDetail(plan);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Eye />
|
||||||
|
</Button>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={7} className="text-center py-4 text-lg">
|
||||||
|
Farmasevtika topilmadi.
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={currentPage}
|
||||||
|
setCurrentPage={setCurrentPage}
|
||||||
|
totalPages={totalPages}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SupportDetail
|
||||||
|
open={open}
|
||||||
|
setOpen={setOpen}
|
||||||
|
supportDetail={supportDetail}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SupportList;
|
||||||
12
src/pages/Distributed.tsx
Normal file
12
src/pages/Distributed.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import DistributedList from "@/features/distributed/ui/DistributedList";
|
||||||
|
import SidebarLayout from "@/SidebarLayout";
|
||||||
|
|
||||||
|
const Distributed = () => {
|
||||||
|
return (
|
||||||
|
<SidebarLayout>
|
||||||
|
<DistributedList />
|
||||||
|
</SidebarLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Distributed;
|
||||||
12
src/pages/Support.tsx
Normal file
12
src/pages/Support.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import SupportList from "@/features/support/ui/SupportList";
|
||||||
|
import SidebarLayout from "@/SidebarLayout";
|
||||||
|
|
||||||
|
const Support = () => {
|
||||||
|
return (
|
||||||
|
<SidebarLayout>
|
||||||
|
<SupportList />
|
||||||
|
</SidebarLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Support;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import LoginLayout from "@/LoginLayout";
|
import LoginLayout from "@/LoginLayout";
|
||||||
|
import Distributed from "@/pages/Distributed";
|
||||||
import Districts from "@/pages/Districts";
|
import Districts from "@/pages/Districts";
|
||||||
import Doctors from "@/pages/Doctors";
|
import Doctors from "@/pages/Doctors";
|
||||||
import UsersPage from "@/pages/Home";
|
import UsersPage from "@/pages/Home";
|
||||||
@@ -11,6 +12,7 @@ import Region from "@/pages/Region";
|
|||||||
import Reports from "@/pages/Reports";
|
import Reports from "@/pages/Reports";
|
||||||
import SentLocations from "@/pages/SentLocations";
|
import SentLocations from "@/pages/SentLocations";
|
||||||
import Specifications from "@/pages/Specifications";
|
import Specifications from "@/pages/Specifications";
|
||||||
|
import Support from "@/pages/Support";
|
||||||
import TourPlan from "@/pages/TourPlan";
|
import TourPlan from "@/pages/TourPlan";
|
||||||
import routesConfig from "@/providers/routing/config";
|
import routesConfig from "@/providers/routing/config";
|
||||||
import { Navigate, useRoutes } from "react-router-dom";
|
import { Navigate, useRoutes } from "react-router-dom";
|
||||||
@@ -78,6 +80,14 @@ const AppRouter = () => {
|
|||||||
path: "/dashboard/region",
|
path: "/dashboard/region",
|
||||||
element: <Region />,
|
element: <Region />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/dashboard/support",
|
||||||
|
element: <Support />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/dashboard/distributed-product/",
|
||||||
|
element: <Distributed />,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return routes;
|
return routes;
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
|
const API_V = "/api/v1/";
|
||||||
|
|
||||||
export const API_URLS = {
|
export const API_URLS = {
|
||||||
BASE_URL: import.meta.env.VITE_API_URL || "https://api.meridynpharma.com",
|
BASE_URL: import.meta.env.VITE_API_URL || "https://api.meridynpharma.com",
|
||||||
LOGIN: "/api/v1/authentication/admin_login/",
|
LOGIN: `${API_V}authentication/admin_login/`,
|
||||||
USER: "/api/v1/admin/user/",
|
USER: `${API_V}admin/user/`,
|
||||||
REGION: "/api/v1/admin/district/",
|
REGION: `${API_V}admin/district/`,
|
||||||
REGIONS: "/api/v1/admin/region/",
|
REGIONS: `${API_V}admin/region/`,
|
||||||
DISTRICT: "/api/v1/admin/district/",
|
DISTRICT: `${API_V}admin/district/`,
|
||||||
DOCTOR: "/api/v1/admin/doctor/",
|
DOCTOR: `${API_V}admin/doctor/`,
|
||||||
OBJECT: "/api/v1/admin/place/",
|
OBJECT: `${API_V}admin/place/`,
|
||||||
PHARMACIES: "/api/v1/admin/pharmacy/",
|
PHARMACIES: `${API_V}admin/pharmacy/`,
|
||||||
PLANS: "/api/v1/admin/plan/",
|
PLANS: `${API_V}admin/plan/`,
|
||||||
PILL: "/api/v1/admin/product/",
|
PILL: `${API_V}admin/product/`,
|
||||||
LOCATION: "/api/v1/admin/location/",
|
LOCATION: `${API_V}admin/location/`,
|
||||||
USER_LOCATION: "/api/v1/admin/user_location/",
|
USER_LOCATION: `${API_V}admin/user_location/`,
|
||||||
ORDER: "/api/v1/admin/order/",
|
ORDER: `${API_V}admin/order/`,
|
||||||
FACTORY: "/api/v1/admin/factory/",
|
FACTORY: `${API_V}admin/factory/`,
|
||||||
REPORT: "/api/v1/admin/payment/",
|
REPORT: `${API_V}admin/payment/`,
|
||||||
TOUR_PLAN: "/api/v1/admin/tour_plan/",
|
TOUR_PLAN: `${API_V}admin/tour_plan/`,
|
||||||
|
SUPPORT: `${API_V}admin/support/list/`,
|
||||||
|
DISTRIBUTED: `${API_V}admin/distributed_product/list/`,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
AlertCircle,
|
||||||
BriefcaseMedical,
|
BriefcaseMedical,
|
||||||
Building2,
|
Building2,
|
||||||
Calendar,
|
Calendar,
|
||||||
@@ -96,6 +97,16 @@ const items = [
|
|||||||
url: "/dashboard/pharmaceuticals",
|
url: "/dashboard/pharmaceuticals",
|
||||||
icon: Microscope,
|
icon: Microscope,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: "Yordam so'rovlari",
|
||||||
|
url: "/dashboard/support",
|
||||||
|
icon: AlertCircle,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "Tarqatilgan dorilar",
|
||||||
|
url: "/dashboard/distributed-product",
|
||||||
|
icon: Pill,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function AppSidebar() {
|
export function AppSidebar() {
|
||||||
|
|||||||
Reference in New Issue
Block a user