Compare commits

..

15 Commits

Author SHA1 Message Date
Samandar Turgunboyev
f1498c4828 create-at added all table 2026-01-30 11:45:06 +05:00
Samandar Turgunboyev
2dd4bccfcb film detail update 2026-01-26 17:04:33 +05:00
Samandar Turgunboyev
80df127d6e film detail update 2026-01-26 16:54:06 +05:00
Samandar Turgunboyev
88dc5470d9 task update 2026-01-26 16:40:07 +05:00
Samandar Turgunboyev
30f842c53d bug fix 2025-12-13 12:50:53 +05:00
Samandar Turgunboyev
18e43f0eb8 delete super admin 2025-12-12 16:39:32 +05:00
Samandar Turgunboyev
92151f4b98 error added 2025-12-11 18:42:32 +05:00
Samandar Turgunboyev
ce0286e075 district error 2025-12-11 18:40:38 +05:00
Samandar Turgunboyev
8b156ab330 send message 2025-12-10 18:49:57 +05:00
04a2f19310 Update Dockerfile 2025-12-09 12:15:23 +00:00
d0671f38ac Update Dockerfile 2025-12-09 12:13:42 +00:00
3a2436593c Update Dockerfile 2025-12-09 12:12:20 +00:00
32519a8820 Add Dockerfile 2025-12-09 12:10:31 +00:00
4670b27e53 Update docker-compose.yaml 2025-12-09 12:10:18 +00:00
7c1662404b Add docker-compose.yaml 2025-12-09 12:07:10 +00:00
47 changed files with 844 additions and 231 deletions

View File

@@ -1 +1,2 @@
VITE_API_URL=string VITE_API_URL=string
VITE_SOCKET_URL=string

2
.gitignore vendored
View File

@@ -11,6 +11,8 @@ node_modules
dist dist
dist-ssr dist-ssr
*.local *.local
.env
.example.env
# Editor directories and files # Editor directories and files
.idea .idea

31
Dockerfile Normal file
View 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
View 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
View File

@@ -43,7 +43,8 @@
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"zod": "^4.1.13" "zod": "^4.1.13",
"zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",
@@ -6156,6 +6157,35 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "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
}
}
} }
} }
} }

View File

@@ -47,7 +47,8 @@
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.13", "tailwindcss": "^4.1.13",
"zod": "^4.1.13" "zod": "^4.1.13",
"zustand": "^5.0.9"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.25.0", "@eslint/js": "^9.25.0",

View File

@@ -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 { SidebarProvider, SidebarTrigger } from "@/shared/ui/sidebar";
import { AppSidebar } from "@/widgets/sidebar-layout"; 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 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 ( return (
<SidebarProvider> <SidebarProvider>
<AppSidebar /> <AppSidebar />

View File

@@ -62,10 +62,17 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
onError: (err: AxiosError) => { onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string }; const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message; const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", { const errMessageName = err.response?.data as { data: { name: [string] } };
richColors: true, const messageTextName = errMessageName.data.name[0];
position: "top-center", 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) => { onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string }; const errMessage = err.response?.data as { message: string };
const errMessageName = err.response?.data as { data: { name: [string] } };
const messageText = errMessage.message; 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, richColors: true,
position: "top-center", position: "top-center",
}); });
@@ -203,7 +212,7 @@ export default function AddDistrict({ initialValues, setDialogOpen }: Props) {
: "opacity-0", : "opacity-0",
)} )}
/> />
{u.first_name} {u.last_name} {u.region.name} {u.first_name} {u.last_name} {u.region?.name}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -1,4 +1,6 @@
import type { DistrictListData } from "@/features/districts/lib/data"; 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 { Button } from "@/shared/ui/button";
import { import {
Table, Table,
@@ -30,6 +32,8 @@ const TableDistrict = ({
setEditingDistrict, setEditingDistrict,
currentPage, currentPage,
}: Props) => { }: Props) => {
const { user } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{isLoading && ( {isLoading && (
@@ -55,6 +59,7 @@ const TableDistrict = ({
<TableHead>ID</TableHead> <TableHead>ID</TableHead>
<TableHead>Tuman nomi</TableHead> <TableHead>Tuman nomi</TableHead>
<TableHead>Kim qoshgan</TableHead> <TableHead>Kim qoshgan</TableHead>
<TableHead>Qo'shgan sanasi</TableHead>
<TableHead className="text-right">Harakatlar</TableHead> <TableHead className="text-right">Harakatlar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -67,6 +72,9 @@ const TableDistrict = ({
<TableCell> <TableCell>
{d.user.first_name} {d.user.last_name} {d.user.first_name} {d.user.last_name}
</TableCell> </TableCell>
<TableCell>
{formatDate.format(d.created_at, "DD-MM-YYYY")}
</TableCell>
<TableCell className="flex gap-2 justify-end"> <TableCell className="flex gap-2 justify-end">
<Button <Button
variant="outline" variant="outline"
@@ -79,15 +87,17 @@ const TableDistrict = ({
> >
<Edit className="w-4 h-4" /> <Edit className="w-4 h-4" />
</Button> </Button>
{user?.is_superuser && (
<Button <Button
variant="destructive" variant="destructive"
size="sm" size="sm"
onClick={() => handleDelete(d)} onClick={() => handleDelete(d)}
className="cursor-pointer" disabled={!user?.is_superuser}
> className="cursor-pointer"
<Trash className="w-4 h-4" /> >
</Button> <Trash className="w-4 h-4" />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -37,4 +37,11 @@ export const doctor_api = {
const res = await httpClient.delete(`${API_URLS.DOCTOR}${id}/delete/`); const res = await httpClient.delete(`${API_URLS.DOCTOR}${id}/delete/`);
return res; return res;
}, },
async export() {
const res = await httpClient.get(`${API_URLS.DOCTOR_EXPORT}`, {
responseType: "blob",
});
return res;
},
}; };

View File

@@ -455,7 +455,7 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
: "opacity-0", : "opacity-0",
)} )}
/> />
{u.first_name} {u.last_name} {u.region.name} {u.first_name} {u.last_name} {u.region?.name}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -1,3 +1,4 @@
import { doctor_api } from "@/features/doctors/lib/api";
import type { DoctorListResData } from "@/features/doctors/lib/data"; import type { DoctorListResData } from "@/features/doctors/lib/data";
import AddedDoctor from "@/features/doctors/ui/AddedDoctor"; import AddedDoctor from "@/features/doctors/ui/AddedDoctor";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
@@ -9,8 +10,11 @@ import {
DialogTrigger, DialogTrigger,
} from "@/shared/ui/dialog"; } from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input"; 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 type { Dispatch, SetStateAction } from "react";
import { toast } from "sonner";
interface Props { interface Props {
searchName: string; searchName: string;
@@ -49,6 +53,45 @@ const FilterDoctor = ({
setEditingPlan, setEditingPlan,
editingPlan, editingPlan,
}: Props) => { }: 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 ( return (
<div className="flex justify-end gap-2 w-full"> <div className="flex justify-end gap-2 w-full">
<Input <Input
@@ -87,6 +130,17 @@ const FilterDoctor = ({
onChange={(e) => setSearchUser(e.target.value)} onChange={(e) => setSearchUser(e.target.value)}
className="w-full md:w-48" 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}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button

View File

@@ -1,4 +1,6 @@
import type { DoctorListResData } from "@/features/doctors/lib/data"; 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 formatPhone from "@/shared/lib/formatPhone";
import { Badge } from "@/shared/ui/badge"; import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
@@ -36,6 +38,7 @@ const TableDoctor = ({
handleDelete, handleDelete,
isFetching, isFetching,
}: Props) => { }: Props) => {
const { user: getme } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{(isLoading || isFetching) && ( {(isLoading || isFetching) && (
@@ -66,6 +69,7 @@ const TableDoctor = ({
<TableHead>Ish joyi</TableHead> <TableHead>Ish joyi</TableHead>
<TableHead>Sohasi</TableHead> <TableHead>Sohasi</TableHead>
<TableHead>Kim qo'shgan</TableHead> <TableHead>Kim qo'shgan</TableHead>
<TableHead>Qo'shgan sanasi</TableHead>
<TableHead className="text-right">Amallar</TableHead> <TableHead className="text-right">Amallar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -89,6 +93,9 @@ const TableDoctor = ({
<TableCell> <TableCell>
{item.user.first_name} {item.user.last_name} {item.user.first_name} {item.user.last_name}
</TableCell> </TableCell>
<TableCell>
{formatDate.format(item.created_at, "DD-MM-YYYY")}
</TableCell>
<TableCell className="text-right flex gap-2 justify-end"> <TableCell className="text-right flex gap-2 justify-end">
<Button <Button
variant="outline" variant="outline"
@@ -112,14 +119,17 @@ const TableDoctor = ({
> >
<Pencil size={18} /> <Pencil size={18} />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="icon" variant="destructive"
className="cursor-pointer" size="icon"
onClick={() => handleDelete(item)} disabled={!getme?.is_superuser}
> className="cursor-pointer"
<Trash2 size={18} /> onClick={() => handleDelete(item)}
</Button> >
<Trash2 size={18} />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))

View File

@@ -1,4 +1,5 @@
import type { LocationListDataRes } from "@/features/location/lib/data"; import type { LocationListDataRes } from "@/features/location/lib/data";
import { userStore } from "@/shared/hooks/user";
import formatDate from "@/shared/lib/formatDate"; import formatDate from "@/shared/lib/formatDate";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
@@ -25,6 +26,7 @@ const LocationTable = ({
setDetailDialog, setDetailDialog,
handleDelete, handleDelete,
}: Props) => { }: Props) => {
const { user: getme } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<Table> <Table>
@@ -72,14 +74,17 @@ const LocationTable = ({
> >
<Eye size={18} /> <Eye size={18} />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="icon" variant="destructive"
className="cursor-pointer" size="icon"
onClick={() => handleDelete(item)} className="cursor-pointer"
> disabled={!getme?.is_superuser}
<Trash2 size={18} /> onClick={() => handleDelete(item)}
</Button> >
<Trash2 size={18} />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -1,4 +1,5 @@
import type { LocationListDataRes } from "@/features/location/lib/data"; import type { LocationListDataRes } from "@/features/location/lib/data";
import { userStore } from "@/shared/hooks/user";
import formatDate from "@/shared/lib/formatDate"; import formatDate from "@/shared/lib/formatDate";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
@@ -25,6 +26,7 @@ const UserLocationTable = ({
setDetailDialog, setDetailDialog,
handleDelete, handleDelete,
}: Props) => { }: Props) => {
const { user: getme } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<Table> <Table>
@@ -72,14 +74,17 @@ const UserLocationTable = ({
> >
<Eye size={18} /> <Eye size={18} />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="icon" variant="destructive"
className="cursor-pointer" size="icon"
onClick={() => handleDelete(item)} disabled={!getme?.is_superuser}
> className="cursor-pointer"
<Trash2 size={18} /> onClick={() => handleDelete(item)}
</Button> >
<Trash2 size={18} />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -332,7 +332,7 @@ export default function AddedObject({ initialValues, setDialogOpen }: Props) {
: "opacity-0", : "opacity-0",
)} )}
/> />
{u.first_name} {u.last_name} {u.region.name} {u.first_name} {u.last_name} {u.region?.name}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -1,4 +1,6 @@
import type { ObjectListData } from "@/features/objects/lib/data"; 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 { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
@@ -33,6 +35,7 @@ const ObjectTable = ({
isError, isError,
isLoading, isLoading,
}: Props) => { }: Props) => {
const { user: getme } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{isLoading && ( {isLoading && (
@@ -58,6 +61,7 @@ const ObjectTable = ({
<TableHead>Obyekt nomi</TableHead> <TableHead>Obyekt nomi</TableHead>
<TableHead>Tuman</TableHead> <TableHead>Tuman</TableHead>
<TableHead>Foydalanuvchi</TableHead> <TableHead>Foydalanuvchi</TableHead>
<TableHead>Qo'shilgan sanasi</TableHead>
<TableHead className="text-right">Amallar</TableHead> <TableHead className="text-right">Amallar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -73,6 +77,9 @@ const ObjectTable = ({
<TableCell> <TableCell>
{item.user.first_name} {item.user.last_name} {item.user.first_name} {item.user.last_name}
</TableCell> </TableCell>
<TableCell>
{formatDate.format(item.created_at, "DD-MM-YYYY")}
</TableCell>
<TableCell className="text-right flex gap-2 justify-end"> <TableCell className="text-right flex gap-2 justify-end">
<Button <Button
variant="outline" variant="outline"
@@ -96,14 +103,17 @@ const ObjectTable = ({
> >
<Pencil size={18} /> <Pencil size={18} />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="icon" variant="destructive"
className="cursor-pointer" size="icon"
onClick={() => handleDelete(item)} className="cursor-pointer"
> disabled={!getme?.is_superuser}
<Trash2 size={18} /> onClick={() => handleDelete(item)}
</Button> >
<Trash2 size={18} />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))

View File

@@ -5,6 +5,8 @@ import {
} from "@/features/pharm/lib/data"; } from "@/features/pharm/lib/data";
import AddedPharm from "@/features/pharm/ui/AddedPharm"; import AddedPharm from "@/features/pharm/ui/AddedPharm";
import DeletePharm from "@/features/pharm/ui/DeletePharm"; 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 { Button } from "@/shared/ui/button";
import { import {
Dialog, Dialog,
@@ -28,6 +30,7 @@ import { Edit, Loader2, Plus, Trash } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
const PharmList = () => { const PharmList = () => {
const { user: getme } = userStore();
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [nameFilter, setNameFilter] = useState<string>(""); const [nameFilter, setNameFilter] = useState<string>("");
const limit = 20; const limit = 20;
@@ -119,6 +122,7 @@ const PharmList = () => {
<TableRow className="text-center"> <TableRow className="text-center">
<TableHead className="text-start">ID</TableHead> <TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Nomi</TableHead> <TableHead className="text-start">Nomi</TableHead>
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
<TableHead className="text-end">Amallar</TableHead> <TableHead className="text-end">Amallar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -128,6 +132,9 @@ const PharmList = () => {
<TableRow key={plan.id} className="text-start"> <TableRow key={plan.id} className="text-start">
<TableCell>{plan.id}</TableCell> <TableCell>{plan.id}</TableCell>
<TableCell>{plan.name}</TableCell> <TableCell>{plan.name}</TableCell>
<TableCell>
{formatDate.format(plan.created_at, "DD-MM-YYYY")}
</TableCell>
<TableCell className="flex gap-2 justify-end"> <TableCell className="flex gap-2 justify-end">
<Button <Button
variant="outline" variant="outline"
@@ -140,14 +147,17 @@ const PharmList = () => {
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="sm" variant="destructive"
className="cursor-pointer" size="sm"
onClick={() => handleDelete(plan)} disabled={!getme.is_superuser}
> className="cursor-pointer"
<Trash className="h-4 w-4" /> onClick={() => handleDelete(plan)}
</Button> >
<Trash className="h-4 w-4" />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))

View File

@@ -38,4 +38,11 @@ export const pharmacies_api = {
const res = await httpClient.delete(`${API_URLS.PHARMACIES}${id}/delete/`); const res = await httpClient.delete(`${API_URLS.PHARMACIES}${id}/delete/`);
return res; return res;
}, },
async export() {
const res = await httpClient.get(`${API_URLS.PHARMACIES_EXPORT}`, {
responseType: "blob",
});
return res;
},
}; };

View File

@@ -123,9 +123,9 @@ export interface PharmaciesListData {
export interface CreatePharmaciesReq { export interface CreatePharmaciesReq {
name: string; name: string;
inn: string; inn?: string;
owner_phone: string; owner_phone: string;
responsible_phone: string; responsible_phone?: string;
district_id: number; district_id: number;
place_id: number; place_id: number;
user_id: number; user_id: number;
@@ -136,9 +136,9 @@ export interface CreatePharmaciesReq {
export interface UpdatePharmaciesReq { export interface UpdatePharmaciesReq {
name: string; name: string;
inn: string; inn?: string;
owner_phone: string; owner_phone: string;
responsible_phone: string; responsible_phone?: string;
longitude: number; longitude: number;
latitude: number; latitude: number;
extra_location: { longitude: number; latitude: number }; extra_location: { longitude: number; latitude: number };

View File

@@ -2,9 +2,9 @@ import z from "zod";
export const PharmForm = z.object({ export const PharmForm = z.object({
name: z.string().min(1, { message: "Majburiy maydon" }), 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" }), 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" }), district: z.string().min(1, { message: "Majburiy maydon" }),
user: z.string().min(1, { message: "Majburiy maydon" }), user: z.string().min(1, { message: "Majburiy maydon" }),
object: z.string().min(1, { message: "Majburiy maydon" }), object: z.string().min(1, { message: "Majburiy maydon" }),

View File

@@ -249,36 +249,31 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
}); });
function onSubmit(values: z.infer<typeof PharmForm>) { 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 bolsa yuboriladi
};
if (initialValues) { if (initialValues) {
edit({ edit({
id: initialValues.id, id: initialValues.id,
body: { body: baseBody,
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),
},
}); });
} else { } else {
mutate({ mutate({
...baseBody,
district_id: Number(values.district), 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), place_id: Number(values.object),
responsible_phone: onlyNumber(values.additional_phone),
user_id: Number(values.user), user_id: Number(values.user),
}); });
} }
@@ -343,7 +338,7 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
<Input <Input
placeholder="+998 90 123-45-67" placeholder="+998 90 123-45-67"
{...field} {...field}
value={formatPhone(field.value)} value={field.value && formatPhone(field.value)}
/> />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
@@ -419,7 +414,7 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
: "opacity-0", : "opacity-0",
)} )}
/> />
{u.first_name} {u.last_name} {u.region.name} {u.first_name} {u.last_name} {u.region?.name}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -1,3 +1,4 @@
import { pharmacies_api } from "@/features/pharmacies/lib/api";
import type { PharmaciesListData } from "@/features/pharmacies/lib/data"; import type { PharmaciesListData } from "@/features/pharmacies/lib/data";
import AddedPharmacies from "@/features/pharmacies/ui/AddedPharmacies"; import AddedPharmacies from "@/features/pharmacies/ui/AddedPharmacies";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
@@ -9,8 +10,11 @@ import {
DialogTrigger, DialogTrigger,
} from "@/shared/ui/dialog"; } from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input"; 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 type { Dispatch, SetStateAction } from "react";
import { toast } from "sonner";
interface Props { interface Props {
searchName: string; searchName: string;
@@ -41,6 +45,45 @@ const PharmaciesFilter = ({
setEditingPlan, setEditingPlan,
editingPlan, editingPlan,
}: Props) => { }: 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 ( return (
<div className="flex justify-end gap-2 w-full"> <div className="flex justify-end gap-2 w-full">
<Input <Input
@@ -67,6 +110,15 @@ const PharmaciesFilter = ({
onChange={(e) => setSearchUser(e.target.value)} onChange={(e) => setSearchUser(e.target.value)}
className="w-full md:w-48" 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}> <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button

View File

@@ -1,4 +1,6 @@
import type { PharmaciesListData } from "@/features/pharmacies/lib/data"; 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 formatPhone from "@/shared/lib/formatPhone";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
@@ -29,6 +31,7 @@ const PharmaciesTable = ({
setDialogOpen, setDialogOpen,
handleDelete, handleDelete,
}: Props) => { }: Props) => {
const { user: getme } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
<Table> <Table>
@@ -42,6 +45,7 @@ const PharmaciesTable = ({
<TableHead>Tuman</TableHead> <TableHead>Tuman</TableHead>
<TableHead>Obyekt</TableHead> <TableHead>Obyekt</TableHead>
<TableHead>Kim qo'shgan</TableHead> <TableHead>Kim qo'shgan</TableHead>
<TableHead>Qo'shgan sanasi</TableHead>
<TableHead className="text-right">Amallar</TableHead> <TableHead className="text-right">Amallar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -59,6 +63,10 @@ const PharmaciesTable = ({
{item.user.first_name} {item.user.last_name} {item.user.first_name} {item.user.last_name}
</TableCell> </TableCell>
<TableCell>
{formatDate.format(item.created_at, "DD-MM-YYYY")}
</TableCell>
<TableCell className="text-right flex gap-2 justify-end"> <TableCell className="text-right flex gap-2 justify-end">
<Button <Button
variant="outline" variant="outline"
@@ -82,14 +90,17 @@ const PharmaciesTable = ({
> >
<Pencil size={18} /> <Pencil size={18} />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="icon" variant="destructive"
className="cursor-pointer" size="icon"
onClick={() => handleDelete(item)} disabled={!getme?.is_superuser}
> className="cursor-pointer"
<Trash2 size={18} /> onClick={() => handleDelete(item)}
</Button> >
<Trash2 size={18} />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -2,6 +2,8 @@ import { pill_api } from "@/features/pill/lib/api";
import { type PillListData, type PillType } from "@/features/pill/lib/data"; import { type PillListData, type PillType } from "@/features/pill/lib/data";
import AddedPill from "@/features/pill/ui/AddedPill"; import AddedPill from "@/features/pill/ui/AddedPill";
import DeletePill from "@/features/pill/ui/DeletePill"; 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 formatPrice from "@/shared/lib/formatPrice";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
@@ -26,6 +28,7 @@ import { Edit, Plus, Trash } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
const PillList = () => { const PillList = () => {
const { user: getme } = userStore();
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const limit = 20; const limit = 20;
const [nameFilter, setNameFilter] = useState<string>(""); const [nameFilter, setNameFilter] = useState<string>("");
@@ -102,6 +105,7 @@ const PillList = () => {
<TableHead className="text-start">ID</TableHead> <TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Nomi</TableHead> <TableHead className="text-start">Nomi</TableHead>
<TableHead className="text-start">Narxi</TableHead> <TableHead className="text-start">Narxi</TableHead>
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
<TableHead className="text-end">Amallar</TableHead> <TableHead className="text-end">Amallar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -111,6 +115,9 @@ const PillList = () => {
<TableCell>{plan.id}</TableCell> <TableCell>{plan.id}</TableCell>
<TableCell>{plan.name}</TableCell> <TableCell>{plan.name}</TableCell>
<TableCell>{formatPrice(plan.price)}</TableCell> <TableCell>{formatPrice(plan.price)}</TableCell>
<TableCell>
{formatDate.format(plan.created_at, "Dd-MM-YYYY")}
</TableCell>
<TableCell className="flex gap-2 justify-end"> <TableCell className="flex gap-2 justify-end">
<Button <Button
variant="outline" variant="outline"
@@ -123,14 +130,17 @@ const PillList = () => {
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="sm" variant="destructive"
className="cursor-pointer" size="sm"
onClick={() => handleDelete(plan)} className="cursor-pointer"
> onClick={() => handleDelete(plan)}
<Trash className="h-4 w-4" /> disabled={!getme.is_superuser}
</Button> >
<Trash className="h-4 w-4" />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -51,7 +51,7 @@ export interface PlanListData {
} }
export interface PlanCreateReq { export interface PlanCreateReq {
title: string; title?: string;
description: string; description: string;
date: string; date: string;
user_id: number; user_id: number;
@@ -63,7 +63,7 @@ export interface PlanCreateReq {
} }
export interface PlanUpdateReq { export interface PlanUpdateReq {
title: string; title?: string;
description: string; description: string;
date: string; date: string;
longitude: number; longitude: number;

View File

@@ -1,7 +1,7 @@
import z from "zod"; import z from "zod";
export const createPlanFormData = z.object({ 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" }), 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" }),

View File

@@ -49,7 +49,7 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
const form = useForm<z.infer<typeof createPlanFormData>>({ const form = useForm<z.infer<typeof createPlanFormData>>({
resolver: zodResolver(createPlanFormData), resolver: zodResolver(createPlanFormData),
defaultValues: { defaultValues: {
name: initialValues?.title || "", // name: initialValues?.title || "",
description: initialValues?.description || "", description: initialValues?.description || "",
user: initialValues ? String(initialValues.user.id) : "", user: initialValues ? String(initialValues.user.id) : "",
date: initialValues ? initialValues?.date : "", date: initialValues ? initialValues?.date : "",
@@ -174,13 +174,15 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
body: { body: {
date: formatDate.format(data.date, "YYYY-MM-DD"), date: formatDate.format(data.date, "YYYY-MM-DD"),
description: data.description, description: data.description,
...(data.name && {
title: data.name,
}),
extra_location: { extra_location: {
latitude: initialValues.latitude, latitude: initialValues.latitude,
longitude: initialValues.longitude, longitude: initialValues.longitude,
}, },
latitude: initialValues.latitude, latitude: initialValues.latitude,
longitude: initialValues.longitude, longitude: initialValues.longitude,
title: data.name,
}, },
id: initialValues.id, id: initialValues.id,
}); });
@@ -194,7 +196,9 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
}, },
latitude: lat, latitude: lat,
longitude: long, longitude: long,
title: data.name, ...(data.name && {
title: data.name,
}),
doctor_id: data.doctor_id ? Number(data.doctor_id) : null, doctor_id: data.doctor_id ? Number(data.doctor_id) : null,
pharmacy_id: data.pharmacy_id ? Number(data.pharmacy_id) : null, pharmacy_id: data.pharmacy_id ? Number(data.pharmacy_id) : null,
user_id: Number(data.user), user_id: Number(data.user),
@@ -274,7 +278,7 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
: "opacity-0", : "opacity-0",
)} )}
/> />
{u.first_name} {u.last_name} {u.region.name} {u.first_name} {u.last_name} {u.region?.name}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -1,4 +1,6 @@
import type { PlanListData } from "@/features/plans/lib/data"; 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 { Button } from "@/shared/ui/button";
import { import {
Table, Table,
@@ -33,6 +35,7 @@ const PalanTable = ({
setDialogOpen, setDialogOpen,
handleDelete, handleDelete,
}: Props) => { }: Props) => {
const { user: getme } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{(isLoading || isFetching) && ( {(isLoading || isFetching) && (
@@ -58,6 +61,8 @@ const PalanTable = ({
<TableHead className="text-start">ID</TableHead> <TableHead className="text-start">ID</TableHead>
<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">Qo'shilgan sanasi</TableHead>
<TableHead className="text-start">Bajarilish sanasi</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">Shifokor biriktirgan</TableHead>
<TableHead className="text-start"> <TableHead className="text-start">
@@ -73,6 +78,12 @@ const PalanTable = ({
<TableCell>{plan.id}</TableCell> <TableCell>{plan.id}</TableCell>
<TableCell>{plan.title}</TableCell> <TableCell>{plan.title}</TableCell>
<TableCell>{plan.description}</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> <TableCell>
{plan.user.first_name + " " + plan.user.last_name} {plan.user.first_name + " " + plan.user.last_name}
</TableCell> </TableCell>
@@ -115,15 +126,19 @@ const PalanTable = ({
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="sm" variant="destructive"
className="cursor-pointer" size="sm"
disabled={plan.comment ? true : false} className="cursor-pointer"
onClick={() => handleDelete(plan)} disabled={
> !getme?.is_superuser || plan.comment ? true : false
<Trash className="h-4 w-4" /> }
</Button> onClick={() => handleDelete(plan)}
>
<Trash className="h-4 w-4" />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -1,4 +1,6 @@
import type { RegionListResData } from "@/features/region/lib/data"; 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 { Button } from "@/shared/ui/button";
import { import {
Table, Table,
@@ -26,6 +28,7 @@ const RegionTable = ({
setDialogOpen: Dispatch<SetStateAction<boolean>>; setDialogOpen: Dispatch<SetStateAction<boolean>>;
handleDelete: (user: RegionListResData) => void; handleDelete: (user: RegionListResData) => void;
}) => { }) => {
const { user: getme } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{isLoading && ( {isLoading && (
@@ -49,6 +52,7 @@ const RegionTable = ({
<TableRow className="text-center"> <TableRow className="text-center">
<TableHead className="text-start">ID</TableHead> <TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Nomi</TableHead> <TableHead className="text-start">Nomi</TableHead>
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -56,6 +60,9 @@ const RegionTable = ({
<TableRow key={plan.id} className="text-start"> <TableRow key={plan.id} className="text-start">
<TableCell>{index + 1}</TableCell> <TableCell>{index + 1}</TableCell>
<TableCell>{plan.name}</TableCell> <TableCell>{plan.name}</TableCell>
<TableCell>
{formatDate.format(plan.created_at, "DD-MM-YYYY")}
</TableCell>
<TableCell className="flex gap-2 justify-end"> <TableCell className="flex gap-2 justify-end">
<Button <Button
variant="outline" variant="outline"
@@ -68,14 +75,17 @@ const RegionTable = ({
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="sm" variant="destructive"
className="cursor-pointer" size="sm"
onClick={() => handleDelete(plan)} disabled={!getme?.is_superuser}
> className="cursor-pointer"
<Trash className="h-4 w-4" /> onClick={() => handleDelete(plan)}
</Button> >
<Trash className="h-4 w-4" />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View 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;

View File

@@ -420,7 +420,7 @@ export const AddedSpecification = ({ initialValues, setDialogOpen }: Props) => {
: "opacity-0", : "opacity-0",
)} )}
/> />
{u.first_name} {u.last_name} {u.region.name} {u.first_name} {u.last_name} {u.region?.name}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -5,6 +5,7 @@ import { type OrderListDataRes } from "@/features/specifications/lib/data";
import { AddedSpecification } from "@/features/specifications/ui/AddedSpecification"; import { AddedSpecification } from "@/features/specifications/ui/AddedSpecification";
import DeleteOrder from "@/features/specifications/ui/DeleteOrder"; import DeleteOrder from "@/features/specifications/ui/DeleteOrder";
import { SpecificationDetail } from "@/features/specifications/ui/SpecificationDetail "; import { SpecificationDetail } from "@/features/specifications/ui/SpecificationDetail ";
import { userStore } from "@/shared/hooks/user";
import formatPrice from "@/shared/lib/formatPrice"; import formatPrice from "@/shared/lib/formatPrice";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
@@ -34,6 +35,7 @@ const SpecificationsList = () => {
const [dialogOpen, setDialogOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const limit = 20; const limit = 20;
const { user: getme } = userStore();
const { const {
data: order, data: order,
@@ -154,14 +156,17 @@ const SpecificationsList = () => {
> >
<Pencil size={18} /> <Pencil size={18} />
</Button> </Button>
<Button {getme?.is_superuser && (
size="icon" <Button
variant="destructive" size="icon"
className="cursor-pointer" disabled={!getme?.is_superuser}
onClick={() => handleDelete(item)} variant="destructive"
> className="cursor-pointer"
<Trash2 size={18} /> onClick={() => handleDelete(item)}
</Button> >
<Trash2 size={18} />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}

View File

@@ -202,7 +202,7 @@ const AddedTourPlan = ({ initialValues, setDialogOpen }: Props) => {
: "opacity-0", : "opacity-0",
)} )}
/> />
{u.first_name} {u.last_name} {u.region.name} {u.first_name} {u.last_name} {u.region?.name}
</CommandItem> </CommandItem>
))} ))}
</CommandGroup> </CommandGroup>

View File

@@ -1,4 +1,5 @@
import type { PlanTourListDataRes } from "@/features/tour-plan/lib/data"; import type { PlanTourListDataRes } from "@/features/tour-plan/lib/data";
import { userStore } from "@/shared/hooks/user";
import formatDate from "@/shared/lib/formatDate"; import formatDate from "@/shared/lib/formatDate";
import { Button } from "@/shared/ui/button"; import { Button } from "@/shared/ui/button";
import { import {
@@ -36,6 +37,7 @@ const TourPlanTable = ({
setDialogOpen, setDialogOpen,
setDetailOpen, setDetailOpen,
}: Props) => { }: Props) => {
const { user: getme } = userStore();
return ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{(isLoading || isFetching) && ( {(isLoading || isFetching) && (
@@ -107,14 +109,17 @@ const TourPlanTable = ({
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="icon" variant="destructive"
className="cursor-pointer" size="icon"
onClick={() => handleDelete(plan)} disabled={!getme?.is_superuser}
> className="cursor-pointer"
<Trash className="h-4 w-4" /> onClick={() => handleDelete(plan)}
</Button> >
<Trash className="h-4 w-4" />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))

View File

@@ -47,3 +47,10 @@ export const user_api = {
return res; 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;
},
};

View File

@@ -65,11 +65,11 @@ export interface UserListRes {
export interface UserListData { export interface UserListData {
id: number; id: number;
first_name: string; first_name: string | null;
last_name: string; last_name: string | null;
region: { region: {
id: number; id: number;
name: string; name: string | null;
}; };
is_active: boolean; is_active: boolean;
telegram_id: string; telegram_id: string;

View File

@@ -1,5 +1,6 @@
import { region_api } from "@/features/region/lib/api"; import { region_api } from "@/features/region/lib/api";
import type { RegionListResData } from "@/features/region/lib/data"; 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 type { UserListData } from "@/features/users/lib/data";
import AddUsers from "@/features/users/ui/AddUsers"; import AddUsers from "@/features/users/ui/AddUsers";
import { cn } from "@/shared/lib/utils"; import { cn } from "@/shared/lib/utils";
@@ -14,12 +15,21 @@ import {
} from "@/shared/ui/command"; } from "@/shared/ui/command";
import { import {
Dialog, Dialog,
DialogClose,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
DialogTrigger, DialogTrigger,
} from "@/shared/ui/dialog"; } from "@/shared/ui/dialog";
import {
Form,
FormControl,
FormField,
FormItem,
FormMessage,
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input"; import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { import {
Select, Select,
@@ -28,9 +38,28 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@/shared/ui/select"; } from "@/shared/ui/select";
import { useQuery } from "@tanstack/react-query"; import { Textarea } from "@/shared/ui/textarea";
import { Check, ChevronsUpDown, Loader2, Plus } from "lucide-react"; 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 { 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 { interface Props {
searchTerm: string; searchTerm: string;
@@ -43,6 +72,10 @@ interface Props {
setDialogOpen: Dispatch<SetStateAction<boolean>>; setDialogOpen: Dispatch<SetStateAction<boolean>>;
editingUser: UserListData | null; editingUser: UserListData | null;
setEditingUser: Dispatch<SetStateAction<UserListData | null>>; setEditingUser: Dispatch<SetStateAction<UserListData | null>>;
setSendMessage: Dispatch<SetStateAction<boolean>>;
setUserList: Dispatch<SetStateAction<number[] | []>>;
sendMessage: boolean;
userList: number[] | null;
} }
const Filter = ({ const Filter = ({
@@ -51,14 +84,19 @@ const Filter = ({
statusFilter, statusFilter,
setStatusFilter, setStatusFilter,
regionValue, regionValue,
sendMessage,
setRegionValue, setRegionValue,
userList,
dialogOpen, dialogOpen,
setUserList,
setDialogOpen, setDialogOpen,
editingUser, editingUser,
setEditingUser, setEditingUser,
setSendMessage,
}: Props) => { }: Props) => {
const [openRegion, setOpenRegion] = useState(false); const [openRegion, setOpenRegion] = useState(false);
const [regionSearch, setRegionSearch] = useState(""); const [regionSearch, setRegionSearch] = useState("");
const [message, setMessage] = useState<boolean>(false);
const { data: regions, isLoading } = useQuery({ const { data: regions, isLoading } = useQuery({
queryKey: ["region_list", regionSearch], queryKey: ["region_list", regionSearch],
@@ -66,6 +104,43 @@ const Filter = ({
select: (res) => res.data.data, 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 ( return (
<div className="flex flex-wrap gap-2 items-center"> <div className="flex flex-wrap gap-2 items-center">
{/* Search input */} {/* Search input */}
@@ -179,6 +254,7 @@ const Filter = ({
variant="default" variant="default"
className="bg-blue-500 cursor-pointer hover:bg-blue-500" className="bg-blue-500 cursor-pointer hover:bg-blue-500"
onClick={() => setEditingUser(null)} onClick={() => setEditingUser(null)}
disabled={sendMessage}
> >
<Plus className="!h-5 !w-5" /> Qo'shish <Plus className="!h-5 !w-5" /> Qo'shish
</Button> </Button>
@@ -195,6 +271,102 @@ const Filter = ({
<AddUsers initialData={editingUser} setDialogOpen={setDialogOpen} /> <AddUsers initialData={editingUser} setDialogOpen={setDialogOpen} />
</DialogContent> </DialogContent>
</Dialog> </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> </div>
); );
}; };

View File

@@ -1,6 +1,9 @@
import { user_api } from "@/features/users/lib/api"; import { user_api } from "@/features/users/lib/api";
import type { UserListData, UserListRes } from "@/features/users/lib/data"; 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 { Button } from "@/shared/ui/button";
import { Checkbox } from "@/shared/ui/checkbox";
import { import {
Table, Table,
TableBody, TableBody,
@@ -22,8 +25,11 @@ interface Props {
isError: boolean; isError: boolean;
setDialogOpen: Dispatch<SetStateAction<boolean>>; setDialogOpen: Dispatch<SetStateAction<boolean>>;
setEditingUser: Dispatch<SetStateAction<UserListData | null>>; setEditingUser: Dispatch<SetStateAction<UserListData | null>>;
setUserList: Dispatch<SetStateAction<number[] | []>>;
handleDelete: (user: UserListData) => void; handleDelete: (user: UserListData) => void;
currentPage: number; currentPage: number;
userList: number[] | null;
sendMessage: boolean;
} }
const UserTable = ({ const UserTable = ({
@@ -32,18 +38,24 @@ const UserTable = ({
isError, isError,
setDialogOpen, setDialogOpen,
handleDelete, handleDelete,
userList,
setEditingUser, setEditingUser,
setUserList,
sendMessage,
currentPage, currentPage,
}: Props) => { }: Props) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { user: getme } = userStore();
const [pendingUserId, setPendingUserId] = useState<number | null>(null); 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({ const { mutate: active } = useMutation({
mutationFn: (id: number) => user_api.active(id), mutationFn: (id: number) => user_api.active(id),
onMutate: (id) => { onMutate: (id) => setPendingUserId(id),
setPendingUserId(id);
},
onSuccess: () => { onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["user_list"] }); queryClient.refetchQueries({ queryKey: ["user_list"] });
toast.success(`Foydalanuvchi aktivlashdi`); toast.success(`Foydalanuvchi aktivlashdi`);
@@ -51,29 +63,50 @@ const UserTable = ({
}, },
onError: (err: AxiosError) => { onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string }; const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
setPendingUserId(null); setPendingUserId(null);
toast.error(messageText || "Xatolik yuz berdi", { toast.error(errMessage?.message || "Xatolik yuz berdi", {
richColors: true, richColors: true,
position: "top-center", position: "top-center",
}); });
}, },
}); });
const handleActivate = (userId: number) => { const handleActivate = (userId: number) => active(userId);
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 ( return (
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{isLoading && ( {isLoading && (
<div className="h-full flex items-center justify-center bg-white/70 z-10"> <div className="h-full flex items-center justify-center bg-white/70 z-10">
<span className="text-lg font-medium"> <Loader2 className="animate-spin" />
<Loader2 className="animate-spin" />
</span>
</div> </div>
)} )}
{isError && ( {isError && (
<div className="h-full flex items-center justify-center z-10"> <div className="h-full flex items-center justify-center z-10">
<span className="text-lg font-medium text-red-600"> <span className="text-lg font-medium text-red-600">
@@ -81,78 +114,65 @@ const UserTable = ({
</span> </span>
</div> </div>
)} )}
{!isLoading && !isError && ( {!isLoading && !isError && (
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow className="text-[16px] text-center"> <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">ID</TableHead>
<TableHead className="text-start">Ismi</TableHead> <TableHead className="text-start">Ismi</TableHead>
<TableHead className="text-start">Familiyasi</TableHead> <TableHead className="text-start">Familiyasi</TableHead>
<TableHead className="text-start">Hududi</TableHead> <TableHead className="text-start">Hududi</TableHead>
<TableHead className="text-start">Qo'shilgan sanasi</TableHead>
<TableHead className="text-center">Holati</TableHead> <TableHead className="text-center">Holati</TableHead>
<TableHead className="text-right">Harakatlar</TableHead> <TableHead className="text-right">Harakatlar</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{data && data.data.data.results.length > 0 ? ( {data && data.data.data.results.length > 0 ? (
data.data.data.results.map((user, index) => ( data.data.data.results.map((user, index) => (
<TableRow key={user.id} className="text-[14px] text-start"> <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>{index + 1 + (currentPage - 1) * 20}</TableCell>
<TableCell>{user.first_name}</TableCell> <TableCell>
<TableCell>{user.last_name}</TableCell> {user.first_name ? user.first_name : "No'malum"}
<TableCell>{user.region.name}</TableCell> </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"> <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 <Button
className={clsx( className={clsx(
"mx-auto cursor-pointer", "mx-auto cursor-pointer",
user.is_active user.is_active
? "bg-green-500 hover:bg-green-500" ? "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)} onClick={() => handleActivate(user.id)}
> >
{pendingUserId === user.id ? ( {pendingUserId === user.id ? (
@@ -170,24 +190,28 @@ const UserTable = ({
setEditingUser(user); setEditingUser(user);
setDialogOpen(true); setDialogOpen(true);
}} }}
disabled={sendMessage}
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"
> >
<Edit className="h-4 w-4" /> <Edit className="h-4 w-4" />
</Button> </Button>
<Button {getme?.is_superuser && (
variant="destructive" <Button
size="sm" variant="destructive"
onClick={() => handleDelete(user)} size="sm"
className="cursor-pointer" disabled={!getme?.is_superuser || sendMessage}
> onClick={() => handleDelete(user)}
<Trash className="h-4 w-4" /> className="cursor-pointer"
</Button> >
<Trash className="h-4 w-4" />
</Button>
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={6} className="text-center py-4 text-lg"> <TableCell colSpan={7} className="text-center py-4 text-lg">
Foydalanuvchilar topilmadi. Foydalanuvchilar topilmadi.
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@@ -21,6 +21,8 @@ const UsersList = () => {
const [statusFilter, setStatusFilter] = useState<"all" | "true" | "false">( const [statusFilter, setStatusFilter] = useState<"all" | "true" | "false">(
"all", "all",
); );
const [sendMessage, setSendMessage] = useState<boolean>(false);
const [userList, setUserList] = useState<number[] | []>([]);
const [userDelete, setUserDelete] = useState<UserListData | null>(null); const [userDelete, setUserDelete] = useState<UserListData | null>(null);
const [opneDelete, setOpenDelete] = useState<boolean>(false); const [opneDelete, setOpenDelete] = useState<boolean>(false);
@@ -72,12 +74,16 @@ const UsersList = () => {
setSearchTerm={setSearchTerm} setSearchTerm={setSearchTerm}
setStatusFilter={setStatusFilter} setStatusFilter={setStatusFilter}
statusFilter={statusFilter} statusFilter={statusFilter}
sendMessage={sendMessage}
setUserList={setUserList}
regionValue={regionValue} regionValue={regionValue}
setSendMessage={setSendMessage}
setRegionValue={setRegionValue} setRegionValue={setRegionValue}
dialogOpen={dialogOpen} dialogOpen={dialogOpen}
setDialogOpen={setDialogOpen} setDialogOpen={setDialogOpen}
editingUser={editingUser} editingUser={editingUser}
setEditingUser={setEditingUser} setEditingUser={setEditingUser}
userList={userList}
/> />
</div> </div>
@@ -89,6 +95,9 @@ const UsersList = () => {
setDialogOpen={setDialogOpen} setDialogOpen={setDialogOpen}
handleDelete={handleDelete} handleDelete={handleDelete}
currentPage={currentPage} currentPage={currentPage}
setUserList={setUserList}
userList={userList}
sendMessage={sendMessage}
/> />
<Pagination <Pagination
currentPage={currentPage} currentPage={currentPage}

12
src/pages/SendMessage.tsx Normal file
View 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;

View File

@@ -8,8 +8,10 @@ export const API_URLS = {
REGIONS: `${API_V}admin/region/`, REGIONS: `${API_V}admin/region/`,
DISTRICT: `${API_V}admin/district/`, DISTRICT: `${API_V}admin/district/`,
DOCTOR: `${API_V}admin/doctor/`, DOCTOR: `${API_V}admin/doctor/`,
DOCTOR_EXPORT: `${API_V}admin/doctor/export/`,
OBJECT: `${API_V}admin/place/`, OBJECT: `${API_V}admin/place/`,
PHARMACIES: `${API_V}admin/pharmacy/`, PHARMACIES: `${API_V}admin/pharmacy/`,
PHARMACIES_EXPORT: `${API_V}admin/pharmacy/export/`,
PLANS: `${API_V}admin/plan/`, PLANS: `${API_V}admin/plan/`,
PILL: `${API_V}admin/product/`, PILL: `${API_V}admin/product/`,
LOCATION: `${API_V}admin/location/`, LOCATION: `${API_V}admin/location/`,
@@ -20,4 +22,6 @@ export const API_URLS = {
TOUR_PLAN: `${API_V}admin/tour_plan/`, TOUR_PLAN: `${API_V}admin/tour_plan/`,
SUPPORT: `${API_V}admin/support/list/`, SUPPORT: `${API_V}admin/support/list/`,
DISTRIBUTED: `${API_V}admin/distributed_product/list/`, DISTRIBUTED: `${API_V}admin/distributed_product/list/`,
SEND_MESSAGE: `${API_V}admin/send_message/`,
GET_ME: `${API_V}accounts/user/me/`,
}; };

View 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;
},
};

View 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
View 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 })),
}));

View File

@@ -6,7 +6,6 @@ import {
ClipboardList, ClipboardList,
FileText, FileText,
Hospital, Hospital,
ListChecks,
LogOut, LogOut,
Map, Map,
MapPin, MapPin,
@@ -77,11 +76,11 @@ const items = [
url: "/dashboard/reports", url: "/dashboard/reports",
icon: ClipboardList, icon: ClipboardList,
}, },
{ // {
title: "Tur planlar", // title: "Tur planlar",
url: "/dashboard/tour-plan", // url: "/dashboard/tour-plan",
icon: ListChecks, // icon: ListChecks,
}, // },
{ {
title: "Hududlar", title: "Hududlar",
url: "/dashboard/region", url: "/dashboard/region",

View File

@@ -10,4 +10,8 @@ export default defineConfig({
resolve: { resolve: {
alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }], alias: [{ find: "@", replacement: path.resolve(__dirname, "src") }],
}, },
server: {
port: 5175,
host: true, // barcha hostlarga ruxsat
},
}); });