From 9bc4c3df1f0d02ed57ddba6550776c2088d2cd34 Mon Sep 17 00:00:00 2001 From: Samandar Turgunboyev Date: Sat, 29 Nov 2025 19:19:40 +0500 Subject: [PATCH] doctor and pharmacies crud --- src/features/districts/lib/api.ts | 1 + src/features/districts/ui/DistrictsList.tsx | 4 +- .../districts/ui/PaginationDistrict.tsx | 57 -- src/features/doctors/lib/api.ts | 21 +- src/features/doctors/lib/data.ts | 29 + src/features/doctors/ui/AddedDoctor.tsx | 700 ++++++++++++++--- src/features/doctors/ui/DeleteDoctor.tsx | 90 +++ .../doctors/ui/DoctorDetailDialog.tsx | 10 +- src/features/doctors/ui/DoctorsList.tsx | 36 +- src/features/doctors/ui/FilterDoctor.tsx | 6 +- src/features/doctors/ui/PaginationDoctor.tsx | 57 -- src/features/doctors/ui/TableDoctor.tsx | 12 +- src/features/location/ui/LocationFilter.tsx | 74 ++ src/features/location/ui/LocationList.tsx | 189 +---- src/features/location/ui/LocationTable.tsx | 92 +++ src/features/objects/lib/api.ts | 36 + src/features/objects/lib/data.ts | 49 ++ src/features/objects/ui/AddedObject.tsx | 534 ++++++++++--- src/features/objects/ui/DeleteObject.tsx | 89 +++ src/features/objects/ui/ObjectDetail.tsx | 127 +++- src/features/objects/ui/ObjectFilter.tsx | 88 +++ src/features/objects/ui/ObjectList.tsx | 279 ++----- src/features/objects/ui/ObjectTable.tsx | 124 +++ src/features/pharmacies/lib/api.ts | 37 + src/features/pharmacies/lib/data.ts | 63 ++ .../pharmacies/ui/AddedPharmacies.tsx | 716 ++++++++++++++---- .../pharmacies/ui/DeletePharmacies.tsx | 89 +++ .../pharmacies/ui/PharmDetailDialog.tsx | 145 +++- .../pharmacies/ui/PharmaciesFilter.tsx | 98 +++ src/features/pharmacies/ui/PharmaciesList.tsx | 292 ++----- .../pharmacies/ui/PharmaciesTable.tsx | 102 +++ src/features/plans/ui/FilterPlans.tsx | 139 ++++ src/features/plans/ui/PalanTable.tsx | 101 +++ src/features/plans/ui/PlansList.tsx | 267 +------ src/features/reports/ui/ReportsList.tsx | 113 +-- src/features/reports/ui/ReportsTable.tsx | 78 ++ src/features/users/lib/api.ts | 10 +- src/features/users/lib/data.ts | 36 + src/features/users/lib/form.ts | 1 + src/features/users/ui/AddUsers.tsx | 65 +- src/features/users/ui/DeleteUser.tsx | 1 - src/features/users/ui/UsersList.tsx | 2 +- src/shared/config/api/URLs.ts | 14 +- src/shared/config/api/httpClient.ts | 2 - .../ui/pagination.tsx} | 4 +- 45 files changed, 3610 insertions(+), 1469 deletions(-) delete mode 100644 src/features/districts/ui/PaginationDistrict.tsx create mode 100644 src/features/doctors/ui/DeleteDoctor.tsx delete mode 100644 src/features/doctors/ui/PaginationDoctor.tsx create mode 100644 src/features/location/ui/LocationFilter.tsx create mode 100644 src/features/location/ui/LocationTable.tsx create mode 100644 src/features/objects/lib/api.ts create mode 100644 src/features/objects/ui/DeleteObject.tsx create mode 100644 src/features/objects/ui/ObjectFilter.tsx create mode 100644 src/features/objects/ui/ObjectTable.tsx create mode 100644 src/features/pharmacies/lib/api.ts create mode 100644 src/features/pharmacies/ui/DeletePharmacies.tsx create mode 100644 src/features/pharmacies/ui/PharmaciesFilter.tsx create mode 100644 src/features/pharmacies/ui/PharmaciesTable.tsx create mode 100644 src/features/plans/ui/FilterPlans.tsx create mode 100644 src/features/plans/ui/PalanTable.tsx create mode 100644 src/features/reports/ui/ReportsTable.tsx rename src/{features/users/ui/Pagination.tsx => shared/ui/pagination.tsx} (100%) diff --git a/src/features/districts/lib/api.ts b/src/features/districts/lib/api.ts index 0b79c81..f6789a3 100644 --- a/src/features/districts/lib/api.ts +++ b/src/features/districts/lib/api.ts @@ -8,6 +8,7 @@ export const discrit_api = { limit?: number; offset?: number; name?: string; + user?: number; }): Promise> { const res = await httpClient.get(`${DISTRICT}list/`, { params }); return res; diff --git a/src/features/districts/ui/DistrictsList.tsx b/src/features/districts/ui/DistrictsList.tsx index 9235743..ef54402 100644 --- a/src/features/districts/ui/DistrictsList.tsx +++ b/src/features/districts/ui/DistrictsList.tsx @@ -2,8 +2,8 @@ import { discrit_api } from "@/features/districts/lib/api"; import { type DistrictListData } from "@/features/districts/lib/data"; import DeleteDiscrit from "@/features/districts/ui/DeleteDiscrit"; import Filter from "@/features/districts/ui/Filter"; -import PaginationDistrict from "@/features/districts/ui/PaginationDistrict"; import TableDistrict from "@/features/districts/ui/TableDistrict"; +import Pagination from "@/shared/ui/pagination"; import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; @@ -66,7 +66,7 @@ const DistrictsList = () => { currentPage={currentPage} /> - >; - totalPages: number; -} - -const PaginationDistrict = ({ - currentPage, - setCurrentPage, - totalPages, -}: Props) => { - return ( -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
- ); -}; - -export default PaginationDistrict; diff --git a/src/features/doctors/lib/api.ts b/src/features/doctors/lib/api.ts index 719c931..69005db 100644 --- a/src/features/doctors/lib/api.ts +++ b/src/features/doctors/lib/api.ts @@ -1,4 +1,8 @@ -import type { DoctorListRes } from "@/features/doctors/lib/data"; +import type { + CreateDoctorReq, + DoctorListRes, + UpdateDoctorReq, +} from "@/features/doctors/lib/data"; import httpClient from "@/shared/config/api/httpClient"; import { DOCTOR } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; @@ -17,4 +21,19 @@ export const doctor_api = { const res = await httpClient.get(`${DOCTOR}list/`, { params }); return res; }, + + async create(body: CreateDoctorReq) { + const res = await httpClient.post(`${DOCTOR}create/`, body); + return res; + }, + + async update({ body, id }: { id: number; body: UpdateDoctorReq }) { + const res = await httpClient.patch(`${DOCTOR}${id}/update/`, body); + return res; + }, + + async delete(id: number) { + const res = await httpClient.delete(`${DOCTOR}${id}/delete/`); + return res; + }, }; diff --git a/src/features/doctors/lib/data.ts b/src/features/doctors/lib/data.ts index ea797ee..9294970 100644 --- a/src/features/doctors/lib/data.ts +++ b/src/features/doctors/lib/data.ts @@ -92,3 +92,32 @@ export interface DoctorListResData { }; created_at: string; } + +export interface CreateDoctorReq { + first_name: string; + last_name: string; + phone_number: string; + work_place: string; + sphere: string; + description: string; + district_id: number; + place_id: number; + user_id: number; + longitude: number; + latitude: number; + extra_location: { + longitude: number; + latitude: number; + }; +} +export interface UpdateDoctorReq { + first_name: string; + last_name: string; + phone_number: string; + work_place: string; + sphere: string; + description: string; + longitude: number; + latitude: number; + extra_location: { longitude: number; latitude: number }; +} diff --git a/src/features/doctors/ui/AddedDoctor.tsx b/src/features/doctors/ui/AddedDoctor.tsx index 9cff9a7..1092e74 100644 --- a/src/features/doctors/ui/AddedDoctor.tsx +++ b/src/features/doctors/ui/AddedDoctor.tsx @@ -1,10 +1,25 @@ -import { fakeDistrict } from "@/features/districts/lib/data"; -import type { DoctorListType } from "@/features/doctors/lib/data"; +import { discrit_api } from "@/features/districts/lib/api"; +import { doctor_api } from "@/features/doctors/lib/api"; +import type { + CreateDoctorReq, + DoctorListResData, + UpdateDoctorReq, +} from "@/features/doctors/lib/data"; import { DoctorForm } from "@/features/doctors/lib/form"; -import { ObjectListData } from "@/features/objects/lib/data"; -import { FakeUserList } from "@/features/users/lib/data"; +import { object_api } from "@/features/objects/lib/api"; +import { user_api } from "@/features/users/lib/api"; import formatPhone from "@/shared/lib/formatPhone"; +import onlyNumber from "@/shared/lib/onlyNumber"; +import { cn } from "@/shared/lib/utils"; import { Button } from "@/shared/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/shared/ui/command"; import { Form, FormControl, @@ -14,58 +29,266 @@ import { } from "@/shared/ui/form"; import { Input } from "@/shared/ui/input"; import { Label } from "@/shared/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { Textarea } from "@/shared/ui/textarea"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; -import { Loader2 } from "lucide-react"; -import { useState, type Dispatch, type SetStateAction } from "react"; +import { + Circle, + Map, + Placemark, + Polygon, + YMaps, + ZoomControl, +} from "@pbe/react-yandex-maps"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; +import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import type z from "zod"; interface Props { - initialValues: DoctorListType | null; + initialValues: DoctorListResData | null; setDialogOpen: Dispatch>; } +interface CoordsData { + lat: number; + lon: number; + polygon: [number, number][][]; +} + const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => { - const [load, setLoad] = useState(false); + const queryClient = useQueryClient(); + const [searchUser, setSearchUser] = useState(""); + const [searchObject, setSearchObject] = useState(""); + const [selectDiscrit, setSelectedDiscrit] = useState(""); + const [searchDiscrit, setSearchDiscrit] = useState(""); + const [openUser, setOpenUser] = useState(false); + const [openDiscrit, setOpenDiscrit] = useState(false); + const [openObject, setOpenObject] = useState(false); + const form = useForm>({ resolver: zodResolver(DoctorForm), defaultValues: { - desc: initialValues?.desc || "", + desc: initialValues?.description || "", district: initialValues?.district.id.toString() || "", first_name: initialValues?.first_name || "", last_name: initialValues?.last_name || "", - lat: initialValues?.lat || "41.2949", - long: initialValues?.long || "69.2361", - object: initialValues?.object.id.toString() || "", + lat: String(initialValues?.latitude) || "41.2949", + long: String(initialValues?.longitude) || "69.2361", + object: initialValues?.place.id.toString() || "", phone_number: initialValues?.phone_number || "+998", - spec: initialValues?.spec || "", - work: initialValues?.work || "", + spec: initialValues?.sphere || "", + work: initialValues?.work_place || "", user: initialValues?.user.id.toString() || "", }, }); - const lat = form.watch("lat"); - const long = form.watch("long"); + const { data: user, isLoading: isUserLoading } = useQuery({ + queryKey: ["user_list", searchUser], + queryFn: () => { + const params: { + limit?: number; + offset?: number; + search?: string; + is_active?: boolean | string; + region_id?: number; + } = { + limit: 8, + search: searchUser, + }; - const handleMapClick = (e: { get: (key: string) => number[] }) => { - const coords = e.get("coords"); - form.setValue("lat", coords[0].toString()); - form.setValue("long", coords[1].toString()); + return user_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const { data: object, isLoading: isObjectLoading } = useQuery({ + queryKey: ["object_list", searchUser, selectDiscrit], + queryFn: () => { + const params: { + name?: string; + district?: string; + } = { + name: searchUser, + district: selectDiscrit, + }; + + return object_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const user_id = form.watch("user"); + + const { data: discrit, isLoading: discritLoading } = useQuery({ + queryKey: ["discrit_list", searchDiscrit, user_id], + queryFn: () => { + const params: { + name?: string; + user?: number; + } = { + name: searchDiscrit, + }; + + if (user_id !== "") { + params.user = Number(user_id); + } + + return discrit_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const { mutate, isPending } = useMutation({ + mutationFn: (body: CreateDoctorReq) => doctor_api.create(body), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["doctor_list"] }); + setDialogOpen(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", + }); + }, + }); + + const { mutate: edit, isPending: editPending } = useMutation({ + mutationFn: ({ body, id }: { body: UpdateDoctorReq; id: number }) => + doctor_api.update({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["doctor_list"] }); + setDialogOpen(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", + }); + }, + }); + + const [coords, setCoords] = useState({ + latitude: 41.311081, + longitude: 69.240562, + }); + const [polygonCoords, setPolygonCoords] = useState< + [number, number][][] | null + >(null); + const [circleCoords, setCircleCoords] = useState<[number, number] | null>( + null, + ); + + const getCoords = async (name: string): Promise => { + const res = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(name)}&format=json&polygon_geojson=1&limit=1`, + ); + const data = await res.json(); + + if (data.length > 0 && data[0].geojson) { + const lat = parseFloat(data[0].lat); + const lon = parseFloat(data[0].lon); + + let polygon: [number, number][][] = []; + + if (data[0].geojson.type === "Polygon") { + polygon = data[0].geojson.coordinates.map((ring: [number, number][]) => + ring.map((coord: [number, number]) => [coord[1], coord[0]]), + ); + } else if (data[0].geojson.type === "MultiPolygon") { + polygon = data[0].geojson.coordinates.map( + (poly: [number, number][][]) => + poly[0].map((coord: [number, number]) => [coord[1], coord[0]]), + ); + } + + return { lat, lon, polygon }; + } + + return null; + }; + + useEffect(() => { + if (initialValues) { + (async () => { + const result = await getCoords(initialValues.district.name); + if (result) { + setCoords({ + latitude: Number(initialValues.latitude), + longitude: Number(initialValues.longitude), + }); + setPolygonCoords(result.polygon); + form.setValue("lat", String(result.lat)); + form.setValue("long", String(result.lon)); + setCircleCoords([ + Number(initialValues.latitude), + Number(initialValues.longitude), + ]); + } + })(); + } + }, [initialValues]); + + const handleMapClick = ( + e: ymaps.IEvent, + ) => { + const [lat, lon] = e.get("coords"); + setCoords({ latitude: lat, longitude: lon }); + form.setValue("lat", String(lat)); + form.setValue("long", String(lon)); }; function onSubmit(values: z.infer) { - setLoad(true); - console.log(values); - setDialogOpen(false); + if (initialValues) { + edit({ + id: initialValues.id, + body: { + description: values.desc, + extra_location: { + latitude: Number(values.lat), + longitude: Number(values.long), + }, + first_name: values.first_name, + last_name: values.last_name, + latitude: Number(values.lat), + longitude: Number(values.long), + phone_number: onlyNumber(values.phone_number), + sphere: values.spec, + work_place: values.work, + }, + }); + } else { + mutate({ + description: values.desc, + district_id: Number(values.district), + extra_location: { + latitude: Number(values.lat), + longitude: Number(values.long), + }, + first_name: values.first_name, + last_name: values.last_name, + latitude: Number(values.lat), + longitude: Number(values.long), + phone_number: onlyNumber(values.phone_number), + place_id: Number(values.object), + sphere: values.spec, + user_id: Number(values.user), + work_place: values.work, + }); + } } return ( @@ -164,98 +387,349 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => { /> ( - - - - - - - - )} - /> - - ( - - - - - - - - )} - /> - - ( - - - - - - - - )} + control={form.control} + render={({ field }) => { + const selectedUser = user?.results.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {isUserLoading ? ( +
+ +
+ ) : user && user.results.length > 0 ? ( + + {user.results.map((u) => ( + { + field.onChange(String(u.id)); + setOpenUser(false); + }} + > + + {u.first_name} {u.last_name} {u.region.name} + + ))} + + ) : ( + Foydalanuvchi topilmadi + )} +
+
+
+
+ + +
+ ); + }} + /> + + { + const selectedDiscrit = discrit?.results.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {discritLoading ? ( +
+ +
+ ) : discrit && discrit.results.length > 0 ? ( + + {discrit.results.map((u) => ( + { + field.onChange(String(u.id)); + const selectedDistrict = + discrit.results?.find( + (d) => d.id === Number(u.id), + ); + setOpenUser(false); + + if (!selectedDistrict) return; + + setSelectedDiscrit(selectedDistrict.name); + + const coordsData = await getCoords( + selectedDistrict?.name, + ); + if (!coordsData) return; + + setCoords({ + latitude: coordsData.lat, + longitude: coordsData.lon, + }); + setPolygonCoords(coordsData.polygon); + + form.setValue("lat", String(coordsData.lat)); + form.setValue("long", String(coordsData.lon)); + setOpenDiscrit(false); + }} + > + + {u.name} + + ))} + + ) : ( + Tuman topilmadi + )} +
+
+
+
+ + +
+ ); + }} + /> + + { + const selectedObject = object?.results.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {isObjectLoading ? ( +
+ +
+ ) : object && object.results.length > 0 ? ( + + {object.results.map((u) => ( + { + field.onChange(String(u.id)); + const selectedObject = object.results?.find( + (d) => d.id === Number(u.id), + ); + setOpenUser(false); + + if (!selectedObject) return; + + setCircleCoords([ + selectedObject.latitude, + selectedObject.longitude, + ]); + setCoords({ + latitude: selectedObject.latitude, + longitude: selectedObject.longitude, + }); + + form.setValue( + "lat", + String(selectedObject.latitude), + ); + form.setValue( + "long", + String(selectedObject.longitude), + ); + setOpenObject(false); + }} + > + + {u.name} + + ))} + + ) : ( + Obyekt topilmadi + )} +
+
+
+
+ + +
+ ); + }} />
- + - - + + {polygonCoords && ( + + )} + {circleCoords && ( + + )}
@@ -263,7 +737,7 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => { className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer" type="submit" > - {load ? ( + {isPending || editPending ? ( ) : initialValues ? ( "Tahrirlash" diff --git a/src/features/doctors/ui/DeleteDoctor.tsx b/src/features/doctors/ui/DeleteDoctor.tsx new file mode 100644 index 0000000..044adeb --- /dev/null +++ b/src/features/doctors/ui/DeleteDoctor.tsx @@ -0,0 +1,90 @@ +import { doctor_api } from "@/features/doctors/lib/api"; +import type { DoctorListResData } from "@/features/doctors/lib/data"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Loader2, Trash, X } from "lucide-react"; +import { type Dispatch, type SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + opneDelete: boolean; + setOpenDelete: Dispatch>; + setDiscritDelete: Dispatch>; + discrit: DoctorListResData | null; +} + +const DeleteDoctor = ({ + opneDelete, + setOpenDelete, + setDiscritDelete, + discrit, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate: deleteDiscrict, isPending } = useMutation({ + mutationFn: (id: number) => doctor_api.delete(id), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["doctor_list"] }); + toast.success(`Shifokor o'chirildi`); + setOpenDelete(false); + setDiscritDelete(null); + }, + 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 ( + + + + Tumanni o'chirish + + Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "} + ga tegishli {discrit?.first_name} {discrit?.last_name} shifokorni + o'chirmoqchimisiz + + + + + + + + + ); +}; + +export default DeleteDoctor; diff --git a/src/features/doctors/ui/DoctorDetailDialog.tsx b/src/features/doctors/ui/DoctorDetailDialog.tsx index e49367f..92c7414 100644 --- a/src/features/doctors/ui/DoctorDetailDialog.tsx +++ b/src/features/doctors/ui/DoctorDetailDialog.tsx @@ -37,7 +37,9 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => { const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]); - const [circleCoords] = useState<[number, number] | null>(null); + const [circleCoords, setCircleCoords] = useState<[number, number] | null>( + null, + ); const getCoords = async (name: string): Promise => { try { @@ -84,12 +86,11 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => { } setCoords([object.latitude, object.longitude]); - // setCircleCoords([object.latitude, object.longitude]); + setCircleCoords([object.latitude, object.longitude]); }; load(); }, [object]); - if (!object) return null; return ( @@ -154,10 +155,9 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => { /> )} - {/* Radius circle (ish joyi atrofida) */} {circleCoords && ( { const [detail, setDetail] = useState(null); const [detailDialog, setDetailDialog] = useState(false); - const [editingPlan, setEditingPlan] = useState(null); + const [editingPlan, setEditingPlan] = useState( + null, + ); const [dialogOpen, setDialogOpen] = useState(false); const [currentPage, setCurrentPage] = useState(1); @@ -24,6 +24,11 @@ const DoctorsList = () => { const [searchSpec, setSearchSpec] = useState(""); const [searchUser, setSearchUser] = useState(""); + const [disricDelete, setDiscritDelete] = useState( + null, + ); + const [opneDelete, setOpenDelete] = useState(false); + const limit = 20; const { @@ -58,8 +63,10 @@ const DoctorsList = () => { }, }); - // const handleDelete = (id: number) => { - // }; + const handleDelete = (user: DoctorListResData) => { + setDiscritDelete(user); + setOpenDelete(true); + }; const totalPages = doctor ? Math.ceil(doctor.count / limit) : 1; @@ -79,7 +86,6 @@ const DoctorsList = () => { searchSpec={searchSpec} searchUser={searchUser} searchWork={searchWork} - // setData={setData} setDialogOpen={setDialogOpen} setEditingPlan={setEditingPlan} setSearchDistrict={setSearchDistrict} @@ -103,15 +109,25 @@ const DoctorsList = () => { isLoading={isLoading} doctor={doctor ? doctor.results : []} setDetail={setDetail} + handleDelete={handleDelete} isFetching={isFetching} setDetailDialog={setDetailDialog} + setDialogOpen={setDialogOpen} + setEditingPlan={setEditingPlan} /> - + + ); }; diff --git a/src/features/doctors/ui/FilterDoctor.tsx b/src/features/doctors/ui/FilterDoctor.tsx index 4f720f8..51d91d0 100644 --- a/src/features/doctors/ui/FilterDoctor.tsx +++ b/src/features/doctors/ui/FilterDoctor.tsx @@ -1,4 +1,4 @@ -import type { DoctorListType } from "@/features/doctors/lib/data"; +import type { DoctorListResData } from "@/features/doctors/lib/data"; import AddedDoctor from "@/features/doctors/ui/AddedDoctor"; import { Button } from "@/shared/ui/button"; import { @@ -27,8 +27,8 @@ interface Props { setDialogOpen: Dispatch>; searchObject: string; setSearchObject: Dispatch>; - setEditingPlan: Dispatch>; - editingPlan: DoctorListType | null; + setEditingPlan: Dispatch>; + editingPlan: DoctorListResData | null; } const FilterDoctor = ({ diff --git a/src/features/doctors/ui/PaginationDoctor.tsx b/src/features/doctors/ui/PaginationDoctor.tsx deleted file mode 100644 index e0f0a9e..0000000 --- a/src/features/doctors/ui/PaginationDoctor.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { Button } from "@/shared/ui/button"; -import clsx from "clsx"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import type { Dispatch, SetStateAction } from "react"; - -interface Props { - currentPage: number; - setCurrentPage: Dispatch>; - totalPages: number; -} - -const PaginationDoctor = ({ - currentPage, - setCurrentPage, - totalPages, -}: Props) => { - return ( -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
- ); -}; - -export default PaginationDoctor; diff --git a/src/features/doctors/ui/TableDoctor.tsx b/src/features/doctors/ui/TableDoctor.tsx index 3f57935..0542e79 100644 --- a/src/features/doctors/ui/TableDoctor.tsx +++ b/src/features/doctors/ui/TableDoctor.tsx @@ -15,11 +15,14 @@ import type { Dispatch, SetStateAction } from "react"; interface Props { setDetail: Dispatch>; + setEditingPlan: Dispatch>; + setDialogOpen: Dispatch>; setDetailDialog: Dispatch>; doctor: DoctorListResData[] | []; isLoading: boolean; isError: boolean; isFetching: boolean; + handleDelete: (user: DoctorListResData) => void; } const TableDoctor = ({ @@ -27,7 +30,10 @@ const TableDoctor = ({ setDetail, setDetailDialog, isError, + setEditingPlan, isLoading, + setDialogOpen, + handleDelete, isFetching, }: Props) => { return ( @@ -100,8 +106,8 @@ const TableDoctor = ({ size="icon" className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white" onClick={() => { - // setEditingPlan(item); - // setDialogOpen(true); + setEditingPlan(item); + setDialogOpen(true); }} > @@ -110,7 +116,7 @@ const TableDoctor = ({ variant="destructive" size="icon" className="cursor-pointer" - // onClick={() => handleDelete(item.id)} + onClick={() => handleDelete(item)} > diff --git a/src/features/location/ui/LocationFilter.tsx b/src/features/location/ui/LocationFilter.tsx new file mode 100644 index 0000000..1d46cc6 --- /dev/null +++ b/src/features/location/ui/LocationFilter.tsx @@ -0,0 +1,74 @@ +import { Button } from "@/shared/ui/button"; +import { Calendar } from "@/shared/ui/calendar"; +import { Input } from "@/shared/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; +import { ChevronDownIcon } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + open: boolean; + setOpen: Dispatch>; + dateFilter: Date | undefined; + setDateFilter: Dispatch>; + searchUser: string; + setSearchUser: Dispatch>; +} + +const LocationFilter = ({ + open, + setOpen, + dateFilter, + setDateFilter, + searchUser, + setSearchUser, +}: Props) => { + return ( +
+ + + + + + { + setDateFilter(date); + setOpen(false); + }} + /> +
+ +
+
+
+ + setSearchUser(e.target.value)} + /> +
+ ); +}; + +export default LocationFilter; diff --git a/src/features/location/ui/LocationList.tsx b/src/features/location/ui/LocationList.tsx index 2d03322..3fc3920 100644 --- a/src/features/location/ui/LocationList.tsx +++ b/src/features/location/ui/LocationList.tsx @@ -3,27 +3,9 @@ import { type LocationListType, } from "@/features/location/lib/data"; import LocationDetailDialog from "@/features/location/ui/LocationDetailDialog"; -import formatDate from "@/shared/lib/formatDate"; -import { Button } from "@/shared/ui/button"; -import { Calendar } from "@/shared/ui/calendar"; -import { Input } from "@/shared/ui/input"; -import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/shared/ui/table"; -import clsx from "clsx"; -import { - ChevronDownIcon, - ChevronLeft, - ChevronRight, - Eye, - Trash2, -} from "lucide-react"; +import LocationFilter from "@/features/location/ui/LocationFilter"; +import LocationTable from "@/features/location/ui/LocationTable"; +import Pagination from "@/shared/ui/pagination"; import { useMemo, useState } from "react"; const LocationList = () => { @@ -62,54 +44,14 @@ const LocationList = () => {

Jo'natilgan lokatsiyalar

-
- - - - - - { - setDateFilter(date); - setOpen(false); - }} - /> -
- -
-
-
- - setSearchUser(e.target.value)} - /> -
+ { />
-
- - - - # - Jo'natgan foydalanuvchi - Jo'natgan vaqti - Qayerdan jo'natdi - Amallar - - - - {filtered.map((item, index) => ( - - {index + 1} - - {item.user.firstName} {item.user.lastName} - - - {formatDate.format(item.createdAt, "DD-MM-YYYY")} - + - - {item.district - ? "Tuman" - : item.object - ? "Obyekt" - : item.doctor - ? "Shifokor" - : item.pharmcies - ? "Dorixona" - : "Turgan joyidan"} - - - - - - - - ))} - -
-
- -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+ ); }; diff --git a/src/features/location/ui/LocationTable.tsx b/src/features/location/ui/LocationTable.tsx new file mode 100644 index 0000000..958af3f --- /dev/null +++ b/src/features/location/ui/LocationTable.tsx @@ -0,0 +1,92 @@ +import type { LocationListType } from "@/features/location/lib/data"; +import formatDate from "@/shared/lib/formatDate"; +import { Button } from "@/shared/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui/table"; +import { Eye, Trash2 } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + filtered: LocationListType[]; + setDetail: Dispatch>; + setDetailDialog: Dispatch>; + handleDelete: (id: number) => void; +} + +const LocationTable = ({ + filtered, + setDetail, + setDetailDialog, + handleDelete, +}: Props) => { + return ( +
+ + + + # + Jo'natgan foydalanuvchi + Jo'natgan vaqti + Qayerdan jo'natdi + Amallar + + + + {filtered.map((item, index) => ( + + {index + 1} + + {item.user.firstName} {item.user.lastName} + + + {formatDate.format(item.createdAt, "DD-MM-YYYY")} + + + + {item.district + ? "Tuman" + : item.object + ? "Obyekt" + : item.doctor + ? "Shifokor" + : item.pharmcies + ? "Dorixona" + : "Turgan joyidan"} + + + + + + + + ))} + +
+
+ ); +}; + +export default LocationTable; diff --git a/src/features/objects/lib/api.ts b/src/features/objects/lib/api.ts new file mode 100644 index 0000000..85b5d79 --- /dev/null +++ b/src/features/objects/lib/api.ts @@ -0,0 +1,36 @@ +import type { + ObjectCreate, + ObjectListRes, + ObjectUpdate, +} from "@/features/objects/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { OBJECT } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const object_api = { + async list(params: { + limit?: number; + offset?: number; + name?: string; + district?: string; + user?: string; + }): Promise> { + const res = await httpClient.get(`${OBJECT}list/`, { params }); + return res; + }, + + async create(body: ObjectCreate) { + const res = await httpClient.post(`${OBJECT}create/`, body); + return res; + }, + + async update({ body, id }: { id: number; body: ObjectUpdate }) { + const res = await httpClient.patch(`${OBJECT}${id}/update/`, body); + return res; + }, + + async delete(id: number) { + const res = await httpClient.delete(`${OBJECT}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/objects/lib/data.ts b/src/features/objects/lib/data.ts index aa00d4f..6dcef83 100644 --- a/src/features/objects/lib/data.ts +++ b/src/features/objects/lib/data.ts @@ -31,3 +31,52 @@ export const ObjectListData: ObjectListType[] = [ moreLong: ["41.2851", "69.2043"], }, ]; + +export interface ObjectListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: string | null; + previous: string | null; + results: ObjectListData[]; + }; +} + +export interface ObjectListData { + id: number; + name: string; + district: { + id: number; + name: string; + }; + user: { + id: string; + first_name: string; + last_name: string; + }; + longitude: number; + latitude: number; + extra_location: { + latitude: number; + longitude: number; + }; + created_at: string; +} + +export interface ObjectCreate { + district_id: number; + user_id: number; + name: string; + longitude: number; + latitude: number; + extra_location: { longitude: number; latitude: number }; +} + +export interface ObjectUpdate { + name: string; + longitude: number; + latitude: number; + extra_location: { longitude: number; latitude: number }; +} diff --git a/src/features/objects/ui/AddedObject.tsx b/src/features/objects/ui/AddedObject.tsx index a99ea89..01795ca 100644 --- a/src/features/objects/ui/AddedObject.tsx +++ b/src/features/objects/ui/AddedObject.tsx @@ -1,8 +1,22 @@ -import { fakeDistrict } from "@/features/districts/lib/data"; -import type { ObjectListType } from "@/features/objects/lib/data"; +import { discrit_api } from "@/features/districts/lib/api"; +import { object_api } from "@/features/objects/lib/api"; +import type { + ObjectCreate, + ObjectListData, + ObjectUpdate, +} from "@/features/objects/lib/data"; import { ObjectForm } from "@/features/objects/lib/form"; -import { FakeUserList } from "@/features/users/lib/data"; +import { user_api } from "@/features/users/lib/api"; +import { cn } from "@/shared/lib/utils"; import { Button } from "@/shared/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/shared/ui/command"; import { Form, FormControl, @@ -12,77 +26,225 @@ import { } from "@/shared/ui/form"; import { Input } from "@/shared/ui/input"; import { Label } from "@/shared/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; -import { Loader2 } from "lucide-react"; -import { useState, type Dispatch, type SetStateAction } from "react"; +import { + Circle, + Map, + Placemark, + Polygon, + YMaps, + ZoomControl, +} from "@pbe/react-yandex-maps"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; +import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import type z from "zod"; interface Props { - initialValues: ObjectListType | null; + initialValues: ObjectListData | null; setDialogOpen: Dispatch>; - setData: Dispatch>; } -export default function AddedObject({ - initialValues, - setDialogOpen, - setData, -}: Props) { - const [load, setLoad] = useState(false); +interface CoordsData { + lat: number; + lon: number; + polygon: [number, number][][]; +} + +export default function AddedObject({ initialValues, setDialogOpen }: Props) { + const [searchUser, setSearchUser] = useState(""); + const [searchDiscrit, setSearchDiscrit] = useState(""); + const queryClient = useQueryClient(); + const [openUser, setOpenUser] = useState(false); + const [openDiscrit, setOpenDiscrit] = useState(false); const form = useForm>({ resolver: zodResolver(ObjectForm), defaultValues: { - lat: initialValues?.lat || "41.2949", - long: initialValues?.long || "69.2361", + lat: initialValues ? String(initialValues?.latitude) : "41.2949", + long: initialValues ? String(initialValues?.longitude) : "69.2361", name: initialValues?.name || "", user: initialValues ? String(initialValues.user.id) : "", district: initialValues ? String(initialValues.district.id) : "", }, }); - const lat = form.watch("lat"); - const long = form.watch("long"); + const { data: user, isLoading: isUserLoading } = useQuery({ + queryKey: ["user_list", searchUser], + queryFn: () => { + const params: { + limit?: number; + offset?: number; + search?: string; + is_active?: boolean | string; + region_id?: number; + } = { + limit: 8, + search: searchUser, + }; - const handleMapClick = (e: { get: (key: string) => number[] }) => { - const coords = e.get("coords"); - form.setValue("lat", coords[0].toString()); - form.setValue("long", coords[1].toString()); + return user_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const user_id = form.watch("user"); + + const { data: discrit, isLoading: discritLoading } = useQuery({ + queryKey: ["discrit_list", searchDiscrit, user_id], + queryFn: () => { + const params: { + name?: string; + user?: number; + } = { + name: searchDiscrit, + }; + + if (user_id !== "") { + params.user = Number(user_id); + } + + return discrit_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const [coords, setCoords] = useState({ + latitude: 41.311081, + longitude: 69.240562, + }); + const [polygonCoords, setPolygonCoords] = useState< + [number, number][][] | null + >(null); + const [circleCoords, setCircleCoords] = useState<[number, number] | null>( + null, + ); + + const getCoords = async (name: string): Promise => { + const res = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(name)}&format=json&polygon_geojson=1&limit=1`, + ); + const data = await res.json(); + + if (data.length > 0 && data[0].geojson) { + const lat = parseFloat(data[0].lat); + const lon = parseFloat(data[0].lon); + + let polygon: [number, number][][] = []; + + if (data[0].geojson.type === "Polygon") { + polygon = data[0].geojson.coordinates.map((ring: [number, number][]) => + ring.map((coord: [number, number]) => [coord[1], coord[0]]), + ); + } else if (data[0].geojson.type === "MultiPolygon") { + polygon = data[0].geojson.coordinates.map( + (poly: [number, number][][]) => + poly[0].map((coord: [number, number]) => [coord[1], coord[0]]), + ); + } + + return { lat, lon, polygon }; + } + + return null; }; - function onSubmit(values: z.infer) { - setLoad(true); - const newObject: ObjectListType = { - id: initialValues ? initialValues.id : Date.now(), - name: values.name, - lat: values.lat, - long: values.long, - moreLong: [values.long, values.lat], - user: FakeUserList.find((u) => u.id === Number(values.user))!, - district: fakeDistrict.find((d) => d.id === Number(values.district))!, - }; - - setTimeout(() => { - setData((prev) => { - if (initialValues) { - return prev.map((item) => - item.id === initialValues.id ? newObject : item, - ); - } else { - return [...prev, newObject]; + useEffect(() => { + if (initialValues) { + (async () => { + const result = await getCoords(initialValues.district.name); + if (result) { + setCoords({ + latitude: initialValues.latitude, + longitude: initialValues.longitude, + }); + setPolygonCoords(result.polygon); + form.setValue("lat", String(result.lat)); + form.setValue("long", String(result.lon)); + setCircleCoords([initialValues.latitude, initialValues.longitude]); } - }); - setLoad(false); + })(); + } + }, [initialValues]); + + const handleMapClick = ( + e: ymaps.IEvent, + ) => { + const [lat, lon] = e.get("coords"); + setCoords({ latitude: lat, longitude: lon }); + form.setValue("lat", String(lat)); + form.setValue("long", String(lon)); + }; + + const { mutate, isPending } = useMutation({ + mutationFn: (body: ObjectCreate) => object_api.create(body), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["object_list"] }); + toast.success(`Obyekt qo'shildi`); setDialogOpen(false); - }, 2000); + }, + 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 { mutate: edit, isPending: editPending } = useMutation({ + mutationFn: ({ body, id }: { id: number; body: ObjectUpdate }) => + object_api.update({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["object_list"] }); + toast.success(`Obyekt qo'shildi`); + setDialogOpen(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(values: z.infer) { + if (initialValues) { + edit({ + body: { + extra_location: { + latitude: Number(values.lat), + longitude: Number(values.long), + }, + latitude: Number(values.lat), + longitude: Number(values.long), + name: values.name, + }, + id: initialValues.id, + }); + } else { + mutate({ + district_id: Number(values.district), + extra_location: { + latitude: Number(values.lat), + longitude: Number(values.long), + }, + latitude: Number(values.lat), + longitude: Number(values.long), + name: values.name, + user_id: Number(values.user), + }); + } } return ( @@ -103,76 +265,236 @@ export default function AddedObject({ /> ( - - - - - - - - )} + render={({ field }) => { + const selectedUser = user?.results.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {isUserLoading ? ( +
+ +
+ ) : user && user.results.length > 0 ? ( + + {user.results.map((u) => ( + { + field.onChange(String(u.id)); + setOpenUser(false); + }} + > + + {u.first_name} {u.last_name} {u.region.name} + + ))} + + ) : ( + Foydalanuvchi topilmadi + )} +
+
+
+
+ + +
+ ); + }} /> ( - - - - - - - - )} + render={({ field }) => { + const selectedDiscrit = discrit?.results.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {discritLoading ? ( +
+ +
+ ) : discrit && discrit.results.length > 0 ? ( + + {discrit.results.map((u) => ( + { + field.onChange(String(u.id)); + const selectedDistrict = + discrit.results?.find( + (d) => d.id === Number(u.id), + ); + setOpenUser(false); + + if (!selectedDistrict) return; + + const coordsData = await getCoords( + selectedDistrict?.name, + ); + if (!coordsData) return; + + setCoords({ + latitude: coordsData.lat, + longitude: coordsData.lon, + }); + setPolygonCoords(coordsData.polygon); + + form.setValue("lat", String(coordsData.lat)); + form.setValue("long", String(coordsData.lon)); + }} + > + + {u.name} + + ))} + + ) : ( + Tuman topilmadi + )} +
+
+
+
+ + +
+ ); + }} />
- + - - + + {polygonCoords && ( + + )} + {circleCoords && ( + + )}
@@ -180,7 +502,7 @@ export default function AddedObject({ className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer" type="submit" > - {load ? ( + {isPending || editPending ? ( ) : initialValues ? ( "Tahrirlash" diff --git a/src/features/objects/ui/DeleteObject.tsx b/src/features/objects/ui/DeleteObject.tsx new file mode 100644 index 0000000..5bb91c4 --- /dev/null +++ b/src/features/objects/ui/DeleteObject.tsx @@ -0,0 +1,89 @@ +import { object_api } from "@/features/objects/lib/api"; +import type { ObjectListData } from "@/features/objects/lib/data"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Loader2, Trash, X } from "lucide-react"; +import { type Dispatch, type SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + opneDelete: boolean; + setOpenDelete: Dispatch>; + setDiscritDelete: Dispatch>; + discrit: ObjectListData | null; +} + +const DeleteObject = ({ + opneDelete, + setOpenDelete, + setDiscritDelete, + discrit, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate: deleteDiscrict, isPending } = useMutation({ + mutationFn: (id: number) => object_api.delete(id), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["object_list"] }); + toast.success(`Tuman o'chirildi`); + setOpenDelete(false); + setDiscritDelete(null); + }, + 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 ( + + + + Tumanni o'chirish + + Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "} + ga tegishli {discrit?.name} obyektni o'chirmoqchimisiz + + + + + + + + + ); +}; + +export default DeleteObject; diff --git a/src/features/objects/ui/ObjectDetail.tsx b/src/features/objects/ui/ObjectDetail.tsx index 00702fd..a76d875 100644 --- a/src/features/objects/ui/ObjectDetail.tsx +++ b/src/features/objects/ui/ObjectDetail.tsx @@ -1,4 +1,4 @@ -import type { ObjectListType } from "@/features/objects/lib/data"; +import type { ObjectListData } from "@/features/objects/lib/data"; import { Button } from "@/shared/ui/button"; import { Card, CardContent } from "@/shared/ui/card"; import { @@ -8,16 +8,89 @@ import { DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; -import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; -import { type Dispatch, type SetStateAction } from "react"; +import { + Circle, + Map, + Placemark, + Polygon, + YMaps, + ZoomControl, +} from "@pbe/react-yandex-maps"; +import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; interface Props { - object: ObjectListType | null; + object: ObjectListData | null; setDetail: Dispatch>; detail: boolean; } +interface CoordsData { + lat: number; + lon: number; + polygon: [number, number][][]; +} + const ObjectDetailDialog = ({ object, detail, setDetail }: Props) => { + const [coords, setCoords] = useState<[number, number]>([ + 41.311081, 69.240562, + ]); + + const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]); + + const [circleCoords, setCircleCoords] = useState<[number, number] | null>( + null, + ); + + const getCoords = async (name: string): Promise => { + try { + const res = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent( + name, + )}&format=json&polygon_geojson=1&limit=1`, + ); + const data = await res.json(); + + if (!data.length || !data[0].geojson) return null; + + const lat = parseFloat(data[0].lat); + const lon = parseFloat(data[0].lon); + + let polygon: [number, number][][] = []; + + if (data[0].geojson.type === "Polygon") { + polygon = data[0].geojson.coordinates.map((ring: []) => + ring.map((c) => [c[1], c[0]]), + ); + } + + if (data[0].geojson.type === "MultiPolygon") { + polygon = data[0].geojson.coordinates[0].map((ring: []) => + ring.map((c) => [c[1], c[0]]), + ); + } + + return { lat, lon, polygon }; + } catch { + return null; + } + }; + + useEffect(() => { + if (!object) return; + + const load = async () => { + const district = await getCoords(object.district.name); + + if (district) { + setPolygonCoords(district.polygon); + } + + setCoords([object.latitude, object.longitude]); + setCircleCoords([object.latitude, object.longitude]); + }; + + load(); + }, [object]); if (!object) return null; return ( @@ -35,30 +108,52 @@ const ObjectDetailDialog = ({ object, detail, setDetail }: Props) => {
Foydalanuvchi:{" "} - {object.user.firstName} {object.user.lastName} + {object.user.first_name} {object.user.last_name}
- + - - + + {/* Ish joyining markazi */} + + + {/* Tuman polygon */} + {polygonCoords.length > 0 && ( + + )} + + {circleCoords && ( + + )}
diff --git a/src/features/objects/ui/ObjectFilter.tsx b/src/features/objects/ui/ObjectFilter.tsx new file mode 100644 index 0000000..c940c92 --- /dev/null +++ b/src/features/objects/ui/ObjectFilter.tsx @@ -0,0 +1,88 @@ +import type { ObjectListData } from "@/features/objects/lib/data"; +import AddedObject from "@/features/objects/ui/AddedObject"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/shared/ui/dialog"; +import { Input } from "@/shared/ui/input"; +import { Plus } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + searchName: string; + setSearchName: Dispatch>; + searchDistrict: string; + setSearchDistrict: Dispatch>; + searchUser: string; + setSearchUser: Dispatch>; + dialogOpen: boolean; + setDialogOpen: Dispatch>; + setEditingPlan: Dispatch>; + editingPlan: ObjectListData | null; +} + +const ObjectFilter = ({ + searchName, + setSearchName, + searchDistrict, + setSearchDistrict, + searchUser, + setSearchUser, + dialogOpen, + setDialogOpen, + editingPlan, + setEditingPlan, +}: Props) => { + return ( +
+ setSearchName(e.target.value)} + className="w-full md:w-48" + /> + setSearchDistrict(e.target.value)} + className="w-full md:w-48" + /> + setSearchUser(e.target.value)} + className="w-full md:w-48" + /> + + + + + + + + + {editingPlan ? "Obyektni tahrirlash" : "Yangi obyekt qo'shish"} + + + + + + +
+ ); +}; + +export default ObjectFilter; diff --git a/src/features/objects/ui/ObjectList.tsx b/src/features/objects/ui/ObjectList.tsx index 1b2d743..33a53ba 100644 --- a/src/features/objects/ui/ObjectList.tsx +++ b/src/features/objects/ui/ObjectList.tsx @@ -1,126 +1,78 @@ -import { - ObjectListData, - type ObjectListType, -} from "@/features/objects/lib/data"; -import AddedObject from "@/features/objects/ui/AddedObject"; +import { object_api } from "@/features/objects/lib/api"; +import { ObjectListData } from "@/features/objects/lib/data"; +import DeleteObject from "@/features/objects/ui/DeleteObject"; import ObjectDetailDialog from "@/features/objects/ui/ObjectDetail"; -import { Badge } from "@/shared/ui/badge"; -import { Button } from "@/shared/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/shared/ui/dialog"; -import { Input } from "@/shared/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/shared/ui/table"; -import clsx from "clsx"; -import { - ChevronLeft, - ChevronRight, - Eye, - Pencil, - Plus, - Trash2, -} from "lucide-react"; -import { useMemo, useState } from "react"; +import ObjectFilter from "@/features/objects/ui/ObjectFilter"; +import ObjectTable from "@/features/objects/ui/ObjectTable"; +import Pagination from "@/shared/ui/pagination"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; export default function ObjectList() { - const [data, setData] = useState(ObjectListData); - const [detail, setDetail] = useState(null); + const [detail, setDetail] = useState(null); const [detailDialog, setDetailDialog] = useState(false); - const [editingPlan, setEditingPlan] = useState(null); + const [editingPlan, setEditingPlan] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; + const limit = 20; - // Filter state const [searchName, setSearchName] = useState(""); const [searchDistrict, setSearchDistrict] = useState(""); const [searchUser, setSearchUser] = useState(""); - const handleDelete = (id: number) => { - setData((prev) => prev.filter((e) => e.id !== id)); + const [disricDelete, setDiscritDelete] = useState( + null, + ); + const [opneDelete, setOpenDelete] = useState(false); + + const handleDelete = (user: ObjectListData) => { + setDiscritDelete(user); + setOpenDelete(true); }; - // Filtered data - const filteredData = useMemo(() => { - return data.filter((item) => { - const nameMatch = item.name - .toLowerCase() - .includes(searchName.toLowerCase()); - const districtMatch = item.district.name - .toLowerCase() - .includes(searchDistrict.toLowerCase()); - const userMatch = `${item.user.firstName} ${item.user.lastName}` - .toLowerCase() - .includes(searchUser.toLowerCase()); + const { + data: object, + isLoading, + isError, + } = useQuery({ + queryKey: [ + "object_list", + searchDistrict, + currentPage, + searchName, + searchUser, + ], + queryFn: () => + object_api.list({ + district: searchDistrict, + limit, + offset: (currentPage - 1) * limit, + name: searchName, + user: searchUser, + }), + select(data) { + return data.data.data; + }, + }); - return nameMatch && districtMatch && userMatch; - }); - }, [data, searchName, searchDistrict, searchUser]); + const totalPages = object ? Math.ceil(object.count / 20) : 1; return (

Obyektlarni boshqarish

- -
- setSearchName(e.target.value)} - className="w-full md:w-48" - /> - setSearchDistrict(e.target.value)} - className="w-full md:w-48" - /> - setSearchUser(e.target.value)} - className="w-full md:w-48" - /> - - - - - - - - - {editingPlan - ? "Obyektni tahrirlash" - : "Yangi obyekt qo'shish"} - - - - - - -
- +
-
- - - - # - Obyekt nomi - Tuman - Foydalanuvchi - Amallar - - - - {filteredData.map((item, index) => ( - - {index + 1} - {item.name} - - {item.district.name} - - - {item.user.firstName} {item.user.lastName} - - - - - - - - ))} - -
-
+ -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+ + +
); } diff --git a/src/features/objects/ui/ObjectTable.tsx b/src/features/objects/ui/ObjectTable.tsx new file mode 100644 index 0000000..721df52 --- /dev/null +++ b/src/features/objects/ui/ObjectTable.tsx @@ -0,0 +1,124 @@ +import type { ObjectListData } from "@/features/objects/lib/data"; +import { Badge } from "@/shared/ui/badge"; +import { Button } from "@/shared/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui/table"; +import { Eye, Loader2, Pencil, Trash2 } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + filteredData: ObjectListData[] | []; + setDetail: Dispatch>; + setDetailDialog: Dispatch>; + setEditingPlan: Dispatch>; + setDialogOpen: Dispatch>; + handleDelete: (object: ObjectListData) => void; + isLoading: boolean; + isError: boolean; +} + +const ObjectTable = ({ + filteredData, + setDetail, + setDetailDialog, + setEditingPlan, + handleDelete, + setDialogOpen, + isError, + isLoading, +}: Props) => { + return ( +
+ {isLoading && ( +
+ + + +
+ )} + + {isError && ( +
+ + Ma'lumotlarni olishda xatolik yuz berdi. + +
+ )} + {!isError && !isLoading && ( + + + + # + Obyekt nomi + Tuman + Foydalanuvchi + Amallar + + + + {filteredData && filteredData.length > 0 ? ( + filteredData.map((item, index) => ( + + {index + 1} + {item.name} + + {item.district.name} + + + {item.user.first_name} {item.user.last_name} + + + + + + + + )) + ) : ( + + + Obyekt topilmadi. + + + )} + +
+ )} +
+ ); +}; + +export default ObjectTable; diff --git a/src/features/pharmacies/lib/api.ts b/src/features/pharmacies/lib/api.ts new file mode 100644 index 0000000..d704dcd --- /dev/null +++ b/src/features/pharmacies/lib/api.ts @@ -0,0 +1,37 @@ +import type { + CreatePharmaciesReq, + PharmaciesListRes, + UpdatePharmaciesReq, +} from "@/features/pharmacies/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { PHARMACIES } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const pharmacies_api = { + async list(params: { + limit?: number; + offset?: number; + name?: string; + place?: string; + district?: string; + user?: string; + }): Promise> { + const res = await httpClient.get(`${PHARMACIES}list/`, { params }); + return res; + }, + + async create(body: CreatePharmaciesReq) { + const res = await httpClient.post(`${PHARMACIES}create/`, body); + return res; + }, + + async update({ body, id }: { id: number; body: UpdatePharmaciesReq }) { + const res = await httpClient.patch(`${PHARMACIES}${id}/update/`, body); + return res; + }, + + async delete(id: number) { + const res = await httpClient.delete(`${PHARMACIES}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/pharmacies/lib/data.ts b/src/features/pharmacies/lib/data.ts index 85fbefc..aa1fe5c 100644 --- a/src/features/pharmacies/lib/data.ts +++ b/src/features/pharmacies/lib/data.ts @@ -80,3 +80,66 @@ export const PharmciesData: PharmciesType[] = [ lat: "41.3", }, ]; + +export interface PharmaciesListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: null | string; + previous: null | string; + results: PharmaciesListData[]; + }; +} + +export interface PharmaciesListData { + id: number; + name: string; + inn: string; + owner_phone: string; + responsible_phone: string; + district: { + id: number; + name: string; + }; + place: { + id: number; + name: string; + }; + user: { + id: number; + first_name: string; + last_name: string; + }; + longitude: number; + latitude: number; + extra_location: { + latitude: number; + longitude: number; + }; + created_at: string; +} + +export interface CreatePharmaciesReq { + name: string; + inn: string; + owner_phone: string; + responsible_phone: string; + district_id: number; + place_id: number; + user_id: number; + longitude: number; + latitude: number; + extra_location: { longitude: number; latitude: number }; +} + +export interface UpdatePharmaciesReq { + name: string; + inn: string; + owner_phone: string; + responsible_phone: string; + longitude: number; + latitude: number; + extra_location: { longitude: number; latitude: number }; +} diff --git a/src/features/pharmacies/ui/AddedPharmacies.tsx b/src/features/pharmacies/ui/AddedPharmacies.tsx index 961df1b..8cfa8ec 100644 --- a/src/features/pharmacies/ui/AddedPharmacies.tsx +++ b/src/features/pharmacies/ui/AddedPharmacies.tsx @@ -1,11 +1,25 @@ -import { fakeDistrict } from "@/features/districts/lib/data"; -import { ObjectListData } from "@/features/objects/lib/data"; -import type { PharmciesType } from "@/features/pharmacies/lib/data"; +import { discrit_api } from "@/features/districts/lib/api"; +import { object_api } from "@/features/objects/lib/api"; +import { pharmacies_api } from "@/features/pharmacies/lib/api"; +import type { + CreatePharmaciesReq, + PharmaciesListData, + UpdatePharmaciesReq, +} from "@/features/pharmacies/lib/data"; import { PharmForm } from "@/features/pharmacies/lib/form"; -import { FakeUserList } from "@/features/users/lib/data"; +import { user_api } from "@/features/users/lib/api"; import formatPhone from "@/shared/lib/formatPhone"; import onlyNumber from "@/shared/lib/onlyNumber"; +import { cn } from "@/shared/lib/utils"; import { Button } from "@/shared/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/shared/ui/command"; import { Form, FormControl, @@ -15,80 +29,258 @@ import { } from "@/shared/ui/form"; import { Input } from "@/shared/ui/input"; import { Label } from "@/shared/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; -import { Loader2 } from "lucide-react"; -import { useState, type Dispatch, type SetStateAction } from "react"; +import { + Circle, + Map, + Placemark, + Polygon, + YMaps, + ZoomControl, +} from "@pbe/react-yandex-maps"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; +import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import type z from "zod"; interface Props { - initialValues: PharmciesType | null; + initialValues: PharmaciesListData | null; setDialogOpen: Dispatch>; - setData: Dispatch>; } -const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => { - const [load, setLoad] = useState(false); +interface CoordsData { + lat: number; + lon: number; + polygon: [number, number][][]; +} + +const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => { + const queryClient = useQueryClient(); + const [searchUser, setSearchUser] = useState(""); + const [searchObject, setSearchObject] = useState(""); + const [selectDiscrit, setSelectedDiscrit] = useState(""); + const [searchDiscrit, setSearchDiscrit] = useState(""); + const [openUser, setOpenUser] = useState(false); + const [openDiscrit, setOpenDiscrit] = useState(false); + const [openObject, setOpenObject] = useState(false); const form = useForm>({ resolver: zodResolver(PharmForm), defaultValues: { - additional_phone: initialValues?.additional_phone || "+998", + additional_phone: initialValues?.responsible_phone || "+998", district: initialValues?.district.id.toString() || "", inn: initialValues?.inn || "", - lat: initialValues?.lat || "41.2949", - long: initialValues?.long || "69.2361", + lat: String(initialValues?.latitude) || "41.2949", + long: String(initialValues?.longitude) || "69.2361", name: initialValues?.name || "", - object: initialValues?.object.id.toString() || "", - phone_number: initialValues?.phone_number || "+998", + object: initialValues?.place.id.toString() || "", + phone_number: initialValues?.owner_phone || "+998", user: initialValues?.user.id.toString() || "", }, }); - const lat = form.watch("lat"); - const long = form.watch("long"); + const { data: user, isLoading: isUserLoading } = useQuery({ + queryKey: ["user_list", searchUser], + queryFn: () => { + const params: { + limit?: number; + offset?: number; + search?: string; + is_active?: boolean | string; + region_id?: number; + } = { + limit: 8, + search: searchUser, + }; - const handleMapClick = (e: { get: (key: string) => number[] }) => { - const coords = e.get("coords"); - form.setValue("lat", coords[0].toString()); - form.setValue("long", coords[1].toString()); + return user_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const { data: object, isLoading: isObjectLoading } = useQuery({ + queryKey: ["object_list", searchUser, selectDiscrit], + queryFn: () => { + const params: { + name?: string; + district?: string; + } = { + name: searchUser, + district: selectDiscrit, + }; + + return object_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const user_id = form.watch("user"); + + const { data: discrit, isLoading: discritLoading } = useQuery({ + queryKey: ["discrit_list", searchDiscrit, user_id], + queryFn: () => { + const params: { + name?: string; + user?: number; + } = { + name: searchDiscrit, + }; + + if (user_id !== "") { + params.user = Number(user_id); + } + + return discrit_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const [coords, setCoords] = useState({ + latitude: 41.311081, + longitude: 69.240562, + }); + const [polygonCoords, setPolygonCoords] = useState< + [number, number][][] | null + >(null); + const [circleCoords, setCircleCoords] = useState<[number, number] | null>( + null, + ); + + const getCoords = async (name: string): Promise => { + const res = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(name)}&format=json&polygon_geojson=1&limit=1`, + ); + const data = await res.json(); + + if (data.length > 0 && data[0].geojson) { + const lat = parseFloat(data[0].lat); + const lon = parseFloat(data[0].lon); + + let polygon: [number, number][][] = []; + + if (data[0].geojson.type === "Polygon") { + polygon = data[0].geojson.coordinates.map((ring: [number, number][]) => + ring.map((coord: [number, number]) => [coord[1], coord[0]]), + ); + } else if (data[0].geojson.type === "MultiPolygon") { + polygon = data[0].geojson.coordinates.map( + (poly: [number, number][][]) => + poly[0].map((coord: [number, number]) => [coord[1], coord[0]]), + ); + } + + return { lat, lon, polygon }; + } + + return null; }; - function onSubmit(values: z.infer) { - setLoad(true); - const newObject: PharmciesType = { - id: initialValues ? initialValues.id : Date.now(), - name: values.name, - lat: values.lat, - long: values.long, - user: FakeUserList.find((u) => u.id === Number(values.user))!, - district: fakeDistrict.find((d) => d.id === Number(values.district))!, - additional_phone: onlyNumber(values.additional_phone), - inn: values.inn, - object: ObjectListData.find((o) => o.id === Number(values.object))!, - phone_number: onlyNumber(values.phone_number), - }; - - setTimeout(() => { - setData((prev) => { - if (initialValues) { - return prev.map((item) => - item.id === initialValues.id ? newObject : item, - ); - } else { - return [...prev, newObject]; + useEffect(() => { + if (initialValues) { + (async () => { + const result = await getCoords(initialValues.district.name); + if (result) { + setCoords({ + latitude: Number(initialValues.latitude), + longitude: Number(initialValues.longitude), + }); + setPolygonCoords(result.polygon); + form.setValue("lat", String(result.lat)); + form.setValue("long", String(result.lon)); + setCircleCoords([ + Number(initialValues.latitude), + Number(initialValues.longitude), + ]); } - }); - setLoad(false); + })(); + } + }, [initialValues]); + + const handleMapClick = ( + e: ymaps.IEvent, + ) => { + const [lat, lon] = e.get("coords"); + setCoords({ latitude: lat, longitude: lon }); + form.setValue("lat", String(lat)); + form.setValue("long", String(lon)); + }; + + const { mutate, isPending } = useMutation({ + mutationFn: (body: CreatePharmaciesReq) => pharmacies_api.create(body), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["pharmacies_list"] }); setDialogOpen(false); - }, 2000); + }, + 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 { mutate: edit, isPending: editPending } = useMutation({ + mutationFn: ({ body, id }: { id: number; body: UpdatePharmaciesReq }) => + pharmacies_api.update({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["pharmacies_list"] }); + setDialogOpen(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(values: z.infer) { + 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), + }, + }); + } else { + mutate({ + 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), + }); + } } return ( @@ -159,101 +351,349 @@ const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => { /> ( - - - - - - - - )} - /> - - ( - - - - - - - - )} - /> - - ( - - - - - - - - )} + control={form.control} + render={({ field }) => { + const selectedUser = user?.results.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {isUserLoading ? ( +
+ +
+ ) : user && user.results.length > 0 ? ( + + {user.results.map((u) => ( + { + field.onChange(String(u.id)); + setOpenUser(false); + }} + > + + {u.first_name} {u.last_name} {u.region.name} + + ))} + + ) : ( + Foydalanuvchi topilmadi + )} +
+
+
+
+ + +
+ ); + }} + /> + + { + const selectedDiscrit = discrit?.results.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {discritLoading ? ( +
+ +
+ ) : discrit && discrit.results.length > 0 ? ( + + {discrit.results.map((u) => ( + { + field.onChange(String(u.id)); + const selectedDistrict = + discrit.results?.find( + (d) => d.id === Number(u.id), + ); + setOpenUser(false); + + if (!selectedDistrict) return; + + setSelectedDiscrit(selectedDistrict.name); + + const coordsData = await getCoords( + selectedDistrict?.name, + ); + if (!coordsData) return; + + setCoords({ + latitude: coordsData.lat, + longitude: coordsData.lon, + }); + setPolygonCoords(coordsData.polygon); + + form.setValue("lat", String(coordsData.lat)); + form.setValue("long", String(coordsData.lon)); + setOpenDiscrit(false); + }} + > + + {u.name} + + ))} + + ) : ( + Tuman topilmadi + )} +
+
+
+
+ + +
+ ); + }} + /> + + { + const selectedObject = object?.results.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {isObjectLoading ? ( +
+ +
+ ) : object && object.results.length > 0 ? ( + + {object.results.map((u) => ( + { + field.onChange(String(u.id)); + const selectedObject = object.results?.find( + (d) => d.id === Number(u.id), + ); + setOpenUser(false); + + if (!selectedObject) return; + + setCircleCoords([ + selectedObject.latitude, + selectedObject.longitude, + ]); + setCoords({ + latitude: selectedObject.latitude, + longitude: selectedObject.longitude, + }); + + form.setValue( + "lat", + String(selectedObject.latitude), + ); + form.setValue( + "long", + String(selectedObject.longitude), + ); + setOpenObject(false); + }} + > + + {u.name} + + ))} + + ) : ( + Obyekt topilmadi + )} +
+
+
+
+ + +
+ ); + }} />
- + - - + + {polygonCoords && ( + + )} + {circleCoords && ( + + )}
@@ -261,7 +701,7 @@ const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => { className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer" type="submit" > - {load ? ( + {isPending || editPending ? ( ) : initialValues ? ( "Tahrirlash" diff --git a/src/features/pharmacies/ui/DeletePharmacies.tsx b/src/features/pharmacies/ui/DeletePharmacies.tsx new file mode 100644 index 0000000..11a6158 --- /dev/null +++ b/src/features/pharmacies/ui/DeletePharmacies.tsx @@ -0,0 +1,89 @@ +import { pharmacies_api } from "@/features/pharmacies/lib/api"; +import type { PharmaciesListData } from "@/features/pharmacies/lib/data"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Loader2, Trash, X } from "lucide-react"; +import { type Dispatch, type SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + opneDelete: boolean; + setOpenDelete: Dispatch>; + setDiscritDelete: Dispatch>; + discrit: PharmaciesListData | null; +} + +const DeletePharmacies = ({ + opneDelete, + setOpenDelete, + setDiscritDelete, + discrit, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate: deleteDiscrict, isPending } = useMutation({ + mutationFn: (id: number) => pharmacies_api.delete(id), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["pharmacies_list"] }); + toast.success(`Dorixona o'chirildi`); + setOpenDelete(false); + setDiscritDelete(null); + }, + 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 ( + + + + Tumanni o'chirish + + Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "} + ga tegishli {discrit?.name} dorixonani o'chirmoqchimisiz + + + + + + + + + ); +}; + +export default DeletePharmacies; diff --git a/src/features/pharmacies/ui/PharmDetailDialog.tsx b/src/features/pharmacies/ui/PharmDetailDialog.tsx index 6f5171d..066560e 100644 --- a/src/features/pharmacies/ui/PharmDetailDialog.tsx +++ b/src/features/pharmacies/ui/PharmDetailDialog.tsx @@ -1,4 +1,4 @@ -import type { PharmciesType } from "@/features/pharmacies/lib/data"; +import type { PharmaciesListData } from "@/features/pharmacies/lib/data"; import formatPhone from "@/shared/lib/formatPhone"; import { Button } from "@/shared/ui/button"; import { @@ -8,30 +8,93 @@ import { DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; -import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; +import { + Circle, + Map, + Placemark, + Polygon, + YMaps, + ZoomControl, +} from "@pbe/react-yandex-maps"; import { useEffect, useState } from "react"; interface Props { detail: boolean; setDetail: (value: boolean) => void; - object: PharmciesType | null; + object: PharmaciesListData | null; +} + +interface CoordsData { + lat: number; + lon: number; + polygon: [number, number][][]; } const PharmDetailDialog = ({ detail, setDetail, object }: Props) => { - const [open, setOpen] = useState(detail); + const [coords, setCoords] = useState<[number, number]>([ + 41.311081, 69.240562, + ]); + + const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]); + + const [circleCoords, setCircleCoords] = useState<[number, number] | null>( + null, + ); + + const getCoords = async (name: string): Promise => { + try { + const res = await fetch( + `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent( + name, + )}&format=json&polygon_geojson=1&limit=1`, + ); + const data = await res.json(); + + if (!data.length || !data[0].geojson) return null; + + const lat = parseFloat(data[0].lat); + const lon = parseFloat(data[0].lon); + + let polygon: [number, number][][] = []; + + if (data[0].geojson.type === "Polygon") { + polygon = data[0].geojson.coordinates.map((ring: []) => + ring.map((c) => [c[1], c[0]]), + ); + } + + if (data[0].geojson.type === "MultiPolygon") { + polygon = data[0].geojson.coordinates[0].map((ring: []) => + ring.map((c) => [c[1], c[0]]), + ); + } + + return { lat, lon, polygon }; + } catch { + return null; + } + }; useEffect(() => { - setOpen(detail); - }, [detail]); + if (!object) return; + + const load = async () => { + const district = await getCoords(object.district.name); + + if (district) { + setPolygonCoords(district.polygon); + } + + setCoords([object.latitude, object.longitude]); + setCircleCoords([object.latitude, object.longitude]); + }; + + load(); + }, [object]); + if (!object) return null; return ( - { - setOpen(val); - setDetail(val); - }} - > + Farmatsiya tafsilotlari @@ -46,44 +109,64 @@ const PharmDetailDialog = ({ detail, setDetail, object }: Props) => { INN: {object.inn}
- Telefon: {formatPhone(object.phone_number)} + Telefon: {formatPhone(object.owner_phone)}
Qo‘shimcha telefon:{" "} - {formatPhone(object.additional_phone)} + {formatPhone(object.responsible_phone)}
Tuman: {object.district.name}
- Obyekt: {object.object.name} + Obyekt: {object.place.name}
- Kimga tegishli: {object.user.firstName}{" "} - {object.user.lastName} + Kimga tegishli: {object.user.first_name}{" "} + {object.user.last_name}
- + - - + + {/* Ish joyining markazi */} + + + {/* Tuman polygon */} + {polygonCoords.length > 0 && ( + + )} + + {circleCoords && ( + + )}
diff --git a/src/features/pharmacies/ui/PharmaciesFilter.tsx b/src/features/pharmacies/ui/PharmaciesFilter.tsx new file mode 100644 index 0000000..3e52242 --- /dev/null +++ b/src/features/pharmacies/ui/PharmaciesFilter.tsx @@ -0,0 +1,98 @@ +import type { PharmaciesListData } from "@/features/pharmacies/lib/data"; +import AddedPharmacies from "@/features/pharmacies/ui/AddedPharmacies"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/shared/ui/dialog"; +import { Input } from "@/shared/ui/input"; +import { Plus } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + searchName: string; + setSearchName: Dispatch>; + searchDistrict: string; + setSearchDistrict: Dispatch>; + searchObject: string; + setSearchObject: Dispatch>; + searchUser: string; + setSearchUser: Dispatch>; + dialogOpen: boolean; + setDialogOpen: Dispatch>; + setEditingPlan: Dispatch>; + editingPlan: PharmaciesListData | null; +} + +const PharmaciesFilter = ({ + searchName, + setSearchName, + searchDistrict, + setSearchDistrict, + searchObject, + searchUser, + setSearchUser, + setSearchObject, + dialogOpen, + setDialogOpen, + setEditingPlan, + editingPlan, +}: Props) => { + return ( +
+ setSearchName(e.target.value)} + className="w-full md:w-48" + /> + setSearchDistrict(e.target.value)} + className="w-full md:w-48" + /> + setSearchObject(e.target.value)} + className="w-full md:w-48" + /> + setSearchUser(e.target.value)} + className="w-full md:w-48" + /> + + + + + + + + {editingPlan + ? "Dorixonani tahrirlash" + : "Yangi dorixona qo'shish"} + + + + + +
+ ); +}; + +export default PharmaciesFilter; diff --git a/src/features/pharmacies/ui/PharmaciesList.tsx b/src/features/pharmacies/ui/PharmaciesList.tsx index 2ddee2b..4d4a8de 100644 --- a/src/features/pharmacies/ui/PharmaciesList.tsx +++ b/src/features/pharmacies/ui/PharmaciesList.tsx @@ -1,132 +1,82 @@ -import { - PharmciesData, - type PharmciesType, -} from "@/features/pharmacies/lib/data"; -import AddedPharmacies from "@/features/pharmacies/ui/AddedPharmacies"; +import { pharmacies_api } from "@/features/pharmacies/lib/api"; +import { type PharmaciesListData } from "@/features/pharmacies/lib/data"; +import DeletePharmacies from "@/features/pharmacies/ui/DeletePharmacies"; +import PharmaciesFilter from "@/features/pharmacies/ui/PharmaciesFilter"; +import PharmaciesTable from "@/features/pharmacies/ui/PharmaciesTable"; import PharmDetailDialog from "@/features/pharmacies/ui/PharmDetailDialog"; -import formatPhone from "@/shared/lib/formatPhone"; -import { Button } from "@/shared/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/shared/ui/dialog"; -import { Input } from "@/shared/ui/input"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/shared/ui/table"; -import clsx from "clsx"; -import { - ChevronLeft, - ChevronRight, - Eye, - Pencil, - Plus, - Trash2, -} from "lucide-react"; -import { useMemo, useState } from "react"; +import Pagination from "@/shared/ui/pagination"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; const PharmaciesList = () => { - const [data, setData] = useState(PharmciesData); - const [detail, setDetail] = useState(null); + const [detail, setDetail] = useState(null); const [detailDialog, setDetailDialog] = useState(false); - const [editingPlan, setEditingPlan] = useState(null); + const [editingPlan, setEditingPlan] = useState( + null, + ); const [dialogOpen, setDialogOpen] = useState(false); const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; + const [disricDelete, setDiscritDelete] = useState( + null, + ); + const [opneDelete, setOpenDelete] = useState(false); + const limit = 20; const [searchName, setSearchName] = useState(""); const [searchDistrict, setSearchDistrict] = useState(""); const [searchObject, setSearchObject] = useState(""); const [searchUser, setSearchUser] = useState(""); - const handleDelete = (id: number) => { - setData((prev) => prev.filter((e) => e.id !== id)); + const { data: pharmacies } = useQuery({ + queryKey: [ + "pharmacies_list", + currentPage, + searchDistrict, + searchName, + searchObject, + searchUser, + ], + queryFn: () => + pharmacies_api.list({ + district: searchDistrict, + offset: (currentPage - 1) * limit, + limit: limit, + name: searchName, + place: searchObject, + user: searchUser, + }), + select(data) { + return data.data.data; + }, + }); + + const totalPages = pharmacies ? Math.ceil(pharmacies.count / limit) : 1; + + const handleDelete = (user: PharmaciesListData) => { + setDiscritDelete(user); + setOpenDelete(true); }; - const filteredData = useMemo(() => { - return data.filter((item) => { - const nameMatch = `${item.name}` - .toLowerCase() - .includes(searchName.toLowerCase()); - const districtMatch = item.district.name - .toLowerCase() - .includes(searchDistrict.toLowerCase()); - const objectMatch = item.object.name - .toLowerCase() - .includes(searchObject.toLowerCase()); - const userMatch = `${item.user.firstName} ${item.user.lastName}` - .toLowerCase() - .includes(searchUser.toLowerCase()); - - return nameMatch && districtMatch && objectMatch && userMatch; - }); - }, [data, searchName, searchDistrict, searchObject, searchUser]); - return (
-

Dorixonalrni boshqarish

+

Dorixonalarni boshqarish

-
- setSearchName(e.target.value)} - className="w-full md:w-48" - /> - setSearchDistrict(e.target.value)} - className="w-full md:w-48" - /> - setSearchObject(e.target.value)} - className="w-full md:w-48" - /> - setSearchUser(e.target.value)} - className="w-full md:w-48" - /> - - - - - - - - {editingPlan - ? "Dorixonani tahrirlash" - : "Yangi dorixona qo'shish"} - - - - - -
+
{ />
-
- - - - # - Dorixona nomi - Inn - Egasining nomeri - Ma'sul shaxsning nomeri - Tuman - Obyekt - Kim qo'shgan - Amallar - - - - {filteredData.map((item, index) => ( - - {index + 1} - {item.name} - {item.inn} - {formatPhone(item.phone_number)} - {formatPhone(item.additional_phone)} - {item.district.name} - {item.object.name} - - {item.user.firstName} {item.user.lastName} - + - - - - - - - ))} - -
-
+ -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+
); }; diff --git a/src/features/pharmacies/ui/PharmaciesTable.tsx b/src/features/pharmacies/ui/PharmaciesTable.tsx new file mode 100644 index 0000000..e738926 --- /dev/null +++ b/src/features/pharmacies/ui/PharmaciesTable.tsx @@ -0,0 +1,102 @@ +import type { PharmaciesListData } from "@/features/pharmacies/lib/data"; +import formatPhone from "@/shared/lib/formatPhone"; +import { Button } from "@/shared/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui/table"; +import { Eye, Pencil, Trash2 } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + filteredData: PharmaciesListData[]; + setDetail: Dispatch>; + setEditingPlan: Dispatch>; + setDetailDialog: Dispatch>; + setDialogOpen: Dispatch>; + handleDelete: (pharmacies: PharmaciesListData) => void; +} + +const PharmaciesTable = ({ + filteredData, + setDetail, + setEditingPlan, + setDetailDialog, + setDialogOpen, + handleDelete, +}: Props) => { + return ( +
+ + + + # + Dorixona nomi + Inn + Egasining nomeri + Ma'sul shaxsning nomeri + Tuman + Obyekt + Kim qo'shgan + Amallar + + + + {filteredData.map((item, index) => ( + + {index + 1} + {item.name} + {item.inn} + {formatPhone(item.owner_phone)} + {formatPhone(item.responsible_phone)} + {item.district.name} + {item.place.name} + + {item.user.first_name} {item.user.last_name} + + + + + + + + + ))} + +
+
+ ); +}; + +export default PharmaciesTable; diff --git a/src/features/plans/ui/FilterPlans.tsx b/src/features/plans/ui/FilterPlans.tsx new file mode 100644 index 0000000..b6b2bdf --- /dev/null +++ b/src/features/plans/ui/FilterPlans.tsx @@ -0,0 +1,139 @@ +import type { Plan } from "@/features/plans/lib/data"; +import AddedPlan from "@/features/plans/ui/AddedPlan"; +import { Button } from "@/shared/ui/button"; +import { Calendar } from "@/shared/ui/calendar"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/shared/ui/dialog"; +import { Input } from "@/shared/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/ui/select"; +import { ChevronDownIcon, Plus } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + statusFilter: string; + setStatusFilter: Dispatch>; + open: boolean; + setOpen: Dispatch>; + dateFilter: Date | undefined; + setDateFilter: Dispatch>; + searchUser: string; + setSearchUser: Dispatch>; + dialogOpen: boolean; + setDialogOpen: Dispatch>; + editingPlan: Plan | null; + setEditingPlan: Dispatch>; + setPlans: Dispatch>; +} + +const FilterPlans = ({ + setStatusFilter, + statusFilter, + open, + setOpen, + dateFilter, + setDateFilter, + searchUser, + setSearchUser, + dialogOpen, + setDialogOpen, + setEditingPlan, + editingPlan, + setPlans, +}: Props) => { + return ( +
+ + + + + + + + { + setDateFilter(date); + setOpen(false); + }} + /> +
+ +
+
+
+ + setSearchUser(e.target.value)} + /> + + + + + + + + {editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"} + + + + + + +
+ ); +}; + +export default FilterPlans; diff --git a/src/features/plans/ui/PalanTable.tsx b/src/features/plans/ui/PalanTable.tsx new file mode 100644 index 0000000..d315588 --- /dev/null +++ b/src/features/plans/ui/PalanTable.tsx @@ -0,0 +1,101 @@ +import type { Plan } from "@/features/plans/lib/data"; +import { Button } from "@/shared/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui/table"; +import clsx from "clsx"; +import { Edit, Eye, Trash } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + filteredPlans: Plan[]; + setEditingPlan: Dispatch>; + setDetail: Dispatch>; + setDialogOpen: Dispatch>; + handleDelete: (id: number) => void; +} + +const PalanTable = ({ + filteredPlans, + setEditingPlan, + setDetail, + setDialogOpen, + handleDelete, +}: Props) => { + return ( +
+ + + + ID + Reja nomi + Tavsifi + Kimga tegishli + Status + Harakatlar + + + + {filteredPlans.map((plan) => ( + + {plan.id} + {plan.name} + {plan.description} + + {plan.user.firstName + " " + plan.user.lastName} + + + {plan.status} + + + + + + + + ))} + +
+
+ ); +}; + +export default PalanTable; diff --git a/src/features/plans/ui/PlansList.tsx b/src/features/plans/ui/PlansList.tsx index 30cd7de..25c8e3b 100644 --- a/src/features/plans/ui/PlansList.tsx +++ b/src/features/plans/ui/PlansList.tsx @@ -1,45 +1,9 @@ -"use client"; - import type { Plan } from "@/features/plans/lib/data"; -import AddedPlan from "@/features/plans/ui/AddedPlan"; +import FilterPlans from "@/features/plans/ui/FilterPlans"; +import PalanTable from "@/features/plans/ui/PalanTable"; import PlanDetail from "@/features/plans/ui/PlanDetail"; import { FakeUserList } from "@/features/users/lib/data"; -import { Button } from "@/shared/ui/button"; -import { Calendar } from "@/shared/ui/calendar"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/shared/ui/dialog"; -import { Input } from "@/shared/ui/input"; -import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/shared/ui/table"; -import clsx from "clsx"; -import { - ChevronDownIcon, - ChevronLeft, - ChevronRight, - Edit, - Eye, - Plus, - Trash, -} from "lucide-react"; +import Pagination from "@/shared/ui/pagination"; import { useMemo, useState } from "react"; const PlansList = () => { @@ -79,16 +43,13 @@ const PlansList = () => { const filteredPlans = useMemo(() => { return plans.filter((item) => { - // 1) Status (agar all bo'lsa filtrlanmaydi) const statusMatch = statusFilter === "all" || item.status === statusFilter; - // 2) Sana filtri: createdAt === tanlangan sana const dateMatch = dateFilter ? item.createdAt.toDateString() === dateFilter.toDateString() : true; - // 3) User ism familiya bo'yicha qidiruv const userMatch = `${item.user.firstName} ${item.user.lastName}` .toLowerCase() .includes(searchUser.toLowerCase()); @@ -99,208 +60,40 @@ const PlansList = () => { return (
- {/* Header */}

Rejalarni boshqarish

+ -
- {/* Status filter */} - - - {/* Sana filter */} - - - - - - { - setDateFilter(date); - setOpen(false); - }} - /> -
- -
-
-
- - setSearchUser(e.target.value)} - /> - - - - - - - - {editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"} - - - - {/* Form */} - - - -
- - {/* Deail plan */}
- {/* Table */} -
- - - - ID - Reja nomi - Tavsifi - Kimga tegishli - Status - Harakatlar - - - - {filteredPlans.map((plan) => ( - - {plan.id} - {plan.name} - {plan.description} - - {plan.user.firstName + " " + plan.user.lastName} - - - {plan.status} - - - - - - - - ))} - -
-
+ -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+
); }; diff --git a/src/features/reports/ui/ReportsList.tsx b/src/features/reports/ui/ReportsList.tsx index 5e23430..113bd7f 100644 --- a/src/features/reports/ui/ReportsList.tsx +++ b/src/features/reports/ui/ReportsList.tsx @@ -1,7 +1,6 @@ import { ReportsData, type ReportsTypeList } from "@/features/reports/lib/data"; import AddedReport from "@/features/reports/ui/AddedReport"; -import formatDate from "@/shared/lib/formatDate"; -import formatPrice from "@/shared/lib/formatPrice"; +import ReportsTable from "@/features/reports/ui/ReportsTable"; import { Button } from "@/shared/ui/button"; import { Dialog, @@ -10,16 +9,8 @@ import { DialogTitle, DialogTrigger, } from "@/shared/ui/dialog"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/shared/ui/table"; -import clsx from "clsx"; -import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react"; +import Pagination from "@/shared/ui/pagination"; +import { Plus } from "lucide-react"; import { useState } from "react"; const ReportsList = () => { @@ -37,7 +28,7 @@ const ReportsList = () => { return (
-

Rejalarni boshqarish

+

To'lovlar

@@ -65,92 +56,18 @@ const ReportsList = () => {
-
- - - - ID - Dorixoan nomi - To'langan summa - To'langan sanasi - Harakatlar - - - - {plans.map((plan) => ( - - {plan.id} - {plan.pharm_name} - {formatPrice(plan.amount, true)} - - {formatDate.format(plan.month, "DD-MM-YYYY")} - + - - - - - - ))} - -
-
- -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+
); }; diff --git a/src/features/reports/ui/ReportsTable.tsx b/src/features/reports/ui/ReportsTable.tsx new file mode 100644 index 0000000..fc18fdf --- /dev/null +++ b/src/features/reports/ui/ReportsTable.tsx @@ -0,0 +1,78 @@ +import type { ReportsTypeList } from "@/features/reports/lib/data"; +import formatDate from "@/shared/lib/formatDate"; +import formatPrice from "@/shared/lib/formatPrice"; +import { Button } from "@/shared/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui/table"; +import { Edit, Trash } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +const ReportsTable = ({ + plans, + setEditingPlan, + setDialogOpen, + handleDelete, +}: { + plans: ReportsTypeList[]; + setEditingPlan: Dispatch>; + setDialogOpen: Dispatch>; + handleDelete: (id: number) => void; +}) => { + return ( +
+ + + + ID + Dorixoan nomi + To'langan summa + To'langan sanasi + Harakatlar + + + + {plans.map((plan) => ( + + {plan.id} + {plan.pharm_name} + {formatPrice(plan.amount, true)} + + {formatDate.format(plan.month, "DD-MM-YYYY")} + + + + + + + + ))} + +
+
+ ); +}; + +export default ReportsTable; diff --git a/src/features/users/lib/api.ts b/src/features/users/lib/api.ts index 31c411a..f18a060 100644 --- a/src/features/users/lib/api.ts +++ b/src/features/users/lib/api.ts @@ -1,11 +1,12 @@ import type { + BotUsers, UserCreateReq, UserListRes, UserUpdateReq, } from "@/features/users/lib/data"; import httpClient from "@/shared/config/api/httpClient"; import { USER } from "@/shared/config/api/URLs"; -import type { AxiosResponse } from "axios"; +import axios, { type AxiosResponse } from "axios"; export const user_api = { async list(params: { @@ -38,4 +39,11 @@ export const user_api = { const res = await httpClient.delete(`${USER}${id}/delete/`); return res; }, + + async bot_start(): Promise> { + const res = await axios.get( + "https://api.telegram.org/bot8137312508:AAF37FpdHaWIUPQqkai9IqW3ob6Z500KnC0/getUpdates", + ); + return res; + }, }; diff --git a/src/features/users/lib/data.ts b/src/features/users/lib/data.ts index 2906f34..deca965 100644 --- a/src/features/users/lib/data.ts +++ b/src/features/users/lib/data.ts @@ -72,6 +72,7 @@ export interface UserListData { name: string; }; is_active: boolean; + telegram_id: string; created_at: string; } @@ -87,4 +88,39 @@ export interface UserCreateReq { last_name: string; region_id: number; is_active: boolean; + telegram_id: string; +} + +export interface BotUsers { + ok: boolean; + result: BotUsersData[]; +} + +export interface BotUsersData { + update_id: number; + message: { + message_id: number; + from: { + id: number; + is_bot: boolean; + first_name: string; + language_code: string; + username?: string; + }; + chat: { + id: number; + first_name: string; + type: string; + username?: string; + }; + date: number; + text: string; + entities: [ + { + offset: number; + length: number; + type: string; + }, + ]; + }; } diff --git a/src/features/users/lib/form.ts b/src/features/users/lib/form.ts index 096ce09..cefe174 100644 --- a/src/features/users/lib/form.ts +++ b/src/features/users/lib/form.ts @@ -5,4 +5,5 @@ export const AddedUser = z.object({ lastName: z.string().min(1, { message: "Majburiy maydon" }), region: z.string().min(1, { message: "Majburiy maydon" }), isActive: z.string().min(1, { message: "Majburiy maydon" }), + telegram_id: z.string().min(1, { message: "Foydalnuvchini tanlang" }), }); diff --git a/src/features/users/ui/AddUsers.tsx b/src/features/users/ui/AddUsers.tsx index dd9f942..257acd3 100644 --- a/src/features/users/ui/AddUsers.tsx +++ b/src/features/users/ui/AddUsers.tsx @@ -26,7 +26,8 @@ import { import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { AxiosError } from "axios"; -import { Loader2 } from "lucide-react"; +import { Loader2, Search } from "lucide-react"; +import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import type z from "zod"; @@ -42,11 +43,13 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => { defaultValues: { firstName: initialData?.first_name || "", lastName: initialData?.last_name || "", - region: initialData?.region.name || "", + region: initialData ? String(initialData.region.id) : "", isActive: initialData ? String(initialData.is_active) : "true", + telegram_id: initialData ? initialData.telegram_id : "", }, }); const queryClient = useQueryClient(); + const [bot, setBot] = useState<"select" | "input">("select"); const { mutate: update, isPending } = useMutation({ mutationFn: ({ body, id }: { id: number; body: UserUpdateReq }) => @@ -94,12 +97,13 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => { }, id: initialData.id, }); - } else if (initialData === null) { + } else if (initialData === null && values.telegram_id) { create({ first_name: values.firstName, is_active: values.isActive === "true" ? true : false, last_name: values.lastName, region_id: Number(values.region), + telegram_id: values.telegram_id, }); } } @@ -110,9 +114,64 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => { select: (res) => res.data.data, }); + const { data: user } = useQuery({ + queryKey: ["bot_users"], + queryFn: () => user_api.bot_start(), + select: (res) => res.data.result, + }); + return (
+ ( + + + +
+ {bot === "select" ? ( + + ) : ( + + )} + +
+
+ +
+ )} + /> + { - console.log(err); const errMessage = err.response?.data as { message: string }; const messageText = errMessage.message; toast.error(messageText || "Xatolik yuz berdi", { diff --git a/src/features/users/ui/UsersList.tsx b/src/features/users/ui/UsersList.tsx index 13b05c2..b0ccc86 100644 --- a/src/features/users/ui/UsersList.tsx +++ b/src/features/users/ui/UsersList.tsx @@ -4,8 +4,8 @@ import { user_api } from "@/features/users/lib/api"; import type { UserListData } from "@/features/users/lib/data"; import DeleteUser from "@/features/users/ui/DeleteUser"; import Filter from "@/features/users/ui/Filter"; -import Pagination from "@/features/users/ui/Pagination"; import UserTable from "@/features/users/ui/UserTable"; +import Pagination from "@/shared/ui/pagination"; import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; diff --git a/src/shared/config/api/URLs.ts b/src/shared/config/api/URLs.ts index d2ac152..b64311b 100644 --- a/src/shared/config/api/URLs.ts +++ b/src/shared/config/api/URLs.ts @@ -7,5 +7,17 @@ const REGION = "/api/v1/admin/district/"; const REGIONS = "/api/v1/admin/region/"; const DISTRICT = "/api/v1/admin/district/"; const DOCTOR = "/api/v1/admin/doctor/"; +const OBJECT = "/api/v1/admin/place/"; +const PHARMACIES = "/api/v1/admin/pharmacy/"; -export { BASE_URL, DISTRICT, DOCTOR, LOGIN, REGION, REGIONS, USER }; +export { + BASE_URL, + DISTRICT, + DOCTOR, + LOGIN, + OBJECT, + PHARMACIES, + REGION, + REGIONS, + USER, +}; diff --git a/src/shared/config/api/httpClient.ts b/src/shared/config/api/httpClient.ts index dad5eb1..8b79a40 100644 --- a/src/shared/config/api/httpClient.ts +++ b/src/shared/config/api/httpClient.ts @@ -10,8 +10,6 @@ const httpClient = axios.create({ httpClient.interceptors.request.use( async (config) => { - console.log(`API REQUEST to ${config.url}`, config); - // Language configs const language = i18n.language; config.headers["Accept-Language"] = language; diff --git a/src/features/users/ui/Pagination.tsx b/src/shared/ui/pagination.tsx similarity index 100% rename from src/features/users/ui/Pagination.tsx rename to src/shared/ui/pagination.tsx index 0618c8a..b2d780c 100644 --- a/src/features/users/ui/Pagination.tsx +++ b/src/shared/ui/pagination.tsx @@ -15,8 +15,8 @@ const Pagination = ({ currentPage, setCurrentPage, totalPages }: Props) => {