diff --git a/src/features/auth/lib/api.ts b/src/features/auth/lib/api.ts index 904fd92..207dc44 100644 --- a/src/features/auth/lib/api.ts +++ b/src/features/auth/lib/api.ts @@ -1,5 +1,5 @@ import httpClient from "@/shared/config/api/httpClient"; -import { LOGIN } from "@/shared/config/api/URLs"; +import { API_URLS } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; interface LoginRes { @@ -16,7 +16,7 @@ export const auth_pai = { username: string; password: string; }): Promise> { - const res = await httpClient.post(LOGIN, body); + const res = await httpClient.post(API_URLS.LOGIN, body); return res; }, }; diff --git a/src/features/districts/lib/api.ts b/src/features/districts/lib/api.ts index f6789a3..25c6273 100644 --- a/src/features/districts/lib/api.ts +++ b/src/features/districts/lib/api.ts @@ -1,6 +1,6 @@ import type { DistrictListRes } from "@/features/districts/lib/data"; import httpClient from "@/shared/config/api/httpClient"; -import { DISTRICT } from "@/shared/config/api/URLs"; +import { API_URLS } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; export const discrit_api = { @@ -10,7 +10,7 @@ export const discrit_api = { name?: string; user?: number; }): Promise> { - const res = await httpClient.get(`${DISTRICT}list/`, { params }); + const res = await httpClient.get(`${API_URLS.DISTRICT}list/`, { params }); return res; }, @@ -18,7 +18,7 @@ export const discrit_api = { name: string; user_id: number; }): Promise> { - const res = await httpClient.post(`${DISTRICT}create/`, body); + const res = await httpClient.post(`${API_URLS.DISTRICT}create/`, body); return res; }, @@ -32,12 +32,15 @@ export const discrit_api = { user: number; }; }): Promise> { - const res = await httpClient.patch(`${DISTRICT}${id}/update/`, body); + const res = await httpClient.patch( + `${API_URLS.DISTRICT}${id}/update/`, + body, + ); return res; }, async delete(id: number): Promise> { - const res = await httpClient.delete(`${DISTRICT}${id}/delete/`); + const res = await httpClient.delete(`${API_URLS.DISTRICT}${id}/delete/`); return res; }, }; diff --git a/src/features/doctors/lib/api.ts b/src/features/doctors/lib/api.ts index 69005db..e980426 100644 --- a/src/features/doctors/lib/api.ts +++ b/src/features/doctors/lib/api.ts @@ -4,7 +4,7 @@ import type { UpdateDoctorReq, } from "@/features/doctors/lib/data"; import httpClient from "@/shared/config/api/httpClient"; -import { DOCTOR } from "@/shared/config/api/URLs"; +import { API_URLS } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; export const doctor_api = { @@ -18,22 +18,22 @@ export const doctor_api = { sphere?: string; user?: string; }): Promise> { - const res = await httpClient.get(`${DOCTOR}list/`, { params }); + const res = await httpClient.get(`${API_URLS.DOCTOR}list/`, { params }); return res; }, async create(body: CreateDoctorReq) { - const res = await httpClient.post(`${DOCTOR}create/`, body); + const res = await httpClient.post(`${API_URLS.DOCTOR}create/`, body); return res; }, async update({ body, id }: { id: number; body: UpdateDoctorReq }) { - const res = await httpClient.patch(`${DOCTOR}${id}/update/`, body); + const res = await httpClient.patch(`${API_URLS.DOCTOR}${id}/update/`, body); return res; }, async delete(id: number) { - const res = await httpClient.delete(`${DOCTOR}${id}/delete/`); + const res = await httpClient.delete(`${API_URLS.DOCTOR}${id}/delete/`); return res; }, }; diff --git a/src/features/location/lib/api.ts b/src/features/location/lib/api.ts new file mode 100644 index 0000000..1b0cccf --- /dev/null +++ b/src/features/location/lib/api.ts @@ -0,0 +1,40 @@ +import type { LocationListRes } from "@/features/location/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { API_URLS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const location_api = { + async list(params: { + limit?: number; + offset?: number; + date?: string; + user?: string; + }): Promise> { + const res = await httpClient.get(`${API_URLS.LOCATION}list/`, { params }); + return res; + }, + + async delete(id: number) { + const res = await httpClient.delete(`${API_URLS.LOCATION}${id}/delete/`); + return res; + }, + + async list_user_location(params: { + limit?: number; + offset?: number; + date?: string; + user?: string; + }): Promise> { + const res = await httpClient.get(`${API_URLS.USER_LOCATION}list/`, { + params, + }); + return res; + }, + + async list_user_location_delete(id: number) { + const res = await httpClient.delete( + `${API_URLS.USER_LOCATION}${id}/delete/`, + ); + return res; + }, +}; diff --git a/src/features/location/lib/data.ts b/src/features/location/lib/data.ts index 985d8c0..64fcb16 100644 --- a/src/features/location/lib/data.ts +++ b/src/features/location/lib/data.ts @@ -75,3 +75,51 @@ export const LocationFakeData: LocationListType[] = [ createdAt: new Date("2025-02-01T10:15:00"), }, ]; + +export interface LocationListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: null | string; + previous: null | string; + results: LocationListDataRes[]; + }; +} + +export interface LocationListDataRes { + id: number; + longitude: number; + latitude: number; + created_at: string; + district: { + id: number; + name: string; + }; + place: { + id: number; + name: string; + longitude: number; + latitude: number; + }; + doctor: { + id: number; + first_name: string; + last_name: string; + longitude: number; + latitude: number; + }; + pharmacy: { + id: number; + name: string; + longitude: number; + latitude: number; + }; + user: { + id: number; + first_name: string; + last_name: string; + }; + updated_at: string; +} diff --git a/src/features/location/ui/DeleteLocation.tsx b/src/features/location/ui/DeleteLocation.tsx new file mode 100644 index 0000000..cb661cb --- /dev/null +++ b/src/features/location/ui/DeleteLocation.tsx @@ -0,0 +1,114 @@ +import { location_api } from "@/features/location/lib/api"; +import type { LocationListDataRes } from "@/features/location/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, SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + opneDelete: boolean; + setOpenDelete: Dispatch>; + setLocationDelete: Dispatch>; + locationDelete: LocationListDataRes | null; + viewLocation: "user_send" | "user_send_object"; +} +const DeleteLocation = ({ + opneDelete, + viewLocation, + locationDelete, + setOpenDelete, + setLocationDelete, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate: deleteUser, isPending } = useMutation({ + mutationFn: (id: number) => location_api.delete(id), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["location_list"] }); + toast.success(`Jo'natilgan lokatsiya o'chirildi`); + setOpenDelete(false); + setLocationDelete(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", + }); + }, + }); + + const { mutate: deleteUserLocation, isPending: deleteUserLocationPen } = + useMutation({ + mutationFn: (id: number) => location_api.list_user_location_delete(id), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["user_location_list"] }); + toast.success(`Jo'natilgan lokatsiya o'chirildi`); + setOpenDelete(false); + setLocationDelete(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 ( + + + + Dorini o'chirish + + Siz rostan ham jo'natilgan lokatsiyani o'chirmoqchimisiz + + + + + + + + + ); +}; + +export default DeleteLocation; diff --git a/src/features/location/ui/LocationDetailDialog.tsx b/src/features/location/ui/LocationDetailDialog.tsx index 99fa6d9..100f25c 100644 --- a/src/features/location/ui/LocationDetailDialog.tsx +++ b/src/features/location/ui/LocationDetailDialog.tsx @@ -1,4 +1,4 @@ -import type { LocationListType } from "@/features/location/lib/data"; +import type { LocationListDataRes } from "@/features/location/lib/data"; import formatDate from "@/shared/lib/formatDate"; import { Dialog, @@ -7,26 +7,100 @@ import { DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; -import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; +import { Circle, Map, Placemark, Polygon, YMaps } from "@pbe/react-yandex-maps"; import { useEffect, useState, type Dispatch, type SetStateAction } from "react"; interface Props { detail: boolean; setDetail: Dispatch>; - object: LocationListType | null; + object: LocationListDataRes | null; +} + +interface CoordsData { + lat: number; + lon: number; + polygon: [number, number][][]; } const LocationDetailDialog = ({ detail, object, setDetail }: Props) => { const [circle, setCircle] = useState([""]); + const [coords, setCoords] = useState<[number, number]>([ + 41.311081, 69.240562, + ]); + + const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]); + + 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 && object.object) { - setCircle([object.object.lat, object.object.long]); - } else if (object && object.pharmcies) { - setCircle([object.pharmcies.lat, object.pharmcies.long]); + if (!object) return; + + const load = async () => { + if (object.district) { + const district = await getCoords(object.district.name); + + if (district) { + setPolygonCoords(district.polygon); + } + } else { + setPolygonCoords([]); + } + + setCoords([Number(object.latitude), Number(object.longitude)]); + }; + + load(); + }, [object]); + + useEffect(() => { + if (object && object.place) { + setCircle([ + object.place.latitude.toString(), + object.place.longitude.toString(), + ]); + } else if (object && object.pharmacy) { + setCircle([ + object.pharmacy.latitude.toString(), + object.pharmacy.longitude.toString(), + ]); } else if (object && object.doctor) { - setCircle([object.doctor.lat, object.doctor.long]); + setCircle([ + object.doctor.latitude.toString(), + object.doctor.longitude.toString(), + ]); } else { setCircle(undefined); } @@ -49,13 +123,13 @@ const LocationDetailDialog = ({ detail, object, setDetail }: Props) => { Jo'natgan foydalanvchi:

- {object.user.firstName} {object.user.lastName} + {object.user.first_name} {object.user.last_name}

Jo'natgan vaqti:

- {formatDate.format(object.createdAt, "DD-MM-YYYY")} + {formatDate.format(object.created_at, "DD-MM-YYYY")}

{object.district && ( @@ -69,15 +143,16 @@ const LocationDetailDialog = ({ detail, object, setDetail }: Props) => { - + {/* Marking user location */} + + + {/* Circle around user */} {circle && ( { }} /> )} + + {/* District polygon */} + {polygonCoords.length > 0 && ( + + )} diff --git a/src/features/location/ui/LocationFilter.tsx b/src/features/location/ui/LocationFilter.tsx index 1d46cc6..1e090d9 100644 --- a/src/features/location/ui/LocationFilter.tsx +++ b/src/features/location/ui/LocationFilter.tsx @@ -2,6 +2,13 @@ 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 { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/ui/select"; import { ChevronDownIcon } from "lucide-react"; import type { Dispatch, SetStateAction } from "react"; @@ -12,6 +19,8 @@ interface Props { setDateFilter: Dispatch>; searchUser: string; setSearchUser: Dispatch>; + viewLocation: "user_send" | "user_send_object"; + setViewLocation: Dispatch>; } const LocationFilter = ({ @@ -21,9 +30,28 @@ const LocationFilter = ({ setDateFilter, searchUser, setSearchUser, + viewLocation, + setViewLocation, }: Props) => { return (
+ + diff --git a/src/features/location/ui/UserLocationTable.tsx b/src/features/location/ui/UserLocationTable.tsx new file mode 100644 index 0000000..7f85abe --- /dev/null +++ b/src/features/location/ui/UserLocationTable.tsx @@ -0,0 +1,92 @@ +import type { LocationListDataRes } 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: LocationListDataRes[] | []; + setDetail: Dispatch>; + setDetailDialog: Dispatch>; + handleDelete: (id: LocationListDataRes) => void; +} + +const UserLocationTable = ({ + filtered, + setDetail, + setDetailDialog, + handleDelete, +}: Props) => { + return ( +
+ + + + # + Jo'natgan foydalanuvchi + Jo'natgan vaqti + Qayerdan jo'natdi + Amallar + + + + {filtered.map((item, index) => ( + + {index + 1} + + {item.user.first_name} {item.user.last_name} + + + {formatDate.format(item.created_at, "DD-MM-YYYY")} + + + + {item.district + ? "Tuman" + : item.place + ? "Obyekt" + : item.doctor + ? "Shifokor" + : item.pharmacy + ? "Dorixona" + : "Turgan joyidan"} + + + + + + + + ))} + +
+
+ ); +}; + +export default UserLocationTable; diff --git a/src/features/objects/lib/api.ts b/src/features/objects/lib/api.ts index 85b5d79..9f96669 100644 --- a/src/features/objects/lib/api.ts +++ b/src/features/objects/lib/api.ts @@ -4,7 +4,7 @@ import type { ObjectUpdate, } from "@/features/objects/lib/data"; import httpClient from "@/shared/config/api/httpClient"; -import { OBJECT } from "@/shared/config/api/URLs"; +import { API_URLS } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; export const object_api = { @@ -15,22 +15,22 @@ export const object_api = { district?: string; user?: string; }): Promise> { - const res = await httpClient.get(`${OBJECT}list/`, { params }); + const res = await httpClient.get(`${API_URLS.OBJECT}list/`, { params }); return res; }, async create(body: ObjectCreate) { - const res = await httpClient.post(`${OBJECT}create/`, body); + const res = await httpClient.post(`${API_URLS.OBJECT}create/`, body); return res; }, async update({ body, id }: { id: number; body: ObjectUpdate }) { - const res = await httpClient.patch(`${OBJECT}${id}/update/`, body); + const res = await httpClient.patch(`${API_URLS.OBJECT}${id}/update/`, body); return res; }, async delete(id: number) { - const res = await httpClient.delete(`${OBJECT}${id}/delete/`); + const res = await httpClient.delete(`${API_URLS.OBJECT}${id}/delete/`); return res; }, }; diff --git a/src/features/pharm/lib/api.ts b/src/features/pharm/lib/api.ts new file mode 100644 index 0000000..0d3b5a7 --- /dev/null +++ b/src/features/pharm/lib/api.ts @@ -0,0 +1,33 @@ +import type { FactoryCreate, FactoryListRes } from "@/features/pharm/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { API_URLS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const factory_api = { + async list(params: { + limit?: number; + offset?: number; + name?: string; + }): Promise> { + const res = await httpClient.get(`${API_URLS.FACTORY}list/`, { params }); + return res; + }, + + async create(body: FactoryCreate) { + const res = await httpClient.post(`${API_URLS.FACTORY}create/`, body); + return res; + }, + + async update({ id, body }: { id: number; body: FactoryCreate }) { + const res = await httpClient.patch( + `${API_URLS.FACTORY}${id}/update/`, + body, + ); + return res; + }, + + async delete(id: number) { + const res = await httpClient.delete(`${API_URLS.FACTORY}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/pharm/lib/data.ts b/src/features/pharm/lib/data.ts index 9dc1e98..238d56f 100644 --- a/src/features/pharm/lib/data.ts +++ b/src/features/pharm/lib/data.ts @@ -9,3 +9,25 @@ export const pharmData: PharmType[] = [ name: "Meridyn", }, ]; + +export interface FactoryListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: null | string; + previous: null | string; + results: FactoryListDataRes[]; + }; +} + +export interface FactoryListDataRes { + id: number; + name: string; + created_at: string; +} + +export interface FactoryCreate { + name: string; +} diff --git a/src/features/pharm/ui/AddedPharm.tsx b/src/features/pharm/ui/AddedPharm.tsx index 90af041..738ccb4 100644 --- a/src/features/pharm/ui/AddedPharm.tsx +++ b/src/features/pharm/ui/AddedPharm.tsx @@ -1,4 +1,5 @@ -import type { PharmType } from "@/features/pharm/lib/data"; +import { factory_api } from "@/features/pharm/lib/api"; +import type { FactoryCreate, PharmType } from "@/features/pharm/lib/data"; import { pharmForm } from "@/features/pharm/lib/form"; import { Button } from "@/shared/ui/button"; import { @@ -11,19 +12,21 @@ import { import { Input } from "@/shared/ui/input"; import { Label } from "@/shared/ui/label"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; import { Loader2 } from "lucide-react"; -import { useState, type Dispatch, type SetStateAction } from "react"; +import { type Dispatch, type SetStateAction } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import type z from "zod"; interface Props { initialValues: PharmType | null; setDialogOpen: Dispatch>; - setPlans: Dispatch>; } -const AddedPharm = ({ initialValues, setDialogOpen, setPlans }: Props) => { - const [load, setLoad] = useState(false); +const AddedPharm = ({ initialValues, setDialogOpen }: Props) => { + const queryClient = useQueryClient(); const form = useForm>({ resolver: zodResolver(pharmForm), defaultValues: { @@ -31,35 +34,51 @@ const AddedPharm = ({ initialValues, setDialogOpen, setPlans }: Props) => { }, }); + const { mutate, isPending } = useMutation({ + mutationFn: (body: FactoryCreate) => factory_api.create(body), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["factory_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: update, isPending: updatePending } = useMutation({ + mutationFn: ({ id, body }: { id: number; body: FactoryCreate }) => + factory_api.update({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["factory_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(data: z.infer) { - setLoad(true); - if (initialValues) { - setTimeout(() => { - setPlans((prev) => - prev.map((plan) => - plan.id === initialValues.id - ? { - ...plan, - ...data, - } - : plan, - ), - ); - setLoad(false); - setDialogOpen(false); - }, 2000); + if (!initialValues) { + mutate({ + name: data.name, + }); } else { - setTimeout(() => { - setPlans((prev) => [ - ...prev, - { - id: prev.length ? prev[prev.length - 1].id + 1 : 1, - name: data.name, - }, - ]); - setLoad(false); - setDialogOpen(false); - }, 2000); + update({ + body: { + name: data.name, + }, + id: initialValues.id, + }); } } @@ -87,9 +106,9 @@ const AddedPharm = ({ initialValues, setDialogOpen, setPlans }: Props) => { + + + + + ); +}; + +export default DeletePharm; diff --git a/src/features/pharm/ui/PharmList.tsx b/src/features/pharm/ui/PharmList.tsx index ba9f720..3b4b89b 100644 --- a/src/features/pharm/ui/PharmList.tsx +++ b/src/features/pharm/ui/PharmList.tsx @@ -1,5 +1,10 @@ -import { pharmData, type PharmType } from "@/features/pharm/lib/data"; +import { factory_api } from "@/features/pharm/lib/api"; +import { + type FactoryListDataRes, + type PharmType, +} from "@/features/pharm/lib/data"; import AddedPharm from "@/features/pharm/ui/AddedPharm"; +import DeletePharm from "@/features/pharm/ui/DeletePharm"; import { Button } from "@/shared/ui/button"; import { Dialog, @@ -9,6 +14,7 @@ import { DialogTrigger, } from "@/shared/ui/dialog"; import { Input } from "@/shared/ui/input"; +import Pagination from "@/shared/ui/pagination"; import { Table, TableBody, @@ -17,34 +23,39 @@ import { TableHeader, TableRow, } from "@/shared/ui/table"; -import clsx from "clsx"; -import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react"; -import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Edit, Loader2, Plus, Trash } from "lucide-react"; +import { useState } from "react"; const PharmList = () => { const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; - const [plans, setPlans] = useState(pharmData); + const [nameFilter, setNameFilter] = useState(""); + const limit = 20; + const { data, isLoading, isError } = useQuery({ + queryKey: ["factory_list", currentPage, nameFilter], + queryFn: () => + factory_api.list({ + limit, + offset: (currentPage - 1) * limit, + name: nameFilter, + }), + select(data) { + return data.data.data; + }, + }); + const totalPages = data ? Math.ceil(data?.count / limit) : 1; const [editingPlan, setEditingPlan] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); - const [nameFilter, setNameFilter] = useState(""); + const [openDelete, setOpenDelete] = useState(false); + const [pillDelete, setPillDelete] = useState(null); - const handleDelete = (id: number) => { - setPlans(plans.filter((p) => p.id !== id)); + const handleDelete = (id: FactoryListDataRes) => { + setOpenDelete(true); + setPillDelete(id); }; - const filteredPlans = useMemo(() => { - return plans.filter((item) => { - const statusMatch = item.name - .toLowerCase() - .includes(nameFilter.toLowerCase()); - - return statusMatch; - }); - }, [plans, nameFilter]); - return (
@@ -80,7 +91,6 @@ const PharmList = () => { @@ -88,84 +98,83 @@ const PharmList = () => {
- - - - ID - Nomi - Amallar - - - - {filteredPlans.map((plan) => ( - - {plan.id} - {plan.name} - - - - + {isLoading && ( +
+ + + +
+ )} + + {isError && ( +
+ + Ma'lumotlarni olishda xatolik yuz berdi. + +
+ )} + {!isLoading && !isError && ( +
+ + + ID + Nomi + Amallar - ))} - -
+ + + {data && data.results.length > 0 ? ( + data?.results.map((plan) => ( + + {plan.id} + {plan.name} + + + + + + )) + ) : ( + + + Farmasevtika topilmadi. + + + )} + + + )}
-
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+ + +
); }; diff --git a/src/features/pharmacies/lib/api.ts b/src/features/pharmacies/lib/api.ts index d704dcd..79de765 100644 --- a/src/features/pharmacies/lib/api.ts +++ b/src/features/pharmacies/lib/api.ts @@ -4,7 +4,7 @@ import type { UpdatePharmaciesReq, } from "@/features/pharmacies/lib/data"; import httpClient from "@/shared/config/api/httpClient"; -import { PHARMACIES } from "@/shared/config/api/URLs"; +import { API_URLS } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; export const pharmacies_api = { @@ -16,22 +16,25 @@ export const pharmacies_api = { district?: string; user?: string; }): Promise> { - const res = await httpClient.get(`${PHARMACIES}list/`, { params }); + const res = await httpClient.get(`${API_URLS.PHARMACIES}list/`, { params }); return res; }, async create(body: CreatePharmaciesReq) { - const res = await httpClient.post(`${PHARMACIES}create/`, body); + const res = await httpClient.post(`${API_URLS.PHARMACIES}create/`, body); return res; }, async update({ body, id }: { id: number; body: UpdatePharmaciesReq }) { - const res = await httpClient.patch(`${PHARMACIES}${id}/update/`, body); + const res = await httpClient.patch( + `${API_URLS.PHARMACIES}${id}/update/`, + body, + ); return res; }, async delete(id: number) { - const res = await httpClient.delete(`${PHARMACIES}${id}/delete/`); + const res = await httpClient.delete(`${API_URLS.PHARMACIES}${id}/delete/`); return res; }, }; diff --git a/src/features/pill/lib/api.ts b/src/features/pill/lib/api.ts new file mode 100644 index 0000000..43e491f --- /dev/null +++ b/src/features/pill/lib/api.ts @@ -0,0 +1,30 @@ +import type { PillCreateReq, PillListRes } from "@/features/pill/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { API_URLS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const pill_api = { + async list(params: { + limit?: number; + offset?: number; + name?: string; + }): Promise> { + const res = await httpClient.get(`${API_URLS.PILL}list/`, { params }); + return res; + }, + + async added(body: PillCreateReq) { + const res = httpClient.post(`${API_URLS.PILL}create/`, body); + return res; + }, + + async update({ body, id }: { id: number; body: PillCreateReq }) { + const res = httpClient.patch(`${API_URLS.PILL}${id}/update/`, body); + return res; + }, + + async delete(id: number) { + const res = httpClient.delete(`${API_URLS.PILL}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/pill/lib/data.ts b/src/features/pill/lib/data.ts index 82bb854..4abe562 100644 --- a/src/features/pill/lib/data.ts +++ b/src/features/pill/lib/data.ts @@ -16,3 +16,27 @@ export const FakePills: PillType[] = [ { id: 9, name: "Amoxicillin 500mg", price: "28000" }, { id: 10, name: "Immuno Plus", price: "30000" }, ]; + +export interface PillListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: null | string; + previous: null | string; + results: PillListData[]; + }; +} + +export interface PillListData { + id: number; + name: string; + price: string; + created_at: string; +} + +export interface PillCreateReq { + name: string; + price: string; +} diff --git a/src/features/pill/ui/AddedPill.tsx b/src/features/pill/ui/AddedPill.tsx index e07fea2..062150e 100644 --- a/src/features/pill/ui/AddedPill.tsx +++ b/src/features/pill/ui/AddedPill.tsx @@ -1,4 +1,5 @@ -import type { PillType } from "@/features/pill/lib/data"; +import { pill_api } from "@/features/pill/lib/api"; +import type { PillCreateReq, PillType } from "@/features/pill/lib/data"; import { createPillFormData } from "@/features/pill/lib/form"; import formatPrice from "@/shared/lib/formatPrice"; import { Button } from "@/shared/ui/button"; @@ -12,20 +13,22 @@ import { import { Input } from "@/shared/ui/input"; import { Label } from "@/shared/ui/label"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; import { 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: PillType | null; setDialogOpen: Dispatch>; - setPlans: Dispatch>; } -const AddedPill = ({ initialValues, setDialogOpen, setPlans }: Props) => { - const [load, setLoad] = useState(false); +const AddedPill = ({ initialValues, setDialogOpen }: Props) => { const [displayPrice, setDisplayPrice] = useState(""); + const queryClient = useQueryClient(); const form = useForm>({ resolver: zodResolver(createPillFormData), defaultValues: { @@ -34,6 +37,44 @@ const AddedPill = ({ initialValues, setDialogOpen, setPlans }: Props) => { }, }); + const { mutate: added, isPending: addedPending } = useMutation({ + mutationFn: (body: PillCreateReq) => { + return pill_api.added(body); + }, + onSuccess: () => { + toast.success("Dori qo'shildi"); + setDialogOpen(false); + queryClient.resetQueries({ queryKey: ["pill_list"] }); + }, + 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: PillCreateReq }) => { + return pill_api.update({ body, id }); + }, + onSuccess: () => { + toast.success("Dori yangilandi"); + queryClient.resetQueries({ queryKey: ["pill_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", + }); + }, + }); + useEffect(() => { if (initialValues) { setDisplayPrice(formatPrice(initialValues.price)); @@ -41,35 +82,19 @@ const AddedPill = ({ initialValues, setDialogOpen, setPlans }: Props) => { }, [initialValues]); function onSubmit(data: z.infer) { - setLoad(true); if (initialValues) { - setTimeout(() => { - setPlans((prev) => - prev.map((plan) => - plan.id === initialValues.id - ? { - ...plan, - ...data, - } - : plan, - ), - ); - setLoad(false); - setDialogOpen(false); - }, 2000); + edit({ + id: initialValues.id, + body: { + name: data.name, + price: data.price, + }, + }); } else { - setTimeout(() => { - setPlans((prev) => [ - ...prev, - { - id: prev.length ? prev[prev.length - 1].id + 1 : 1, - name: data.name, - price: data.price, - }, - ]); - setLoad(false); - setDialogOpen(false); - }, 2000); + added({ + name: data.name, + price: data.price, + }); } } @@ -125,9 +150,9 @@ const AddedPill = ({ initialValues, setDialogOpen, setPlans }: Props) => { + + + + + ); +}; + +export default DeletePill; diff --git a/src/features/pill/ui/PillList.tsx b/src/features/pill/ui/PillList.tsx index 10f37be..12d5a01 100644 --- a/src/features/pill/ui/PillList.tsx +++ b/src/features/pill/ui/PillList.tsx @@ -1,5 +1,7 @@ -import { FakePills, type PillType } from "@/features/pill/lib/data"; +import { pill_api } from "@/features/pill/lib/api"; +import { type PillListData, type PillType } from "@/features/pill/lib/data"; import AddedPill from "@/features/pill/ui/AddedPill"; +import DeletePill from "@/features/pill/ui/DeletePill"; import formatPrice from "@/shared/lib/formatPrice"; import { Button } from "@/shared/ui/button"; import { @@ -10,6 +12,7 @@ import { DialogTrigger, } from "@/shared/ui/dialog"; import { Input } from "@/shared/ui/input"; +import Pagination from "@/shared/ui/pagination"; import { Table, TableBody, @@ -18,34 +21,41 @@ import { TableHeader, TableRow, } from "@/shared/ui/table"; -import clsx from "clsx"; -import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react"; -import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Edit, Plus, Trash } from "lucide-react"; +import { useState } from "react"; const PillList = () => { const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; - const [plans, setPlans] = useState(FakePills); + const limit = 20; + const [nameFilter, setNameFilter] = useState(""); + + const { data } = useQuery({ + queryKey: ["pill_list", nameFilter, currentPage], + queryFn: () => + pill_api.list({ + limit, + offset: (currentPage - 1) * limit, + name: nameFilter, + }), + select(data) { + return data.data.data; + }, + }); + + const totalPages = data ? Math.ceil(data.count / limit) : 1; const [editingPlan, setEditingPlan] = useState(null); const [dialogOpen, setDialogOpen] = useState(false); - const [nameFilter, setNameFilter] = useState(""); + const [openDelete, setOpenDelete] = useState(false); + const [pillDelete, setPillDelete] = useState(null); - const handleDelete = (id: number) => { - setPlans(plans.filter((p) => p.id !== id)); + const handleDelete = (id: PillListData) => { + setOpenDelete(true); + setPillDelete(id); }; - const filteredPlans = useMemo(() => { - return plans.filter((item) => { - const statusMatch = item.name - .toLowerCase() - .includes(nameFilter.toLowerCase()); - - return statusMatch; - }); - }, [plans, nameFilter]); - return (
@@ -79,7 +89,6 @@ const PillList = () => { @@ -97,7 +106,7 @@ const PillList = () => { - {filteredPlans.map((plan) => ( + {data?.results.map((plan) => ( {plan.id} {plan.name} @@ -118,7 +127,7 @@ const PillList = () => { variant="destructive" size="sm" className="cursor-pointer" - onClick={() => handleDelete(plan.id)} + onClick={() => handleDelete(plan)} > @@ -129,44 +138,18 @@ const PillList = () => {
-
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+ + +
); }; diff --git a/src/features/plans/lib/api.ts b/src/features/plans/lib/api.ts new file mode 100644 index 0000000..88ba6b3 --- /dev/null +++ b/src/features/plans/lib/api.ts @@ -0,0 +1,36 @@ +import type { + PlanCreateReq, + PlanListRes, + PlanUpdateReq, +} from "@/features/plans/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { API_URLS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const plans_api = { + async list(params: { + limit?: number; + offset?: number; + status?: boolean; + date?: string; + user?: string; + }): Promise> { + const res = await httpClient.get(`${API_URLS.PLANS}list/`, { params }); + return res; + }, + + async create(body: PlanCreateReq) { + const res = await httpClient.post(`${API_URLS.PLANS}create/`, body); + return res; + }, + + async update({ body, id }: { id: number; body: PlanUpdateReq }) { + const res = await httpClient.patch(`${API_URLS.PLANS}${id}/update/`, body); + return res; + }, + + async delete(id: number) { + const res = await httpClient.delete(`${API_URLS.PLANS}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/plans/lib/data.ts b/src/features/plans/lib/data.ts index ad1819c..1f8ac74 100644 --- a/src/features/plans/lib/data.ts +++ b/src/features/plans/lib/data.ts @@ -8,3 +8,42 @@ export interface Plan { status: "Bajarildi" | "Bajarilmagan"; createdAt: Date; } + +export interface PlanListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: null | string; + previous: null | string; + results: PlanListData[]; + }; +} + +export interface PlanListData { + id: number; + title: string; + description: string; + date: string; + user: { + id: number; + first_name: string; + last_name: string; + }; + is_done: true; + created_at: string; +} + +export interface PlanCreateReq { + title: string; + description: string; + date: string; + user_id: number; +} + +export interface PlanUpdateReq { + title: string; + description: string; + date: string; +} diff --git a/src/features/plans/lib/form.ts b/src/features/plans/lib/form.ts index 3516d9b..48c38ef 100644 --- a/src/features/plans/lib/form.ts +++ b/src/features/plans/lib/form.ts @@ -4,4 +4,5 @@ export const createPlanFormData = z.object({ name: z.string().min(1, { message: "Majburiy maydon" }), description: z.string().min(1, { message: "Majburiy maydon" }), user: z.string().min(1, { message: "Majburiy maydon" }), + date: z.string().min(1, { message: "Majburiy maydon" }), }); diff --git a/src/features/plans/ui/AddedPlan.tsx b/src/features/plans/ui/AddedPlan.tsx index 6d8958f..4ce1008 100644 --- a/src/features/plans/ui/AddedPlan.tsx +++ b/src/features/plans/ui/AddedPlan.tsx @@ -1,7 +1,23 @@ -import type { Plan } from "@/features/plans/lib/data"; +import { plans_api } from "@/features/plans/lib/api"; +import type { + PlanCreateReq, + PlanListData, + PlanUpdateReq, +} from "@/features/plans/lib/data"; import { createPlanFormData } from "@/features/plans/lib/form"; -import { FakeUserList } from "@/features/users/lib/data"; +import { user_api } from "@/features/users/lib/api"; +import formatDate from "@/shared/lib/formatDate"; +import { cn } from "@/shared/lib/utils"; import { Button } from "@/shared/ui/button"; +import { Calendar } from "@/shared/ui/calendar"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/shared/ui/command"; import { Form, FormControl, @@ -11,71 +27,110 @@ 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 { Loader2 } from "lucide-react"; -import React, { useState } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Check, ChevronDownIcon, ChevronsUpDown, Loader2 } from "lucide-react"; +import { useState } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import z from "zod"; interface Props { - initialValues?: Plan | null; + initialValues?: PlanListData | null; setDialogOpen: (open: boolean) => void; - setPlans: React.Dispatch>; } -const AddedPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => { - const [load, setLoad] = useState(false); +const AddedPlan = ({ initialValues, setDialogOpen }: Props) => { const form = useForm>({ resolver: zodResolver(createPlanFormData), defaultValues: { - name: initialValues?.name || "", + name: initialValues?.title || "", description: initialValues?.description || "", user: initialValues ? String(initialValues.user.id) : "", + date: initialValues ? initialValues?.date : "", + }, + }); + const [searchUser, setSearchUser] = useState(""); + const [openUser, setOpenUser] = useState(false); + const queryClient = useQueryClient(); + const [open, setOpen] = useState(false); + + 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, + }; + + return user_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const { mutate, isPending } = useMutation({ + mutationFn: (body: PlanCreateReq) => plans_api.create(body), + onSuccess: () => { + setDialogOpen(false); + toast.success("Reja qo'shildi"); + queryClient.refetchQueries({ queryKey: ["plan_list"] }); + }, + 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: PlanUpdateReq; id: number }) => + plans_api.update({ body, id }), + onSuccess: () => { + setDialogOpen(false); + toast.success("Reja tahrirlandi"); + queryClient.refetchQueries({ queryKey: ["plan_list"] }); + }, + onError: (err: AxiosError) => { + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); }, }); function onSubmit(data: z.infer) { - setLoad(true); if (initialValues) { - setTimeout(() => { - setPlans((prev) => - prev.map((plan) => - plan.id === initialValues.id - ? { - ...plan, - ...data, - user: FakeUserList.find((u) => u.id === Number(data.user))!, // user obyekt - } - : plan, - ), - ); - setLoad(false); - setDialogOpen(false); - }, 2000); + edit({ + id: initialValues.id, + body: { + date: formatDate.format(data.date, "YYYY-MM-DD"), + description: data.description, + title: data.name, + }, + }); } else { - setTimeout(() => { - setPlans((prev) => [ - ...prev, - { - id: prev.length ? prev[prev.length - 1].id + 1 : 1, - name: data.name, - description: data.description, - user: FakeUserList.find((u) => u.id === Number(data.user))!, // user obyekt - status: "Bajarilmagan", - createdAt: new Date(), - }, - ]); - setLoad(false); - setDialogOpen(false); - }, 2000); + mutate({ + date: formatDate.format(data.date, "YYYY-MM-DD"), + description: data.description, + title: data.name, + user_id: Number(data.user), + }); } } @@ -86,26 +141,87 @@ const AddedPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => { ( - - - - - - - - )} + 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 + )} +
+
+
+
+ + +
+ ); + }} /> { )} /> + ( + + + + + + + + + + { + if (value) { + field.onChange(value.toISOString()); // ⬅️ forma ichiga yozamiz + } + setOpen(false); + }} + /> + + + + + + )} + /> + + + + + + ); +}; + +export default DeletePlan; diff --git a/src/features/plans/ui/FilterPlans.tsx b/src/features/plans/ui/FilterPlans.tsx index b6b2bdf..2ed4b91 100644 --- a/src/features/plans/ui/FilterPlans.tsx +++ b/src/features/plans/ui/FilterPlans.tsx @@ -1,4 +1,4 @@ -import type { Plan } from "@/features/plans/lib/data"; +import type { PlanListData } from "@/features/plans/lib/data"; import AddedPlan from "@/features/plans/ui/AddedPlan"; import { Button } from "@/shared/ui/button"; import { Calendar } from "@/shared/ui/calendar"; @@ -32,9 +32,8 @@ interface Props { setSearchUser: Dispatch>; dialogOpen: boolean; setDialogOpen: Dispatch>; - editingPlan: Plan | null; - setEditingPlan: Dispatch>; - setPlans: Dispatch>; + editingPlan: PlanListData | null; + setEditingPlan: Dispatch>; } const FilterPlans = ({ @@ -50,7 +49,6 @@ const FilterPlans = ({ setDialogOpen, setEditingPlan, editingPlan, - setPlans, }: Props) => { return (
@@ -60,8 +58,8 @@ const FilterPlans = ({ Barchasi - Bajarildi - Bajarilmagan + Bajarildi + Bajarilmagan @@ -128,7 +126,6 @@ const FilterPlans = ({ diff --git a/src/features/plans/ui/PalanTable.tsx b/src/features/plans/ui/PalanTable.tsx index d315588..475ae8b 100644 --- a/src/features/plans/ui/PalanTable.tsx +++ b/src/features/plans/ui/PalanTable.tsx @@ -1,4 +1,4 @@ -import type { Plan } from "@/features/plans/lib/data"; +import type { PlanListData } from "@/features/plans/lib/data"; import { Button } from "@/shared/ui/button"; import { Table, @@ -9,91 +9,115 @@ import { TableRow, } from "@/shared/ui/table"; import clsx from "clsx"; -import { Edit, Eye, Trash } from "lucide-react"; +import { Edit, Eye, Loader2, Trash } from "lucide-react"; import type { Dispatch, SetStateAction } from "react"; interface Props { - filteredPlans: Plan[]; - setEditingPlan: Dispatch>; + filteredPlans: PlanListData[] | []; + setEditingPlan: Dispatch>; setDetail: Dispatch>; setDialogOpen: Dispatch>; - handleDelete: (id: number) => void; + handleDelete: (id: PlanListData) => void; + isLoading: boolean; + isError: boolean; + isFetching: boolean; } const PalanTable = ({ filteredPlans, setEditingPlan, setDetail, + isError, + isFetching, + isLoading, 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} - - - - - - + {(isLoading || isFetching) && ( +
+ + + +
+ )} + + {isError && ( +
+ + Ma'lumotlarni olishda xatolik yuz berdi. + +
+ )} + + {!isLoading && !isError && ( +
+ + + ID + Reja nomi + Tavsifi + Kimga tegishli + Status + Harakatlar - ))} - -
+ + + {filteredPlans.map((plan) => ( + + {plan.id} + {plan.title} + {plan.description} + + {plan.user.first_name + " " + plan.user.last_name} + + + {plan.is_done ? "Bajarilgan" : "Bajarilmagan"} + + + + + + + + ))} + + + )}
); }; diff --git a/src/features/plans/ui/PlanDetail.tsx b/src/features/plans/ui/PlanDetail.tsx index cd60efe..146ea3c 100644 --- a/src/features/plans/ui/PlanDetail.tsx +++ b/src/features/plans/ui/PlanDetail.tsx @@ -1,4 +1,4 @@ -import type { Plan } from "@/features/plans/lib/data"; +import type { PlanListData } from "@/features/plans/lib/data"; import { Dialog, DialogContent, @@ -14,7 +14,7 @@ import { type Dispatch, type SetStateAction } from "react"; interface Props { setDetail: Dispatch>; detail: boolean; - plan: Plan | null; + plan: PlanListData | null; } const PlanDetail = ({ detail, setDetail, plan }: Props) => { @@ -33,7 +33,7 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => { {/* Reja nomi */}

Reja nomi:

-

{plan.name}

+

{plan.title}

{/* Reja tavsifi */} @@ -46,7 +46,7 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {

Kimga tegishli:

- {plan.user.firstName} {plan.user.lastName} + {plan.user.first_name} {plan.user.last_name}

@@ -56,13 +56,13 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => { - {plan.status} + {plan.is_done ? "Bajarilgan" : "Bajarilmagan"}
diff --git a/src/features/plans/ui/PlansList.tsx b/src/features/plans/ui/PlansList.tsx index 25c8e3b..fbd23ef 100644 --- a/src/features/plans/ui/PlansList.tsx +++ b/src/features/plans/ui/PlansList.tsx @@ -1,63 +1,61 @@ -import type { Plan } from "@/features/plans/lib/data"; +import { plans_api } from "@/features/plans/lib/api"; +import type { PlanListData } from "@/features/plans/lib/data"; +import DeletePlan from "@/features/plans/ui/DeletePlan"; 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 formatDate from "@/shared/lib/formatDate"; import Pagination from "@/shared/ui/pagination"; -import { useMemo, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; const PlansList = () => { const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; - const [plans, setPlans] = useState([ - { - id: 1, - name: "Tumanga borish", - description: "Tumanga borish rejasi", - user: FakeUserList[0], - status: "Bajarildi", - createdAt: new Date("2025-02-03"), - }, - { - id: 2, - name: "Yangi reja", - description: "Yangi reja tavsifi", - user: FakeUserList[1], - status: "Bajarilmagan", - createdAt: new Date("2025-01-12"), - }, - ]); - - const [editingPlan, setEditingPlan] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [detail, setDetail] = useState(false); - const [statusFilter, setStatusFilter] = useState("all"); const [dateFilter, setDateFilter] = useState(undefined); const [open, setOpen] = useState(false); const [searchUser, setSearchUser] = useState(""); + const limit = 20; + const { data, isLoading, isError, isFetching } = useQuery({ + queryKey: ["plan_list", dateFilter, searchUser, statusFilter, currentPage], + queryFn: () => { + const params: { + limit?: number; + offset?: number; + status?: boolean; + date?: string; + user?: string; + } = { + date: dateFilter && formatDate.format(dateFilter, "YYYY-MM-DD"), + user: searchUser, + limit, + offset: (currentPage - 1) * limit, + }; - const handleDelete = (id: number) => { - setPlans(plans.filter((p) => p.id !== id)); + if (statusFilter !== "all") { + params.status = statusFilter === "true" ? true : false; + } + + return plans_api.list(params); + }, + select(data) { + return data.data.data; + }, + }); + + const totalPages = data ? Math.ceil(data.count / limit) : 1; + const [editingPlan, setEditingPlan] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + const [detail, setDetail] = useState(false); + + const [openDelete, setOpenDelete] = useState(false); + const [planDelete, setPlanDelete] = useState(null); + + const handleDelete = (id: PlanListData) => { + setOpenDelete(true); + setPlanDelete(id); }; - const filteredPlans = useMemo(() => { - return plans.filter((item) => { - const statusMatch = - statusFilter === "all" || item.status === statusFilter; - - const dateMatch = dateFilter - ? item.createdAt.toDateString() === dateFilter.toDateString() - : true; - - const userMatch = `${item.user.firstName} ${item.user.lastName}` - .toLowerCase() - .includes(searchUser.toLowerCase()); - - return statusMatch && dateMatch && userMatch; - }); - }, [plans, statusFilter, dateFilter, searchUser]); - return (
@@ -72,7 +70,6 @@ const PlansList = () => { setDialogOpen={setDialogOpen} setEditingPlan={setEditingPlan} setOpen={setOpen} - setPlans={setPlans} setSearchUser={setSearchUser} setStatusFilter={setStatusFilter} statusFilter={statusFilter} @@ -82,11 +79,14 @@ const PlansList = () => {
{ setCurrentPage={setCurrentPage} totalPages={totalPages} /> + +
); }; diff --git a/src/features/region/lib/api.ts b/src/features/region/lib/api.ts index a8e3da5..ab194ce 100644 --- a/src/features/region/lib/api.ts +++ b/src/features/region/lib/api.ts @@ -1,6 +1,6 @@ import type { RegionListRes } from "@/features/region/lib/data"; import httpClient from "@/shared/config/api/httpClient"; -import { REGIONS } from "@/shared/config/api/URLs"; +import { API_URLS } from "@/shared/config/api/URLs"; import type { AxiosResponse } from "axios"; export const region_api = { @@ -9,21 +9,24 @@ export const region_api = { offset?: number; name?: string; }): Promise> { - return await httpClient.get(`${REGIONS}list/`, { params }); + return await httpClient.get(`${API_URLS.REGIONS}list/`, { params }); }, async create(body: { name: string }) { - const res = await httpClient.post(`${REGIONS}create/`, body); + const res = await httpClient.post(`${API_URLS.REGIONS}create/`, body); return res; }, async update({ body, id }: { id: number; body: { name: string } }) { - const res = await httpClient.patch(`${REGIONS}${id}/update/`, body); + const res = await httpClient.patch( + `${API_URLS.REGIONS}${id}/update/`, + body, + ); return res; }, async delete(id: number) { - const res = await httpClient.delete(`${REGIONS}${id}/delete/`); + const res = await httpClient.delete(`${API_URLS.REGIONS}${id}/delete/`); return res; }, }; diff --git a/src/features/reports/lib/api.ts b/src/features/reports/lib/api.ts new file mode 100644 index 0000000..3a74e7d --- /dev/null +++ b/src/features/reports/lib/api.ts @@ -0,0 +1,14 @@ +import type { ResportListRes } from "@/features/reports/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { API_URLS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const report_api = { + async list(params: { + limit: number; + offset: number; + }): Promise> { + const res = await httpClient.get(`${API_URLS.REPORT}list/`, { params }); + return res; + }, +}; diff --git a/src/features/reports/lib/data.ts b/src/features/reports/lib/data.ts index a0abfa8..7ab5af7 100644 --- a/src/features/reports/lib/data.ts +++ b/src/features/reports/lib/data.ts @@ -67,3 +67,26 @@ export const ReportsData: ReportsTypeList[] = [ month: new Date(2025, 4, 1), }, ]; + +export interface ResportListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: null | string; + previous: null | string; + results: ResportListResData[]; + }; +} + +export interface ResportListResData { + id: number; + employee_name: string; + factory: { + id: number; + name: string; + }; + price: string; + created_at: string; +} diff --git a/src/features/reports/ui/ReportsList.tsx b/src/features/reports/ui/ReportsList.tsx index 113bd7f..77b525d 100644 --- a/src/features/reports/ui/ReportsList.tsx +++ b/src/features/reports/ui/ReportsList.tsx @@ -1,36 +1,36 @@ -import { ReportsData, type ReportsTypeList } from "@/features/reports/lib/data"; -import AddedReport from "@/features/reports/ui/AddedReport"; +import { report_api } from "@/features/reports/lib/api"; import ReportsTable from "@/features/reports/ui/ReportsTable"; -import { Button } from "@/shared/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/shared/ui/dialog"; import Pagination from "@/shared/ui/pagination"; -import { Plus } from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; import { useState } from "react"; const ReportsList = () => { const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; - const [plans, setPlans] = useState(ReportsData); + const limit = 20; + const { data, isLoading, isError } = useQuery({ + queryKey: ["report_list", currentPage], + queryFn: () => + report_api.list({ limit, offset: (currentPage - 1) * limit }), + select(data) { + return data.data.data; + }, + }); + const totalPages = data ? Math.ceil(data.count / limit) : 1; + // const [plans, setPlans] = useState(ReportsData); - const [editingPlan, setEditingPlan] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); + // const [editingPlan, setEditingPlan] = useState(null); + // const [dialogOpen, setDialogOpen] = useState(false); - const handleDelete = (id: number) => { - setPlans(plans.filter((p) => p.id !== id)); - }; + // const handleDelete = (id: number) => { + // setPlans(plans.filter((p) => p.id !== id)); + // }; return (

To'lovlar

- + {/* + */}
>; - setDialogOpen: Dispatch>; - handleDelete: (id: number) => void; + plans: ResportListResData[]; + isLoading: boolean; + isError: boolean; + // 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")} - + {isLoading && ( +
+ + + +
+ )} - + {isError && ( +
+ + Ma'lumotlarni olishda xatolik yuz berdi. + +
+ )} + {!isError && !isLoading && ( +
+ + + ID + Dorixoan nomi + To'langan summa + To'langan sanasi + {/* Harakatlar */} + + + + {plans.map((plan) => ( + + {plan.id} + {plan.employee_name} + {formatPrice(plan.price, true)} + + {formatDate.format(plan.created_at, "DD-MM-YYYY")} + + + {/* - - - ))} - -
+ */} + + ))} + + + )}
); }; diff --git a/src/features/specifications/lib/api.ts b/src/features/specifications/lib/api.ts new file mode 100644 index 0000000..b44fb0b --- /dev/null +++ b/src/features/specifications/lib/api.ts @@ -0,0 +1,33 @@ +import type { + OrderCreateReq, + OrderListRes, + OrderUpdateReq, +} from "@/features/specifications/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { API_URLS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const order_api = { + async list(params: { + limit: number; + offset: number; + }): Promise> { + const res = await httpClient.get(`${API_URLS.ORDER}list/`, { params }); + return res; + }, + + async create(body: OrderCreateReq) { + const res = await httpClient.post(`${API_URLS.ORDER}create/`, body); + return res; + }, + + async update({ body, id }: { id: number; body: OrderUpdateReq }) { + const res = await httpClient.patch(`${API_URLS.ORDER}${id}/update/`, body); + return res; + }, + + async delete(id: number) { + const res = await httpClient.delete(`${API_URLS.ORDER}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/specifications/lib/data.ts b/src/features/specifications/lib/data.ts index b7eeace..baae581 100644 --- a/src/features/specifications/lib/data.ts +++ b/src/features/specifications/lib/data.ts @@ -79,3 +79,71 @@ export const FakeSpecifications: SpecificationsType[] = [ paidPrice: 22400, }, ]; + +export interface OrderListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: null | string; + previous: null | string; + results: OrderListDataRes[]; + }; +} + +export interface OrderListDataRes { + id: number; + factory: { + id: number; + name: string; + }; + total_price: string; + paid_price: string; + advance: number; + employee_name: string; + overdue_price: string; + order_items: [ + { + id: number; + product: number; + quantity: number; + total_price: string; + }, + ]; + file: string; + user: { + id: number; + first_name: string; + last_name: string; + }; +} + +export interface OrderCreateReq { + factory_id: number; + paid_price: string; + total_price: string; + advance: number; + employee_name: string; + user_id: number; + items: { + product: number; + quantity: number; + total_price: string; + }[]; +} + +export interface OrderUpdateReq { + total_price: string; + user_id: number; + factory_id: number; + paid_price: string; + advance: number; + employee_name: string; + overdue_price?: string; + items: { + product_id: number; + quantity: number; + total_price: string; + }[]; +} diff --git a/src/features/specifications/ui/AddedSpecification.tsx b/src/features/specifications/ui/AddedSpecification.tsx index b9411d5..9d54cec 100644 --- a/src/features/specifications/ui/AddedSpecification.tsx +++ b/src/features/specifications/ui/AddedSpecification.tsx @@ -1,17 +1,29 @@ "use client"; -import { pharmData } from "@/features/pharm/lib/data"; +import { factory_api } from "@/features/pharm/lib/api"; +import { pill_api } from "@/features/pill/lib/api"; +import { order_api } from "@/features/specifications/lib/api"; import { - SpecificationsFakePills, - type SpecificationsType, + type OrderCreateReq, + type OrderListDataRes, + type OrderUpdateReq, } from "@/features/specifications/lib/data"; import { SpecificationsForm, type SpecificationsFormType, } from "@/features/specifications/lib/form"; -import { FakeUserList } from "@/features/users/lib/data"; +import { user_api } from "@/features/users/lib/api"; import formatPrice from "@/shared/lib/formatPrice"; +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, @@ -21,65 +33,161 @@ 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 { useEffect } from "react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; +import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; interface Props { - initialValues: SpecificationsType | null; + initialValues: OrderListDataRes | null; setDialogOpen: React.Dispatch>; - setData: React.Dispatch>; } -export const AddedSpecification = ({ - setData, - initialValues, - setDialogOpen, -}: Props) => { +export const AddedSpecification = ({ initialValues, setDialogOpen }: Props) => { + const queryClient = useQueryClient(); + const { data: pill } = useQuery({ + queryKey: ["pill_list", initialValues], + queryFn: () => + pill_api.list({ + limit: 999, + }), + select(data) { + return data.data.data; + }, + }); + + const [userSearch, setUserSearch] = useState(""); + const [openUser, setOpenUser] = useState(false); + + const { data: user, isLoading: isUserLoading } = useQuery({ + queryKey: ["user_list", userSearch, initialValues], + queryFn: () => user_api.list({ search: userSearch }), + select(data) { + return data.data.data.results; + }, + }); + + const [factorySearch, setFactorySearch] = useState(""); + const [openFactory, setOpenFactory] = useState(false); + + const { data: pharm, isLoading: isPharmLoading } = useQuery({ + queryKey: ["factory_list", userSearch, initialValues], + queryFn: () => factory_api.list({ name: factorySearch }), + select(data) { + return data.data.data.results; + }, + }); + + const { mutate: create, isPending: createPending } = useMutation({ + mutationFn: (body: OrderCreateReq) => order_api.create(body), + onSuccess: () => { + setDialogOpen(false); + queryClient.resetQueries({ queryKey: ["order_list"] }); + }, + 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: update, isPending: updatePending } = useMutation({ + mutationFn: ({ body, id }: { id: number; body: OrderUpdateReq }) => + order_api.update({ body, id }), + onSuccess: () => { + setDialogOpen(false); + queryClient.resetQueries({ queryKey: ["order_list"] }); + }, + 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 form = useForm({ resolver: zodResolver(SpecificationsForm), - defaultValues: initialValues - ? { - client: initialValues.client, - pharm: String(initialValues.pharm.id), - percentage: initialValues.percentage, - totalPrice: initialValues.totalPrice, - paidPrice: initialValues.paidPrice, - user: String(initialValues.user.id), - medicines: [ - ...initialValues.medicines, - ...SpecificationsFakePills.filter( - (p) => !initialValues.medicines.some((m) => m.id === p.id), - ).map((p) => ({ - id: p.id, - name: p.name, - count: 0, - price: p.price, - })), - ], - } - : { - client: "", - pharm: "", - user: "", - percentage: 0, - totalPrice: 0, - paidPrice: 0, - medicines: SpecificationsFakePills.map((p) => ({ + defaultValues: { + client: "", + pharm: "", + user: "", + percentage: 0, + totalPrice: 0, + paidPrice: 0, + medicines: [], + }, + }); + + useEffect(() => { + if (!pill) return; + + if (initialValues) { + const mergedMedicines = [ + ...initialValues.order_items.map((item) => { + const pillItem = pill.results.find((p) => p.id === item.product); + + return { + id: item.product, + name: pillItem ? pillItem.name : "Unknown", + price: pillItem ? Number(pillItem.price) : 0, + count: Number(item.quantity), + }; + }), + + ...pill.results + .filter( + (p) => !initialValues.order_items.some((m) => m.product === p.id), + ) + .map((p) => ({ id: p.id, name: p.name, + price: Number(p.price), count: 0, - price: p.price, })), - }, - }); + ]; + + form.reset({ + client: initialValues.employee_name, + pharm: String(initialValues.factory.id), + percentage: initialValues.advance, + totalPrice: Number(initialValues.total_price), + paidPrice: Number(initialValues.paid_price), + user: String(initialValues.user.id), + medicines: mergedMedicines, + }); + + return; + } + + const fakeMedicines = pill.results.map((p) => ({ + id: p.id, + name: p.name, + price: Number(p.price), + count: 0, + })); + + form.reset({ + client: "", + pharm: "", + user: "", + percentage: 0, + totalPrice: 0, + paidPrice: 0, + medicines: fakeMedicines, + }); + }, [pill, initialValues, form]); const medicines = form.watch("medicines"); @@ -105,84 +213,229 @@ export const AddedSpecification = ({ }, [form]); const onSubmit = (values: SpecificationsFormType) => { - if (initialValues) { - setData((prev) => - prev.map((item) => - item.id === initialValues.id - ? { - ...item, - ...values, - pharm: pharmData.find((e) => e.id === Number(values.pharm))!, - user: FakeUserList.find((e) => e.id === Number(values.user))!, - } - : item, - ), - ); - } else { - setData((prev) => [ - ...prev, - { - ...values, - id: Date.now(), - pharm: pharmData.find((e) => e.id === Number(values.pharm))!, - user: FakeUserList[1], + if (!initialValues) { + const items = medicines + .filter((med) => med.count > 0) + .map((med) => ({ + product: med.id, + quantity: med.count, + total_price: (med.price * med.count).toFixed(2), + })); + + const total_price = items + .reduce((sum, item) => sum + parseFloat(item.total_price), 0) + .toFixed(2); + + create({ + advance: values.percentage, + employee_name: values.client, + factory_id: Number(values.pharm), + paid_price: String(values.paidPrice), + total_price, + items, + user_id: Number(values.user), + }); + } else if (initialValues) { + const items = medicines + .filter((med) => med.count > 0) + .map((med) => ({ + product_id: med.id, + quantity: med.count, + total_price: (med.price * med.count).toFixed(2), + })); + + const total_price = items + .reduce((sum, item) => sum + parseFloat(item.total_price), 0) + .toFixed(2); + + update({ + body: { + advance: values.percentage, + employee_name: values.client, + paid_price: String(values.paidPrice), + total_price, + items: items, + factory_id: Number(values.pharm), + user_id: Number(values.user), }, - ]); + id: initialValues.id, + }); } - setDialogOpen(false); }; return (
( - - - - - - - - )} + control={form.control} + render={({ field }) => { + const selectedUser = pharm?.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {isPharmLoading ? ( +
+ +
+ ) : pharm && pharm.length > 0 ? ( + + {pharm.map((u) => ( + { + field.onChange(String(u.id)); + setOpenFactory(false); + }} + > + + {u.name} + + ))} + + ) : ( + Farmasevtika topilmadi + )} +
+
+
+
+ + +
+ ); + }} /> ( - - - - - - - - )} + control={form.control} + render={({ field }) => { + const selectedUser = user?.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {isUserLoading ? ( +
+ +
+ ) : user && user.length > 0 ? ( + + {user.map((u) => ( + { + field.onChange(String(u.id)); + setOpenUser(false); + }} + > + + {u.first_name} {u.last_name} {u.region.name} + + ))} + + ) : ( + Foydalanuvchi topilmadi + )} +
+
+
+
+ + +
+ ); + }} /> -

{med.name}

+
+

{med.name}

+

Narxi:{formatPrice(med.price)}

+
@@ -293,9 +549,10 @@ export const AddedSpecification = ({ diff --git a/src/features/specifications/ui/DeleteOrder.tsx b/src/features/specifications/ui/DeleteOrder.tsx new file mode 100644 index 0000000..4093ad4 --- /dev/null +++ b/src/features/specifications/ui/DeleteOrder.tsx @@ -0,0 +1,88 @@ +import { order_api } from "@/features/specifications/lib/api"; +import type { OrderListDataRes } from "@/features/specifications/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, SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + opneDelete: boolean; + setOpenDelete: Dispatch>; + setPillDelete: Dispatch>; + pillDelete: OrderListDataRes | null; +} + +const DeleteOrder = ({ + opneDelete, + setOpenDelete, + pillDelete, + setPillDelete, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate: deleteUser, isPending } = useMutation({ + mutationFn: (id: number) => order_api.delete(id), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["order_list"] }); + toast.success(`Foydalanuvchi o'chirildi`); + setOpenDelete(false); + setPillDelete(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 ( + + + + Dorini o'chirish + + Siz rostan ham bu zakazni o'chirmoqchimisiz + + + + + + + + + ); +}; + +export default DeleteOrder; diff --git a/src/features/specifications/ui/SpecificationDetail .tsx b/src/features/specifications/ui/SpecificationDetail .tsx index ea67b3a..396858e 100644 --- a/src/features/specifications/ui/SpecificationDetail .tsx +++ b/src/features/specifications/ui/SpecificationDetail .tsx @@ -1,17 +1,19 @@ "use client"; -import type { SpecificationsType } from "@/features/specifications/lib/data"; +import type { OrderListDataRes } from "@/features/specifications/lib/data"; import formatPrice from "@/shared/lib/formatPrice"; +import { Button } from "@/shared/ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from "@/shared/ui/dialog"; +import { HardDriveDownloadIcon } from "lucide-react"; import type { Dispatch, SetStateAction } from "react"; interface Props { - specification: SpecificationsType | null; + specification: OrderListDataRes | null; open: boolean; setOpen: Dispatch>; } @@ -21,6 +23,37 @@ export const SpecificationDetail = ({ open, setOpen, }: Props) => { + const downloadFile = async (fileUrl: string, fileName: string) => { + try { + const response = await fetch(fileUrl, { + method: "GET", + headers: { + // Agar token kerak bo'lsa qo'shing + // Authorization: `Bearer ${yourToken}`, + }, + }); + + if (!response.ok) { + throw new Error("Fayl yuklab olishda xatolik yuz berdi"); + } + + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + + const a = document.createElement("a"); + a.href = url; + a.download = fileName || "file.pdf"; // fayl nomi + document.body.appendChild(a); + a.click(); + + // Tozalash + a.remove(); + window.URL.revokeObjectURL(url); + } catch (err) { + console.error(err); + } + }; + if (!specification) return null; return ( @@ -39,7 +72,7 @@ export const SpecificationDetail = ({

Xaridor

- {specification.client} + {specification.employee_name}

@@ -49,7 +82,7 @@ export const SpecificationDetail = ({ Farmasevtika

- {specification.pharm.name} + {specification.factory.name}

@@ -59,7 +92,7 @@ export const SpecificationDetail = ({ Mas'ul xodim

- {specification.user.firstName} {specification.user.lastName} + {specification.user.first_name} {specification.user.last_name}

@@ -68,13 +101,13 @@ export const SpecificationDetail = ({

- {specification.medicines.length} + {specification.order_items.length} Dorilar ro'yxati

- {specification.medicines.map((med, index) => ( + {specification.order_items.map((med, index) => (

- {med.name} + {med.product}

- Miqdor: {med.count} ta + Miqdor: {med.quantity} ta × - Narx: {formatPrice(med.price)} + Narx:{" "} + + {formatPrice( + Number(med.total_price) / med.quantity, + )} +

Jami

- {formatPrice(med.count * med.price)} + {formatPrice(med.total_price)}

@@ -121,29 +159,38 @@ export const SpecificationDetail = ({
Jami narx: - {formatPrice(specification.totalPrice)} + {formatPrice(specification.total_price)}
- Chegirma foizi: + To'langan foizi: - {specification.percentage}% + {specification.advance}%
- To'lanadi: + To'langan: - {formatPrice(specification.paidPrice)} + {formatPrice(specification.paid_price)}
+ diff --git a/src/features/specifications/ui/SpecificationsList.tsx b/src/features/specifications/ui/SpecificationsList.tsx index 0968558..b92d8f7 100644 --- a/src/features/specifications/ui/SpecificationsList.tsx +++ b/src/features/specifications/ui/SpecificationsList.tsx @@ -1,10 +1,9 @@ "use client"; -import { - FakeSpecifications, - type SpecificationsType, -} from "@/features/specifications/lib/data"; +import { order_api } from "@/features/specifications/lib/api"; +import { type OrderListDataRes } from "@/features/specifications/lib/data"; import { AddedSpecification } from "@/features/specifications/ui/AddedSpecification"; +import DeleteOrder from "@/features/specifications/ui/DeleteOrder"; import { SpecificationDetail } from "@/features/specifications/ui/SpecificationDetail "; import formatPrice from "@/shared/lib/formatPrice"; import { Button } from "@/shared/ui/button"; @@ -15,6 +14,7 @@ import { DialogTitle, DialogTrigger, } from "@/shared/ui/dialog"; +import Pagination from "@/shared/ui/pagination"; import { Table, TableBody, @@ -23,30 +23,38 @@ import { TableHeader, TableRow, } from "@/shared/ui/table"; -import clsx from "clsx"; -import { - ChevronLeft, - ChevronRight, - Eye, - Pencil, - Plus, - Trash2, -} from "lucide-react"; +import { useQuery } from "@tanstack/react-query"; +import { Eye, Loader2, Pencil, Plus, Trash2 } from "lucide-react"; import { useState } from "react"; const SpecificationsList = () => { - const [data, setData] = useState(FakeSpecifications); - const [editingPlan, setEditingPlan] = useState( - null, - ); - const [detail, setDetail] = useState(null); + const [editingPlan, setEditingPlan] = useState(null); + const [detail, setDetail] = useState(null); const [detailOpen, setDetailOpen] = useState(false); const [dialogOpen, setDialogOpen] = useState(false); const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; + const limit = 20; - const handleDelete = (id: number) => - setData((prev) => prev.filter((e) => e.id !== id)); + const { + data: order, + isLoading, + isError, + } = useQuery({ + queryKey: ["order_list", currentPage], + queryFn: () => order_api.list({ limit, offset: (currentPage - 1) * limit }), + select(data) { + return data.data.data; + }, + }); + const totalPages = order ? Math.ceil(order.count / limit) : 1; + + const [openDelete, setOpenDelete] = useState(false); + const [pillDelete, setPillDelete] = useState(null); + + const handleDelete = (id: OrderListDataRes) => { + setOpenDelete(true); + setPillDelete(id); + }; return (
@@ -70,7 +78,6 @@ const SpecificationsList = () => { @@ -83,103 +90,98 @@ const SpecificationsList = () => {
- - - - # - Foydalanuvchi - Farmasevtika - Zakaz qilgan - Jami - % To‘langan - To‘langan summa - Amallar - - - - {data.map((item, idx) => ( - - {idx + 1} - - {item.user.firstName} {item.user.lastName} - - {item.pharm.name} - {item.client} - {formatPrice(item.totalPrice)} - {item.percentage}% - {formatPrice(item.paidPrice)} - - - - - + {isLoading && ( +
+ + + +
+ )} + + {isError && ( +
+ + Ma'lumotlarni olishda xatolik yuz berdi. + +
+ )} + {!isLoading && !isError && ( +
+ + + # + Foydalanuvchi + Farmasevtika + Zakaz qilgan + Jami + % To‘langan + To‘langan summa + Amallar - ))} - -
+ + + {order?.results.map((item, idx) => ( + + {idx + 1} + + {item.user.first_name} {item.user.last_name} + + {item.factory.name} + {item.employee_name} + {formatPrice(item.total_price)} + {item.advance}% + {formatPrice(item.paid_price)} + + + + + + + ))} + + + )}
- {/* Pagination */} -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+ + + ); }; diff --git a/src/features/tour-plan/lib/api.ts b/src/features/tour-plan/lib/api.ts new file mode 100644 index 0000000..afb9c43 --- /dev/null +++ b/src/features/tour-plan/lib/api.ts @@ -0,0 +1,39 @@ +import type { + PlanTourCreate, + PlanTourListRes, + PlanTourUpdate, +} from "@/features/tour-plan/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { API_URLS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const tour_plan_api = { + async list(params: { + limit?: number; + offset?: number; + name?: string; + date?: string; + user?: string; + }): Promise> { + const res = await httpClient.get(`${API_URLS.TOUR_PLAN}list/`, { params }); + return res; + }, + + async create(body: PlanTourCreate) { + const res = await httpClient.post(`${API_URLS.TOUR_PLAN}create/`, body); + return res; + }, + + async update({ body, id }: { id: number; body: PlanTourUpdate }) { + const res = await httpClient.patch( + `${API_URLS.TOUR_PLAN}${id}/update/`, + body, + ); + return res; + }, + + async delete(id: number) { + const res = await httpClient.delete(`${API_URLS.TOUR_PLAN}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/tour-plan/lib/data.ts b/src/features/tour-plan/lib/data.ts index 83e3e5b..3bbe79a 100644 --- a/src/features/tour-plan/lib/data.ts +++ b/src/features/tour-plan/lib/data.ts @@ -58,3 +58,46 @@ export const fakeTourPlan: TourPlanType[] = [ status: "planned", }, ]; + +export interface PlanTourListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: null | string; + previous: null | string; + results: PlanTourListDataRes[]; + }; +} + +export interface PlanTourListDataRes { + id: number; + place_name: string; + user: { + id: number; + first_name: string; + last_name: string; + }; + latitude: null | string; + longitude: null | string; + location_send: boolean; + date: null | string; + created_at: string; +} + +export interface PlanTourCreate { + place_name: string; + // latitude: number; + // longitude: number; + user_id: number; + date: string; +} + +export interface PlanTourUpdate { + place_name: string; + user: number; + // latitude: number; + // longitude: number; + date: string; +} diff --git a/src/features/tour-plan/lib/form.ts b/src/features/tour-plan/lib/form.ts index 9306c68..962fd30 100644 --- a/src/features/tour-plan/lib/form.ts +++ b/src/features/tour-plan/lib/form.ts @@ -4,6 +4,4 @@ export const tourPlanForm = z.object({ district: z.string().min(1, { message: "Majburiy maydon" }), user: z.string().min(1, { message: "Majburiy maydon" }), date: z.date().min(1, { message: "Majburiy maydon" }), - long: z.string().min(1, { message: "Majburiy maydon" }), - lat: z.string().min(1, { message: "Majburiy maydon" }), }); diff --git a/src/features/tour-plan/ui/AddedTourPlan.tsx b/src/features/tour-plan/ui/AddedTourPlan.tsx index e54342f..635b4c9 100644 --- a/src/features/tour-plan/ui/AddedTourPlan.tsx +++ b/src/features/tour-plan/ui/AddedTourPlan.tsx @@ -1,8 +1,23 @@ -import type { TourPlanType } from "@/features/tour-plan/lib/data"; +import { tour_plan_api } from "@/features/tour-plan/lib/api"; +import type { + PlanTourCreate, + PlanTourListDataRes, + PlanTourUpdate, +} from "@/features/tour-plan/lib/data"; import { tourPlanForm } from "@/features/tour-plan/lib/form"; -import { FakeUserList } from "@/features/users/lib/data"; +import { user_api } from "@/features/users/lib/api"; +import formatDate from "@/shared/lib/formatDate"; +import { cn } from "@/shared/lib/utils"; import { Button } from "@/shared/ui/button"; import { Calendar } from "@/shared/ui/calendar"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/shared/ui/command"; import { Form, FormControl, @@ -13,103 +28,196 @@ import { import { Input } from "@/shared/ui/input"; import { Label } from "@/shared/ui/label"; import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; -import { ChevronDownIcon, Loader2 } from "lucide-react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Check, ChevronDownIcon, ChevronsUpDown, Loader2 } from "lucide-react"; import { 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: TourPlanType | null; + initialValues: PlanTourListDataRes | null; setDialogOpen: Dispatch>; - setPlans: Dispatch>; } -const AddedTourPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => { - const [load, setLoad] = useState(false); +const AddedTourPlan = ({ initialValues, setDialogOpen }: Props) => { + const queryClient = useQueryClient(); const form = useForm>({ resolver: zodResolver(tourPlanForm), defaultValues: { - date: initialValues?.date || undefined, - district: initialValues?.district || "", - lat: initialValues?.lat || "41.2949", - long: initialValues?.long || "69.2361", + date: initialValues?.date ? new Date(initialValues?.date) : undefined, + district: initialValues?.place_name || "", + user: initialValues?.user.id.toString() || "", }, }); + const { mutate, isPending } = useMutation({ + mutationFn: (body: PlanTourCreate) => tour_plan_api.create(body), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["tour_plan_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 }: { id: number; body: PlanTourUpdate }) => + tour_plan_api.update({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["tour_plan_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 [open, setOpen] = useState(false); + const [searchUser, setSearchUser] = useState(""); - const lat = form.watch("lat"); - const long = form.watch("long"); + const [openUser, setOpenUser] = useState(false); + 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; + }, + }); function onSubmit(values: z.infer) { - setLoad(true); - const newObject: TourPlanType = { - id: initialValues ? initialValues.id : Date.now(), - user: FakeUserList.find((u) => u.id === Number(values.user))!, - date: values.date, - district: values.district, - lat: values.lat, - long: values.long, - status: "planned", - }; - - setTimeout(() => { - setPlans((prev) => { - if (initialValues) { - return prev.map((item) => - item.id === initialValues.id ? newObject : item, - ); - } else { - return [...prev, newObject]; - } + if (!initialValues) { + mutate({ + date: formatDate.format(values.date, "YYYY-MM-DD"), + place_name: values.district, + user_id: Number(values.user), }); - setLoad(false); - setDialogOpen(false); - }, 2000); + } else if (initialValues) { + edit({ + body: { + user: Number(values.user), + date: formatDate.format(values.date, "YYYY-MM-DD"), + place_name: values.district, + }, + id: initialValues.id, + }); + } } return (
( - - - - - - - - )} + 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 + )} +
+
+
+
+ + +
+ ); + }} /> { )} /> -
+ {/*
{ /> -
+
*/} + + + + + ); +}; + +export default DeleteTourPlan; diff --git a/src/features/tour-plan/ui/FilterTourPlan.tsx b/src/features/tour-plan/ui/FilterTourPlan.tsx new file mode 100644 index 0000000..fc0e411 --- /dev/null +++ b/src/features/tour-plan/ui/FilterTourPlan.tsx @@ -0,0 +1,113 @@ +import type { PlanTourListDataRes } from "@/features/tour-plan/lib/data"; +import AddedTourPlan from "@/features/tour-plan/ui/AddedTourPlan"; +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 { ChevronDownIcon, Plus } from "lucide-react"; +import { useState, type Dispatch, type SetStateAction } from "react"; + +interface Props { + dateFilter: Date | undefined; + setDateFilter: Dispatch>; + searchUser: string; + setSearchUser: Dispatch>; + dialogOpen: boolean; + setDialogOpen: Dispatch>; + setEditingPlan: Dispatch>; + editingPlan: PlanTourListDataRes | null; +} + +const FilterTourPlan = ({ + dateFilter, + setDateFilter, + searchUser, + setSearchUser, + setDialogOpen, + dialogOpen, + setEditingPlan, + editingPlan, +}: Props) => { + const [open, setOpen] = useState(false); + + return ( +
+ + + + + + { + setDateFilter(date); + setOpen(false); + }} + toYear={new Date().getFullYear() + 50} + /> +
+ +
+
+
+ + setSearchUser(e.target.value)} + /> + + + + + + + + {editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"} + + + + + + +
+ ); +}; + +export default FilterTourPlan; diff --git a/src/features/tour-plan/ui/TourPlanDetailDialog.tsx b/src/features/tour-plan/ui/TourPlanDetailDialog.tsx index 7ab7768..445e661 100644 --- a/src/features/tour-plan/ui/TourPlanDetailDialog.tsx +++ b/src/features/tour-plan/ui/TourPlanDetailDialog.tsx @@ -1,6 +1,7 @@ "use client"; -import type { TourPlanType } from "@/features/tour-plan/lib/data"; +import type { PlanTourListDataRes } from "@/features/tour-plan/lib/data"; +import formatDate from "@/shared/lib/formatDate"; import { Badge } from "@/shared/ui/badge"; import { Dialog, @@ -13,7 +14,7 @@ import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps"; interface Props { open: boolean; setOpen: (v: boolean) => void; - plan: TourPlanType | null; + plan: PlanTourListDataRes | null; } const TourPlanDetailDialog = ({ open, setOpen, plan }: Props) => { @@ -33,55 +34,50 @@ const TourPlanDetailDialog = ({ open, setOpen, plan }: Props) => {

Foydalanuvchi:

- {plan.user.firstName} {plan.user.lastName} + {plan.user.first_name} {plan.user.last_name}

{/* District */}

Hudud:

-

{plan.district}

+

{plan.place_name}

{/* Sana */}

Sana:

-

{plan.date.toLocaleString()}

+

{plan.date && formatDate.format(plan.date, "YYYY-MM-DD")}

{/* Status */}

Status:

- {plan.status === "completed" ? "Bajarilgan" : "Rejalashtirilgan"} + {plan.location_send ? "Bajarilgan" : "Rejalashtirilgan"}
- {plan.userLocation && ( + {plan.location_send && ( { const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; - const [plans, setPlans] = useState(fakeTourPlan); - - const [editingPlan, setEditingPlan] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - const [detail, setDetail] = useState(null); - const [detailOpen, setDetailOpen] = useState(false); - + const limit = 20; const [dateFilter, setDateFilter] = useState(undefined); - const [open, setOpen] = useState(false); const [searchUser, setSearchUser] = useState(""); - const handleDelete = (id: number) => { - setPlans(plans.filter((p) => p.id !== id)); + const { data, isError, isLoading, isFetching } = useQuery({ + queryKey: ["tour_plan_list", currentPage, dateFilter, searchUser], + queryFn: () => + tour_plan_api.list({ + limit, + offset: (currentPage - 1) * limit, + date: dateFilter && formatDate.format(dateFilter, "YYYY-MM-DD"), + user: searchUser, + }), + select(data) { + return data.data.data; + }, + }); + + const totalPages = data ? Math.ceil(data.count / limit) : 1; + + const [editingPlan, setEditingPlan] = useState( + null, + ); + const [dialogOpen, setDialogOpen] = useState(false); + const [detail, setDetail] = useState(null); + const [detailOpen, setDetailOpen] = useState(false); + const [openDelete, setOpenDelete] = useState(false); + const [planDelete, setPlanDelete] = useState( + null, + ); + + const handleDelete = (id: PlanTourListDataRes) => { + setOpenDelete(true); + setPlanDelete(id); }; - const filteredPlans = useMemo(() => { - return plans.filter((item) => { - // 2) Sana filtri: createdAt === tanlangan sana - const dateMatch = dateFilter - ? item.date.toDateString() === dateFilter.toDateString() - : true; - - // 3) User ism familiya bo'yicha qidiruv - const userMatch = `${item.user.firstName} ${item.user.lastName}` - .toLowerCase() - .includes(searchUser.toLowerCase()); - - return dateMatch && userMatch; - }); - }, [plans, dateFilter, searchUser]); - return (
- {/* Header */}

Rejalarni boshqarish

-
- {/* Sana filter */} - - - - - - { - setDateFilter(date); - setOpen(false); - }} - /> -
- -
-
-
- - setSearchUser(e.target.value)} - /> - - - - - - - - {editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"} - - - - - - -
+ { />
- {/* Table */} -
- - - - ID - Kimga tegishli - Boriladigan joyi - Sanasi - Statusi - Harakatlar - - - - {filteredPlans.map((plan) => ( - - {plan.id} - - {plan.user.firstName + " " + plan.user.lastName} - - {plan.district} - - {formatDate.format(plan.date, "DD-MM-YYYY")} - - - {plan.status === "completed" ? "Borildi" : "Borilmagan"} - + - - - - - - - ))} - -
-
+ -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+
); }; diff --git a/src/features/tour-plan/ui/TourPlanTable.tsx b/src/features/tour-plan/ui/TourPlanTable.tsx new file mode 100644 index 0000000..1f25ca9 --- /dev/null +++ b/src/features/tour-plan/ui/TourPlanTable.tsx @@ -0,0 +1,135 @@ +import type { PlanTourListDataRes } from "@/features/tour-plan/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 clsx from "clsx"; +import { Edit, Eye, Loader2, Trash } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + data: PlanTourListDataRes[]; + isLoading: boolean; + isFetching: boolean; + isError: boolean; + setDetail: Dispatch>; + setDetailOpen: Dispatch>; + setEditingPlan: Dispatch>; + setDialogOpen: Dispatch>; + handleDelete: (paln: PlanTourListDataRes) => void; +} + +const TourPlanTable = ({ + data, + isFetching, + isError, + isLoading, + setDetail, + setEditingPlan, + handleDelete, + setDialogOpen, + setDetailOpen, +}: Props) => { + return ( +
+ {(isLoading || isFetching) && ( +
+ + + +
+ )} + {isError && ( +
+ + Ma'lumotlarni olishda xatolik yuz berdi. + +
+ )}{" "} + {!isLoading && !isError && ( + + + + ID + Kimga tegishli + Boriladigan joyi + Sanasi + Statusi + Harakatlar + + + + {data.length > 0 ? ( + data.map((plan) => ( + + {plan.id} + + {plan.user.first_name + " " + plan.user.last_name} + + {plan.place_name} + + {plan.date && formatDate.format(plan.date, "DD-MM-YYYY")} + + + {plan.location_send ? "Borildi" : "Borilmagan"} + + + + + + + + + )) + ) : ( + + + Tur plan topilmadi. + + + )} + +
+ )} +
+ ); +}; + +export default TourPlanTable; diff --git a/src/features/users/lib/api.ts b/src/features/users/lib/api.ts index f18a060..f3075aa 100644 --- a/src/features/users/lib/api.ts +++ b/src/features/users/lib/api.ts @@ -5,7 +5,7 @@ import type { UserUpdateReq, } from "@/features/users/lib/data"; import httpClient from "@/shared/config/api/httpClient"; -import { USER } from "@/shared/config/api/URLs"; +import { API_URLS } from "@/shared/config/api/URLs"; import axios, { type AxiosResponse } from "axios"; export const user_api = { @@ -16,27 +16,27 @@ export const user_api = { is_active?: boolean | string; region_id?: number; }): Promise> { - const res = await httpClient.get(`${USER}list/`, { params }); + const res = await httpClient.get(`${API_URLS.USER}list/`, { params }); return res; }, async update({ body, id }: { id: number; body: UserUpdateReq }) { - const res = await httpClient.patch(`${USER}${id}/update/`, body); + const res = await httpClient.patch(`${API_URLS.USER}${id}/update/`, body); return res; }, async create(body: UserCreateReq) { - const res = await httpClient.post(`${USER}create/`, body); + const res = await httpClient.post(`${API_URLS.USER}create/`, body); return res; }, async active(id: number) { - const res = await httpClient.post(`${USER}${id}/activate/`); + const res = await httpClient.post(`${API_URLS.USER}${id}/activate/`); return res; }, async delete({ id }: { id: number }) { - const res = await httpClient.delete(`${USER}${id}/delete/`); + const res = await httpClient.delete(`${API_URLS.USER}${id}/delete/`); return res; }, diff --git a/src/shared/config/api/URLs.ts b/src/shared/config/api/URLs.ts index b64311b..bc5a0b0 100644 --- a/src/shared/config/api/URLs.ts +++ b/src/shared/config/api/URLs.ts @@ -1,23 +1,19 @@ -const BASE_URL = - import.meta.env.VITE_API_URL || "https://api.meridynpharma.com"; - -const LOGIN = "/api/v1/authentication/admin_login/"; -const USER = "/api/v1/admin/user/"; -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, - OBJECT, - PHARMACIES, - REGION, - REGIONS, - USER, +export const API_URLS = { + BASE_URL: import.meta.env.VITE_API_URL || "https://api.meridynpharma.com", + LOGIN: "/api/v1/authentication/admin_login/", + USER: "/api/v1/admin/user/", + REGION: "/api/v1/admin/district/", + REGIONS: "/api/v1/admin/region/", + DISTRICT: "/api/v1/admin/district/", + DOCTOR: "/api/v1/admin/doctor/", + OBJECT: "/api/v1/admin/place/", + PHARMACIES: "/api/v1/admin/pharmacy/", + PLANS: "/api/v1/admin/plan/", + PILL: "/api/v1/admin/product/", + LOCATION: "/api/v1/admin/location/", + USER_LOCATION: "/api/v1/admin/user_location/", + ORDER: "/api/v1/admin/order/", + FACTORY: "/api/v1/admin/factory/", + REPORT: "/api/v1/admin/payment/", + TOUR_PLAN: "/api/v1/admin/tour_plan/", }; diff --git a/src/shared/config/api/httpClient.ts b/src/shared/config/api/httpClient.ts index 8b79a40..3c70a47 100644 --- a/src/shared/config/api/httpClient.ts +++ b/src/shared/config/api/httpClient.ts @@ -1,10 +1,10 @@ import i18n from "@/shared/config/i18n"; import { getToken } from "@/shared/lib/cookie"; import axios from "axios"; -import { BASE_URL } from "./URLs"; +import { API_URLS } from "./URLs"; const httpClient = axios.create({ - baseURL: BASE_URL, + baseURL: API_URLS.BASE_URL, timeout: 10000, }); diff --git a/src/widgets/sidebar-layout/index.tsx b/src/widgets/sidebar-layout/index.tsx index 9941050..007d406 100644 --- a/src/widgets/sidebar-layout/index.tsx +++ b/src/widgets/sidebar-layout/index.tsx @@ -61,11 +61,6 @@ const items = [ url: "/dashboard/pharmacies", icon: Hospital, }, - { - title: "Hisobotlar", - url: "/dashboard/reports", - icon: ClipboardList, - }, { title: "Jo'natilgan joylar", url: "/dashboard/sent-locations", @@ -76,6 +71,11 @@ const items = [ url: "/dashboard/specifications", icon: FileText, }, + { + title: "Hisobotlar", + url: "/dashboard/reports", + icon: ClipboardList, + }, { title: "Tur planlar", url: "/dashboard/tour-plan",