Compare commits
15 Commits
e656ad2e61
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1498c4828 | ||
|
|
2dd4bccfcb | ||
|
|
80df127d6e | ||
|
|
88dc5470d9 | ||
|
|
30f842c53d | ||
|
|
18e43f0eb8 | ||
|
|
92151f4b98 | ||
|
|
ce0286e075 | ||
|
|
8b156ab330 | ||
| 04a2f19310 | |||
| d0671f38ac | |||
| 3a2436593c | |||
| 32519a8820 | |||
| 4670b27e53 | |||
| 7c1662404b |
@@ -1 +1,2 @@
|
||||
VITE_API_URL=string
|
||||
VITE_SOCKET_URL=string
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,6 +11,8 @@ node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
.env
|
||||
.example.env
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
|
||||
31
Dockerfile
Normal file
31
Dockerfile
Normal file
@@ -0,0 +1,31 @@
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm i ---force
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
RUN rm -rf /usr/share/nginx/html/*
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
RUN echo 'server { \
|
||||
listen 80; \
|
||||
server_name _; \
|
||||
root /usr/share/nginx/html; \
|
||||
index index.html; \
|
||||
location / { \
|
||||
try_files $uri /index.html; \
|
||||
} \
|
||||
error_page 404 /index.html; \
|
||||
}' > /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
12
docker-compose.yaml
Normal file
12
docker-compose.yaml
Normal file
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
frontend:
|
||||
build: .
|
||||
container_name: meridyn-admin
|
||||
ports:
|
||||
- "3002:80"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /app/node_modules
|
||||
environment:
|
||||
- CHOKIDAR_USEPOLLING=true
|
||||
- HOST=0.0.0.0
|
||||
32
package-lock.json
generated
32
package-lock.json
generated
@@ -43,7 +43,8 @@
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"zod": "^4.1.13"
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
@@ -6156,6 +6157,35 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/zustand": {
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz",
|
||||
"integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.20.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18.0.0",
|
||||
"immer": ">=9.0.6",
|
||||
"react": ">=18.0.0",
|
||||
"use-sync-external-store": ">=1.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"immer": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"use-sync-external-store": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,8 @@
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.13",
|
||||
"zod": "^4.1.13"
|
||||
"zod": "^4.1.13",
|
||||
"zustand": "^5.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.25.0",
|
||||
|
||||
@@ -1,8 +1,26 @@
|
||||
import { user_api } from "@/shared/config/api/user/api";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import { SidebarProvider, SidebarTrigger } from "@/shared/ui/sidebar";
|
||||
import { AppSidebar } from "@/widgets/sidebar-layout";
|
||||
import React from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import React, { useEffect } from "react";
|
||||
|
||||
const SidebarLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
const { data } = useQuery({
|
||||
queryKey: ["get_me"],
|
||||
queryFn: () => user_api.getMe(),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
const { addUser } = userStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
addUser(data);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
|
||||
@@ -62,10 +62,17 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
const errMessageName = err.response?.data as { data: { name: [string] } };
|
||||
const messageTextName = errMessageName.data.name[0];
|
||||
toast.error(
|
||||
(messageTextName && "Bu tuman oldin qo'shilgan") ||
|
||||
messageText ||
|
||||
"Xatolik yuz berdi",
|
||||
{
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
},
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -84,8 +91,10 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const errMessageName = err.response?.data as { data: { name: [string] } };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
const messageTextName = errMessageName.data.name[0];
|
||||
toast.error(messageTextName || messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
@@ -203,7 +212,7 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
{u.first_name} {u.last_name} {u.region?.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { DistrictListData } from "@/features/districts/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
@@ -30,6 +32,8 @@ const TableDistrict = ({
|
||||
setEditingDistrict,
|
||||
currentPage,
|
||||
}: Props) => {
|
||||
const { user } = userStore();
|
||||
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
@@ -55,6 +59,7 @@ const TableDistrict = ({
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Tuman nomi</TableHead>
|
||||
<TableHead>Kim qo‘shgan</TableHead>
|
||||
<TableHead>Qo'shgan sanasi</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -67,6 +72,9 @@ const TableDistrict = ({
|
||||
<TableCell>
|
||||
{d.user.first_name} {d.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(d.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -79,15 +87,17 @@ const TableDistrict = ({
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(d)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
{user?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(d)}
|
||||
disabled={!user?.is_superuser}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -37,4 +37,11 @@ export const doctor_api = {
|
||||
const res = await httpClient.delete(`${API_URLS.DOCTOR}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
|
||||
async export() {
|
||||
const res = await httpClient.get(`${API_URLS.DOCTOR_EXPORT}`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -455,7 +455,7 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
{u.first_name} {u.last_name} {u.region?.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { doctor_api } from "@/features/doctors/lib/api";
|
||||
import type { DoctorListResData } from "@/features/doctors/lib/data";
|
||||
import AddedDoctor from "@/features/doctors/ui/AddedDoctor";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
@@ -9,8 +10,11 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { CloudDownload, Loader2, Plus } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
searchName: string;
|
||||
@@ -49,6 +53,45 @@ const FilterDoctor = ({
|
||||
setEditingPlan,
|
||||
editingPlan,
|
||||
}: Props) => {
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await doctor_api.export();
|
||||
return res.data;
|
||||
},
|
||||
onSuccess: (data: Blob) => {
|
||||
// Blob URL yaratish
|
||||
const url = window.URL.createObjectURL(
|
||||
new Blob([data], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
}),
|
||||
);
|
||||
|
||||
// <a> elementi orqali yuklab olish
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "doctor_export.xlsx"); // Fayl nomi
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
|
||||
// Blob URL-ni ozod qilish
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast.success("Excel muvaffaqiyatli yuklab olindi", {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div className="flex justify-end gap-2 w-full">
|
||||
<Input
|
||||
@@ -87,6 +130,17 @@ const FilterDoctor = ({
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => mutate()}
|
||||
disabled={isPending}
|
||||
>
|
||||
<CloudDownload className="!h-5 !w-5" /> Excel formatda yuklash
|
||||
{isPending && <Loader2 className="animate-spin" />}
|
||||
</Button>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { DoctorListResData } from "@/features/doctors/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
@@ -36,6 +38,7 @@ const TableDoctor = ({
|
||||
handleDelete,
|
||||
isFetching,
|
||||
}: Props) => {
|
||||
const { user: getme } = userStore();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{(isLoading || isFetching) && (
|
||||
@@ -66,6 +69,7 @@ const TableDoctor = ({
|
||||
<TableHead>Ish joyi</TableHead>
|
||||
<TableHead>Sohasi</TableHead>
|
||||
<TableHead>Kim qo'shgan</TableHead>
|
||||
<TableHead>Qo'shgan sanasi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -89,6 +93,9 @@ const TableDoctor = ({
|
||||
<TableCell>
|
||||
{item.user.first_name} {item.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(item.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -112,14 +119,17 @@ const TableDoctor = ({
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
disabled={!getme?.is_superuser}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LocationListDataRes } from "@/features/location/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ const LocationTable = ({
|
||||
setDetailDialog,
|
||||
handleDelete,
|
||||
}: Props) => {
|
||||
const { user: getme } = userStore();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
@@ -72,14 +74,17 @@ const LocationTable = ({
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
disabled={!getme?.is_superuser}
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { LocationListDataRes } from "@/features/location/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -25,6 +26,7 @@ const UserLocationTable = ({
|
||||
setDetailDialog,
|
||||
handleDelete,
|
||||
}: Props) => {
|
||||
const { user: getme } = userStore();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
@@ -72,14 +74,17 @@ const UserLocationTable = ({
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
disabled={!getme?.is_superuser}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -332,7 +332,7 @@ export default function AddedObject({ initialValues, setDialogOpen }: Props) {
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
{u.first_name} {u.last_name} {u.region?.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { ObjectListData } from "@/features/objects/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -33,6 +35,7 @@ const ObjectTable = ({
|
||||
isError,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
const { user: getme } = userStore();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
@@ -58,6 +61,7 @@ const ObjectTable = ({
|
||||
<TableHead>Obyekt nomi</TableHead>
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Foydalanuvchi</TableHead>
|
||||
<TableHead>Qo'shilgan sanasi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -73,6 +77,9 @@ const ObjectTable = ({
|
||||
<TableCell>
|
||||
{item.user.first_name} {item.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(item.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -96,14 +103,17 @@ const ObjectTable = ({
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
disabled={!getme?.is_superuser}
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
@@ -5,6 +5,8 @@ import {
|
||||
} from "@/features/pharm/lib/data";
|
||||
import AddedPharm from "@/features/pharm/ui/AddedPharm";
|
||||
import DeletePharm from "@/features/pharm/ui/DeletePharm";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -28,6 +30,7 @@ import { Edit, Loader2, Plus, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const PharmList = () => {
|
||||
const { user: getme } = userStore();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [nameFilter, setNameFilter] = useState<string>("");
|
||||
const limit = 20;
|
||||
@@ -119,6 +122,7 @@ const PharmList = () => {
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Nomi</TableHead>
|
||||
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
|
||||
<TableHead className="text-end">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -128,6 +132,9 @@ const PharmList = () => {
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.name}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -140,14 +147,17 @@ const PharmList = () => {
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={!getme.is_superuser}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
@@ -38,4 +38,11 @@ export const pharmacies_api = {
|
||||
const res = await httpClient.delete(`${API_URLS.PHARMACIES}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
|
||||
async export() {
|
||||
const res = await httpClient.get(`${API_URLS.PHARMACIES_EXPORT}`, {
|
||||
responseType: "blob",
|
||||
});
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -123,9 +123,9 @@ export interface PharmaciesListData {
|
||||
|
||||
export interface CreatePharmaciesReq {
|
||||
name: string;
|
||||
inn: string;
|
||||
inn?: string;
|
||||
owner_phone: string;
|
||||
responsible_phone: string;
|
||||
responsible_phone?: string;
|
||||
district_id: number;
|
||||
place_id: number;
|
||||
user_id: number;
|
||||
@@ -136,9 +136,9 @@ export interface CreatePharmaciesReq {
|
||||
|
||||
export interface UpdatePharmaciesReq {
|
||||
name: string;
|
||||
inn: string;
|
||||
inn?: string;
|
||||
owner_phone: string;
|
||||
responsible_phone: string;
|
||||
responsible_phone?: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: { longitude: number; latitude: number };
|
||||
|
||||
@@ -2,9 +2,9 @@ import z from "zod";
|
||||
|
||||
export const PharmForm = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
inn: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
inn: z.string().optional(),
|
||||
phone_number: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
additional_phone: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
additional_phone: z.string().optional(),
|
||||
district: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
object: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
|
||||
@@ -249,36 +249,31 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof PharmForm>) {
|
||||
const baseBody = {
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
name: values.name,
|
||||
owner_phone: onlyNumber(values.phone_number),
|
||||
...(values.additional_phone && {
|
||||
responsible_phone: onlyNumber(values.additional_phone),
|
||||
}),
|
||||
...(values.inn && { inn: values.inn }), // 👈 faqat bo‘lsa yuboriladi
|
||||
};
|
||||
|
||||
if (initialValues) {
|
||||
edit({
|
||||
id: initialValues.id,
|
||||
body: {
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
inn: values.inn,
|
||||
name: values.name,
|
||||
owner_phone: onlyNumber(values.phone_number),
|
||||
responsible_phone: onlyNumber(values.additional_phone),
|
||||
},
|
||||
body: baseBody,
|
||||
});
|
||||
} else {
|
||||
mutate({
|
||||
...baseBody,
|
||||
district_id: Number(values.district),
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
inn: values.inn,
|
||||
name: values.name,
|
||||
owner_phone: onlyNumber(values.phone_number),
|
||||
place_id: Number(values.object),
|
||||
responsible_phone: onlyNumber(values.additional_phone),
|
||||
user_id: Number(values.user),
|
||||
});
|
||||
}
|
||||
@@ -343,7 +338,7 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
|
||||
<Input
|
||||
placeholder="+998 90 123-45-67"
|
||||
{...field}
|
||||
value={formatPhone(field.value)}
|
||||
value={field.value && formatPhone(field.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
@@ -419,7 +414,7 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
{u.first_name} {u.last_name} {u.region?.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { pharmacies_api } from "@/features/pharmacies/lib/api";
|
||||
import type { PharmaciesListData } from "@/features/pharmacies/lib/data";
|
||||
import AddedPharmacies from "@/features/pharmacies/ui/AddedPharmacies";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
@@ -9,8 +10,11 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { CloudDownload, Loader2, Plus } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
searchName: string;
|
||||
@@ -41,6 +45,45 @@ const PharmaciesFilter = ({
|
||||
setEditingPlan,
|
||||
editingPlan,
|
||||
}: Props) => {
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: async () => {
|
||||
const res = await pharmacies_api.export();
|
||||
return res.data;
|
||||
},
|
||||
onSuccess: (data: Blob) => {
|
||||
// Blob URL yaratish
|
||||
const url = window.URL.createObjectURL(
|
||||
new Blob([data], {
|
||||
type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
}),
|
||||
);
|
||||
|
||||
// <a> elementi orqali yuklab olish
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.setAttribute("download", "dorixonalar.xlsx"); // Fayl nomi
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
|
||||
// Blob URL-ni ozod qilish
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
toast.success("Excel muvaffaqiyatli yuklab olindi", {
|
||||
position: "top-center",
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
return (
|
||||
<div className="flex justify-end gap-2 w-full">
|
||||
<Input
|
||||
@@ -67,6 +110,15 @@ const PharmaciesFilter = ({
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => mutate()}
|
||||
disabled={isPending}
|
||||
>
|
||||
<CloudDownload className="!h-5 !w-5" /> Excel formatda yuklash
|
||||
{isPending && <Loader2 className="animate-spin" />}
|
||||
</Button>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { PharmaciesListData } from "@/features/pharmacies/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -29,6 +31,7 @@ const PharmaciesTable = ({
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
}: Props) => {
|
||||
const { user: getme } = userStore();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
@@ -42,6 +45,7 @@ const PharmaciesTable = ({
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Obyekt</TableHead>
|
||||
<TableHead>Kim qo'shgan</TableHead>
|
||||
<TableHead>Qo'shgan sanasi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -59,6 +63,10 @@ const PharmaciesTable = ({
|
||||
{item.user.first_name} {item.user.last_name}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{formatDate.format(item.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -82,14 +90,17 @@ const PharmaciesTable = ({
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
disabled={!getme?.is_superuser}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -2,6 +2,8 @@ import { pill_api } from "@/features/pill/lib/api";
|
||||
import { type PillListData, type PillType } from "@/features/pill/lib/data";
|
||||
import AddedPill from "@/features/pill/ui/AddedPill";
|
||||
import DeletePill from "@/features/pill/ui/DeletePill";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -26,6 +28,7 @@ import { Edit, Plus, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const PillList = () => {
|
||||
const { user: getme } = userStore();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const limit = 20;
|
||||
const [nameFilter, setNameFilter] = useState<string>("");
|
||||
@@ -102,6 +105,7 @@ const PillList = () => {
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Nomi</TableHead>
|
||||
<TableHead className="text-start">Narxi</TableHead>
|
||||
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
|
||||
<TableHead className="text-end">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
@@ -111,6 +115,9 @@ const PillList = () => {
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.name}</TableCell>
|
||||
<TableCell>{formatPrice(plan.price)}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.created_at, "Dd-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -123,14 +130,17 @@ const PillList = () => {
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
disabled={!getme.is_superuser}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -51,7 +51,7 @@ export interface PlanListData {
|
||||
}
|
||||
|
||||
export interface PlanCreateReq {
|
||||
title: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
date: string;
|
||||
user_id: number;
|
||||
@@ -63,7 +63,7 @@ export interface PlanCreateReq {
|
||||
}
|
||||
|
||||
export interface PlanUpdateReq {
|
||||
title: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
date: string;
|
||||
longitude: number;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import z from "zod";
|
||||
|
||||
export const createPlanFormData = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
name: z.string().optional(),
|
||||
description: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
date: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
|
||||
@@ -49,7 +49,7 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
const form = useForm<z.infer<typeof createPlanFormData>>({
|
||||
resolver: zodResolver(createPlanFormData),
|
||||
defaultValues: {
|
||||
name: initialValues?.title || "",
|
||||
// name: initialValues?.title || "",
|
||||
description: initialValues?.description || "",
|
||||
user: initialValues ? String(initialValues.user.id) : "",
|
||||
date: initialValues ? initialValues?.date : "",
|
||||
@@ -174,13 +174,15 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
body: {
|
||||
date: formatDate.format(data.date, "YYYY-MM-DD"),
|
||||
description: data.description,
|
||||
...(data.name && {
|
||||
title: data.name,
|
||||
}),
|
||||
extra_location: {
|
||||
latitude: initialValues.latitude,
|
||||
longitude: initialValues.longitude,
|
||||
},
|
||||
latitude: initialValues.latitude,
|
||||
longitude: initialValues.longitude,
|
||||
title: data.name,
|
||||
},
|
||||
id: initialValues.id,
|
||||
});
|
||||
@@ -194,7 +196,9 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
},
|
||||
latitude: lat,
|
||||
longitude: long,
|
||||
title: data.name,
|
||||
...(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),
|
||||
@@ -274,7 +278,7 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
{u.first_name} {u.last_name} {u.region?.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { PlanListData } from "@/features/plans/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
@@ -33,6 +35,7 @@ const PalanTable = ({
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
}: Props) => {
|
||||
const { user: getme } = userStore();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{(isLoading || isFetching) && (
|
||||
@@ -58,6 +61,8 @@ const PalanTable = ({
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Reja nomi</TableHead>
|
||||
<TableHead className="text-start">Tavsifi</TableHead>
|
||||
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
|
||||
<TableHead className="text-start">Bajarilish sanasi</TableHead>
|
||||
<TableHead className="text-start">Kimga tegishli</TableHead>
|
||||
<TableHead className="text-start">Shifokor biriktirgan</TableHead>
|
||||
<TableHead className="text-start">
|
||||
@@ -73,6 +78,12 @@ const PalanTable = ({
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.title}</TableCell>
|
||||
<TableCell>{plan.description}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.date, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{plan.user.first_name + " " + plan.user.last_name}
|
||||
</TableCell>
|
||||
@@ -115,15 +126,19 @@ const PalanTable = ({
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
disabled={plan.comment ? true : false}
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
disabled={
|
||||
!getme?.is_superuser || plan.comment ? true : false
|
||||
}
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import type { RegionListResData } from "@/features/region/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
@@ -26,6 +28,7 @@ const RegionTable = ({
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (user: RegionListResData) => void;
|
||||
}) => {
|
||||
const { user: getme } = userStore();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
@@ -49,6 +52,7 @@ const RegionTable = ({
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Nomi</TableHead>
|
||||
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
@@ -56,6 +60,9 @@ const RegionTable = ({
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>{plan.name}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -68,14 +75,17 @@ const RegionTable = ({
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={!getme?.is_superuser}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
11
src/features/send-message/ui/Send.tsx
Normal file
11
src/features/send-message/ui/Send.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
const Send = () => {
|
||||
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">Xabar jo'natish</h1>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Send;
|
||||
@@ -420,7 +420,7 @@ export const AddedSpecification = ({ initialValues, setDialogOpen }: Props) => {
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
{u.first_name} {u.last_name} {u.region?.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { type OrderListDataRes } from "@/features/specifications/lib/data";
|
||||
import { AddedSpecification } from "@/features/specifications/ui/AddedSpecification";
|
||||
import DeleteOrder from "@/features/specifications/ui/DeleteOrder";
|
||||
import { SpecificationDetail } from "@/features/specifications/ui/SpecificationDetail ";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -34,6 +35,7 @@ const SpecificationsList = () => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const limit = 20;
|
||||
const { user: getme } = userStore();
|
||||
|
||||
const {
|
||||
data: order,
|
||||
@@ -154,14 +156,17 @@ const SpecificationsList = () => {
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
size="icon"
|
||||
disabled={!getme?.is_superuser}
|
||||
variant="destructive"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
@@ -202,7 +202,7 @@ const AddedTourPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
{u.first_name} {u.last_name} {u.region?.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PlanTourListDataRes } from "@/features/tour-plan/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ const TourPlanTable = ({
|
||||
setDialogOpen,
|
||||
setDetailOpen,
|
||||
}: Props) => {
|
||||
const { user: getme } = userStore();
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{(isLoading || isFetching) && (
|
||||
@@ -107,14 +109,17 @@ const TourPlanTable = ({
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
disabled={!getme?.is_superuser}
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
|
||||
@@ -47,3 +47,10 @@ export const user_api = {
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
export const send_message = {
|
||||
async send(body: { user_ids: number[]; message: string }) {
|
||||
const res = await httpClient.post(API_URLS.SEND_MESSAGE, body);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -65,11 +65,11 @@ export interface UserListRes {
|
||||
|
||||
export interface UserListData {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
first_name: string | null;
|
||||
last_name: string | null;
|
||||
region: {
|
||||
id: number;
|
||||
name: string;
|
||||
name: string | null;
|
||||
};
|
||||
is_active: boolean;
|
||||
telegram_id: string;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { region_api } from "@/features/region/lib/api";
|
||||
import type { RegionListResData } from "@/features/region/lib/data";
|
||||
import { send_message } from "@/features/users/lib/api";
|
||||
import type { UserListData } from "@/features/users/lib/data";
|
||||
import AddUsers from "@/features/users/ui/AddUsers";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
@@ -14,12 +15,21 @@ import {
|
||||
} from "@/shared/ui/command";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
@@ -28,9 +38,28 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Check, ChevronsUpDown, Loader2, Plus } from "lucide-react";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import {
|
||||
Check,
|
||||
ChevronsUpDown,
|
||||
Loader2,
|
||||
MessageCircle,
|
||||
Plus,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import z from "zod";
|
||||
|
||||
const FormSchema = z.object({
|
||||
text: z.string().min(10, {
|
||||
message: "Xabar uzunligi kamida 10ta belgidan katta bolishi kerak",
|
||||
}),
|
||||
});
|
||||
|
||||
interface Props {
|
||||
searchTerm: string;
|
||||
@@ -43,6 +72,10 @@ interface Props {
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
editingUser: UserListData | null;
|
||||
setEditingUser: Dispatch<SetStateAction<UserListData | null>>;
|
||||
setSendMessage: Dispatch<SetStateAction<boolean>>;
|
||||
setUserList: Dispatch<SetStateAction<number[] | []>>;
|
||||
sendMessage: boolean;
|
||||
userList: number[] | null;
|
||||
}
|
||||
|
||||
const Filter = ({
|
||||
@@ -51,14 +84,19 @@ const Filter = ({
|
||||
statusFilter,
|
||||
setStatusFilter,
|
||||
regionValue,
|
||||
sendMessage,
|
||||
setRegionValue,
|
||||
userList,
|
||||
dialogOpen,
|
||||
setUserList,
|
||||
setDialogOpen,
|
||||
editingUser,
|
||||
setEditingUser,
|
||||
setSendMessage,
|
||||
}: Props) => {
|
||||
const [openRegion, setOpenRegion] = useState(false);
|
||||
const [regionSearch, setRegionSearch] = useState("");
|
||||
const [message, setMessage] = useState<boolean>(false);
|
||||
|
||||
const { data: regions, isLoading } = useQuery({
|
||||
queryKey: ["region_list", regionSearch],
|
||||
@@ -66,6 +104,43 @@ const Filter = ({
|
||||
select: (res) => res.data.data,
|
||||
});
|
||||
|
||||
const form = useForm<z.infer<typeof FormSchema>>({
|
||||
resolver: zodResolver(FormSchema),
|
||||
defaultValues: {
|
||||
text: "",
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: { user_ids: number[]; message: string }) =>
|
||||
send_message.send(body),
|
||||
onSuccess: () => {
|
||||
toast.success("Xabar jo'natildi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
setMessage(false);
|
||||
setSendMessage(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(data: z.infer<typeof FormSchema>) {
|
||||
if (userList) {
|
||||
mutate({
|
||||
message: data.text,
|
||||
user_ids: userList,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
{/* Search input */}
|
||||
@@ -179,6 +254,7 @@ const Filter = ({
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingUser(null)}
|
||||
disabled={sendMessage}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
@@ -195,6 +271,102 @@ const Filter = ({
|
||||
<AddUsers initialData={editingUser} setDialogOpen={setDialogOpen} />
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => {
|
||||
setSendMessage((prev) => !prev);
|
||||
setUserList([]);
|
||||
}}
|
||||
>
|
||||
{sendMessage ? (
|
||||
<>
|
||||
<XIcon className="!h-5 !w-5" />
|
||||
Bekor qilish
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<MessageCircle className="!h-5 !w-5" />
|
||||
Xabar jo'natish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
{sendMessage && (
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => {
|
||||
if (userList === null) {
|
||||
toast.error("Kamida 1ta foydalanuvchi tanlash kerak.", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
} else {
|
||||
setMessage(true);
|
||||
form.reset({
|
||||
text: "",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Xabarni jo'natish
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Dialog open={message} onOpenChange={setMessage}>
|
||||
<DialogContent className="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Xabar jo'natish</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4 flex flex-col"
|
||||
>
|
||||
<FormField
|
||||
name="text"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Xabarni yozing</Label>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
className="min-h-44 max-h-64"
|
||||
placeholder="Xabar"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-2">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setMessage(false);
|
||||
setSendMessage(false);
|
||||
}}
|
||||
>
|
||||
Bekor qilish
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button type="submit">
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
"Jo'natish"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import type { UserListData, UserListRes } from "@/features/users/lib/data";
|
||||
import { userStore } from "@/shared/hooks/user";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Checkbox } from "@/shared/ui/checkbox";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -22,8 +25,11 @@ interface Props {
|
||||
isError: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setEditingUser: Dispatch<SetStateAction<UserListData | null>>;
|
||||
setUserList: Dispatch<SetStateAction<number[] | []>>;
|
||||
handleDelete: (user: UserListData) => void;
|
||||
currentPage: number;
|
||||
userList: number[] | null;
|
||||
sendMessage: boolean;
|
||||
}
|
||||
|
||||
const UserTable = ({
|
||||
@@ -32,18 +38,24 @@ const UserTable = ({
|
||||
isError,
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
userList,
|
||||
setEditingUser,
|
||||
setUserList,
|
||||
sendMessage,
|
||||
currentPage,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { user: getme } = userStore();
|
||||
const [pendingUserId, setPendingUserId] = useState<number | null>(null);
|
||||
|
||||
// TableHeader checkbox holati
|
||||
const allSelected = data?.data.data.results.length
|
||||
? userList?.length === data.data.data.results.length
|
||||
: false;
|
||||
|
||||
const { mutate: active } = useMutation({
|
||||
mutationFn: (id: number) => user_api.active(id),
|
||||
onMutate: (id) => {
|
||||
setPendingUserId(id);
|
||||
},
|
||||
onMutate: (id) => setPendingUserId(id),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["user_list"] });
|
||||
toast.success(`Foydalanuvchi aktivlashdi`);
|
||||
@@ -51,29 +63,50 @@ const UserTable = ({
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
setPendingUserId(null);
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
toast.error(errMessage?.message || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleActivate = (userId: number) => {
|
||||
active(userId);
|
||||
const handleActivate = (userId: number) => active(userId);
|
||||
|
||||
// TableHeader checkbox toggle
|
||||
const handleSelectAll = () => {
|
||||
if (!data) return;
|
||||
if (allSelected) {
|
||||
setUserList([]);
|
||||
setUserList([]);
|
||||
} else {
|
||||
const allIds = data.data.data.results.map((u) => u.id);
|
||||
setUserList(allIds);
|
||||
setUserList(allIds);
|
||||
}
|
||||
};
|
||||
|
||||
// Individual checkbox toggle
|
||||
const handleSelect = (id: number) => {
|
||||
let updated: number[] = [];
|
||||
if (userList) {
|
||||
if (userList?.includes(id)) {
|
||||
updated = userList.filter((i) => i !== id);
|
||||
} else {
|
||||
updated = [...userList, id];
|
||||
}
|
||||
setUserList(updated);
|
||||
setUserList(updated.length ? updated : []);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
<Loader2 className="animate-spin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<div className="h-full flex items-center justify-center z-10">
|
||||
<span className="text-lg font-medium text-red-600">
|
||||
@@ -81,78 +114,65 @@ const UserTable = ({
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isLoading && !isError && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-[16px] text-center">
|
||||
{sendMessage && (
|
||||
<TableHead className="text-start">
|
||||
<Checkbox
|
||||
id="user_id_all"
|
||||
checked={allSelected}
|
||||
onCheckedChange={handleSelectAll}
|
||||
/>
|
||||
</TableHead>
|
||||
)}
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Ismi</TableHead>
|
||||
<TableHead className="text-start">Familiyasi</TableHead>
|
||||
<TableHead className="text-start">Hududi</TableHead>
|
||||
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
|
||||
<TableHead className="text-center">Holati</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{data && data.data.data.results.length > 0 ? (
|
||||
data.data.data.results.map((user, index) => (
|
||||
<TableRow key={user.id} className="text-[14px] text-start">
|
||||
{sendMessage && (
|
||||
<TableCell className="text-start">
|
||||
<Checkbox
|
||||
id={`user_id_${user.id}`}
|
||||
checked={userList?.includes(user.id)}
|
||||
onCheckedChange={() => handleSelect(user.id)}
|
||||
/>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>{index + 1 + (currentPage - 1) * 20}</TableCell>
|
||||
<TableCell>{user.first_name}</TableCell>
|
||||
<TableCell>{user.last_name}</TableCell>
|
||||
<TableCell>{user.region.name}</TableCell>
|
||||
<TableCell>
|
||||
{user.first_name ? user.first_name : "No'malum"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.last_name ? user.last_name : "No'malum"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user?.region ? user?.region?.name : "No'malum"}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(user?.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{/* <Select
|
||||
value={user.is_active ? "true" : "false"}
|
||||
onValueChange={() => handleActivate(user.id)}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={clsx(
|
||||
"w-[180px] mx-auto",
|
||||
user.is_active
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800",
|
||||
)}
|
||||
>
|
||||
{pendingUserId === user.id ? (
|
||||
<Loader2 className="animate-spin h-4 w-4 mx-auto" />
|
||||
) : (
|
||||
<SelectValue placeholder="Holati" />
|
||||
)}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
disabled={pendingUserId === user.id}
|
||||
value="true"
|
||||
className="text-green-500 hover:!text-green-500"
|
||||
>
|
||||
{pendingUserId === user.id ? (
|
||||
<Loader2 className="animate-spin h-4 w-4 mx-auto" />
|
||||
) : (
|
||||
"Faol"
|
||||
)}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
disabled={pendingUserId === user.id}
|
||||
value="false"
|
||||
className="text-red-500 hover:!text-red-500"
|
||||
>
|
||||
{pendingUserId === user.id ? (
|
||||
<Loader2 className="animate-spin h-4 w-4 mx-auto" />
|
||||
) : (
|
||||
"Faol emas"
|
||||
)}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
<Button
|
||||
className={clsx(
|
||||
"mx-auto cursor-pointer",
|
||||
user.is_active
|
||||
? "bg-green-500 hover:bg-green-500"
|
||||
: "bg-blue-500 hover:bg-blue-500",
|
||||
: "bg-blue-500 hover:bg-blue-500",
|
||||
)}
|
||||
disabled={user.is_active}
|
||||
disabled={user.is_active || sendMessage}
|
||||
onClick={() => handleActivate(user.id)}
|
||||
>
|
||||
{pendingUserId === user.id ? (
|
||||
@@ -170,24 +190,28 @@ const UserTable = ({
|
||||
setEditingUser(user);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
disabled={sendMessage}
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(user)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
{getme?.is_superuser && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={!getme?.is_superuser || sendMessage}
|
||||
onClick={() => handleDelete(user)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-4 text-lg">
|
||||
<TableCell colSpan={7} className="text-center py-4 text-lg">
|
||||
Foydalanuvchilar topilmadi.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@@ -21,6 +21,8 @@ const UsersList = () => {
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | "true" | "false">(
|
||||
"all",
|
||||
);
|
||||
const [sendMessage, setSendMessage] = useState<boolean>(false);
|
||||
const [userList, setUserList] = useState<number[] | []>([]);
|
||||
|
||||
const [userDelete, setUserDelete] = useState<UserListData | null>(null);
|
||||
const [opneDelete, setOpenDelete] = useState<boolean>(false);
|
||||
@@ -72,12 +74,16 @@ const UsersList = () => {
|
||||
setSearchTerm={setSearchTerm}
|
||||
setStatusFilter={setStatusFilter}
|
||||
statusFilter={statusFilter}
|
||||
sendMessage={sendMessage}
|
||||
setUserList={setUserList}
|
||||
regionValue={regionValue}
|
||||
setSendMessage={setSendMessage}
|
||||
setRegionValue={setRegionValue}
|
||||
dialogOpen={dialogOpen}
|
||||
setDialogOpen={setDialogOpen}
|
||||
editingUser={editingUser}
|
||||
setEditingUser={setEditingUser}
|
||||
userList={userList}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -89,6 +95,9 @@ const UsersList = () => {
|
||||
setDialogOpen={setDialogOpen}
|
||||
handleDelete={handleDelete}
|
||||
currentPage={currentPage}
|
||||
setUserList={setUserList}
|
||||
userList={userList}
|
||||
sendMessage={sendMessage}
|
||||
/>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
|
||||
12
src/pages/SendMessage.tsx
Normal file
12
src/pages/SendMessage.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import Send from "@/features/send-message/ui/Send";
|
||||
import SidebarLayout from "@/SidebarLayout";
|
||||
|
||||
const SendMessage = () => {
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<Send />
|
||||
</SidebarLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendMessage;
|
||||
@@ -8,8 +8,10 @@ export const API_URLS = {
|
||||
REGIONS: `${API_V}admin/region/`,
|
||||
DISTRICT: `${API_V}admin/district/`,
|
||||
DOCTOR: `${API_V}admin/doctor/`,
|
||||
DOCTOR_EXPORT: `${API_V}admin/doctor/export/`,
|
||||
OBJECT: `${API_V}admin/place/`,
|
||||
PHARMACIES: `${API_V}admin/pharmacy/`,
|
||||
PHARMACIES_EXPORT: `${API_V}admin/pharmacy/export/`,
|
||||
PLANS: `${API_V}admin/plan/`,
|
||||
PILL: `${API_V}admin/product/`,
|
||||
LOCATION: `${API_V}admin/location/`,
|
||||
@@ -20,4 +22,6 @@ export const API_URLS = {
|
||||
TOUR_PLAN: `${API_V}admin/tour_plan/`,
|
||||
SUPPORT: `${API_V}admin/support/list/`,
|
||||
DISTRIBUTED: `${API_V}admin/distributed_product/list/`,
|
||||
SEND_MESSAGE: `${API_V}admin/send_message/`,
|
||||
GET_ME: `${API_V}accounts/user/me/`,
|
||||
};
|
||||
|
||||
11
src/shared/config/api/user/api.ts
Normal file
11
src/shared/config/api/user/api.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { GetMeRes } from "@/shared/config/api/user/type";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const user_api = {
|
||||
async getMe(): Promise<AxiosResponse<GetMeRes>> {
|
||||
const res = httpClient.get(API_URLS.GET_ME);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
16
src/shared/config/api/user/type.ts
Normal file
16
src/shared/config/api/user/type.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export interface GetMeRes {
|
||||
status: string;
|
||||
status_code: number;
|
||||
data: GetMeResData;
|
||||
}
|
||||
|
||||
export interface GetMeResData {
|
||||
created_at: string;
|
||||
first_name: string;
|
||||
id: number;
|
||||
is_active: boolean;
|
||||
is_superuser: boolean;
|
||||
last_name: string;
|
||||
region: null | number;
|
||||
telegram_id: null | number;
|
||||
}
|
||||
15
src/shared/hooks/user.ts
Normal file
15
src/shared/hooks/user.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { GetMeResData } from "@/shared/config/api/user/type";
|
||||
import { create } from "zustand";
|
||||
|
||||
type State = {
|
||||
user: GetMeResData | null;
|
||||
};
|
||||
|
||||
type Actions = {
|
||||
addUser: (user: GetMeResData) => void;
|
||||
};
|
||||
|
||||
export const userStore = create<State & Actions>((set) => ({
|
||||
user: null,
|
||||
addUser: (user: GetMeResData | null) => set(() => ({ user })),
|
||||
}));
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
ClipboardList,
|
||||
FileText,
|
||||
Hospital,
|
||||
ListChecks,
|
||||
LogOut,
|
||||
Map,
|
||||
MapPin,
|
||||
@@ -77,11 +76,11 @@ const items = [
|
||||
url: "/dashboard/reports",
|
||||
icon: ClipboardList,
|
||||
},
|
||||
{
|
||||
title: "Tur planlar",
|
||||
url: "/dashboard/tour-plan",
|
||||
icon: ListChecks,
|
||||
},
|
||||
// {
|
||||
// title: "Tur planlar",
|
||||
// url: "/dashboard/tour-plan",
|
||||
// icon: ListChecks,
|
||||
// },
|
||||
{
|
||||
title: "Hududlar",
|
||||
url: "/dashboard/region",
|
||||
|
||||
@@ -10,4 +10,8 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
|
||||
},
|
||||
server: {
|
||||
port: 5175,
|
||||
host: true, // barcha hostlarga ruxsat
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user