doctor and pharmacies crud
This commit is contained in:
@@ -8,6 +8,7 @@ export const discrit_api = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
name?: string;
|
||||
user?: number;
|
||||
}): Promise<AxiosResponse<DistrictListRes>> {
|
||||
const res = await httpClient.get(`${DISTRICT}list/`, { params });
|
||||
return res;
|
||||
|
||||
@@ -2,8 +2,8 @@ import { discrit_api } from "@/features/districts/lib/api";
|
||||
import { type DistrictListData } from "@/features/districts/lib/data";
|
||||
import DeleteDiscrit from "@/features/districts/ui/DeleteDiscrit";
|
||||
import Filter from "@/features/districts/ui/Filter";
|
||||
import PaginationDistrict from "@/features/districts/ui/PaginationDistrict";
|
||||
import TableDistrict from "@/features/districts/ui/TableDistrict";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { useState } from "react";
|
||||
@@ -66,7 +66,7 @@ const DistrictsList = () => {
|
||||
currentPage={currentPage}
|
||||
/>
|
||||
|
||||
<PaginationDistrict
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
currentPage: number;
|
||||
setCurrentPage: Dispatch<SetStateAction<number>>;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const PaginationDistrict = ({
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginationDistrict;
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { DoctorListRes } from "@/features/doctors/lib/data";
|
||||
import type {
|
||||
CreateDoctorReq,
|
||||
DoctorListRes,
|
||||
UpdateDoctorReq,
|
||||
} from "@/features/doctors/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { DOCTOR } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
@@ -17,4 +21,19 @@ export const doctor_api = {
|
||||
const res = await httpClient.get(`${DOCTOR}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: CreateDoctorReq) {
|
||||
const res = await httpClient.post(`${DOCTOR}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: UpdateDoctorReq }) {
|
||||
const res = await httpClient.patch(`${DOCTOR}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${DOCTOR}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -92,3 +92,32 @@ export interface DoctorListResData {
|
||||
};
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreateDoctorReq {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone_number: string;
|
||||
work_place: string;
|
||||
sphere: string;
|
||||
description: string;
|
||||
district_id: number;
|
||||
place_id: number;
|
||||
user_id: number;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
};
|
||||
}
|
||||
export interface UpdateDoctorReq {
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone_number: string;
|
||||
work_place: string;
|
||||
sphere: string;
|
||||
description: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: { longitude: number; latitude: number };
|
||||
}
|
||||
|
||||
@@ -1,10 +1,25 @@
|
||||
import { fakeDistrict } from "@/features/districts/lib/data";
|
||||
import type { DoctorListType } from "@/features/doctors/lib/data";
|
||||
import { discrit_api } from "@/features/districts/lib/api";
|
||||
import { doctor_api } from "@/features/doctors/lib/api";
|
||||
import type {
|
||||
CreateDoctorReq,
|
||||
DoctorListResData,
|
||||
UpdateDoctorReq,
|
||||
} from "@/features/doctors/lib/data";
|
||||
import { DoctorForm } from "@/features/doctors/lib/form";
|
||||
import { ObjectListData } from "@/features/objects/lib/data";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { object_api } from "@/features/objects/lib/api";
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import onlyNumber from "@/shared/lib/onlyNumber";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/shared/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -14,58 +29,266 @@ import {
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import {
|
||||
Circle,
|
||||
Map,
|
||||
Placemark,
|
||||
Polygon,
|
||||
YMaps,
|
||||
ZoomControl,
|
||||
} from "@pbe/react-yandex-maps";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: DoctorListType | null;
|
||||
initialValues: DoctorListResData | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
interface CoordsData {
|
||||
lat: number;
|
||||
lon: number;
|
||||
polygon: [number, number][][];
|
||||
}
|
||||
|
||||
const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
const queryClient = useQueryClient();
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
const [searchObject, setSearchObject] = useState<string>("");
|
||||
const [selectDiscrit, setSelectedDiscrit] = useState<string>("");
|
||||
const [searchDiscrit, setSearchDiscrit] = useState<string>("");
|
||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||
const [openDiscrit, setOpenDiscrit] = useState<boolean>(false);
|
||||
const [openObject, setOpenObject] = useState<boolean>(false);
|
||||
|
||||
const form = useForm<z.infer<typeof DoctorForm>>({
|
||||
resolver: zodResolver(DoctorForm),
|
||||
defaultValues: {
|
||||
desc: initialValues?.desc || "",
|
||||
desc: initialValues?.description || "",
|
||||
district: initialValues?.district.id.toString() || "",
|
||||
first_name: initialValues?.first_name || "",
|
||||
last_name: initialValues?.last_name || "",
|
||||
lat: initialValues?.lat || "41.2949",
|
||||
long: initialValues?.long || "69.2361",
|
||||
object: initialValues?.object.id.toString() || "",
|
||||
lat: String(initialValues?.latitude) || "41.2949",
|
||||
long: String(initialValues?.longitude) || "69.2361",
|
||||
object: initialValues?.place.id.toString() || "",
|
||||
phone_number: initialValues?.phone_number || "+998",
|
||||
spec: initialValues?.spec || "",
|
||||
work: initialValues?.work || "",
|
||||
spec: initialValues?.sphere || "",
|
||||
work: initialValues?.work_place || "",
|
||||
user: initialValues?.user.id.toString() || "",
|
||||
},
|
||||
});
|
||||
|
||||
const lat = form.watch("lat");
|
||||
const long = form.watch("long");
|
||||
const { data: user, isLoading: isUserLoading } = useQuery({
|
||||
queryKey: ["user_list", searchUser],
|
||||
queryFn: () => {
|
||||
const params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
is_active?: boolean | string;
|
||||
region_id?: number;
|
||||
} = {
|
||||
limit: 8,
|
||||
search: searchUser,
|
||||
};
|
||||
|
||||
const handleMapClick = (e: { get: (key: string) => number[] }) => {
|
||||
const coords = e.get("coords");
|
||||
form.setValue("lat", coords[0].toString());
|
||||
form.setValue("long", coords[1].toString());
|
||||
return user_api.list(params);
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: object, isLoading: isObjectLoading } = useQuery({
|
||||
queryKey: ["object_list", searchUser, selectDiscrit],
|
||||
queryFn: () => {
|
||||
const params: {
|
||||
name?: string;
|
||||
district?: string;
|
||||
} = {
|
||||
name: searchUser,
|
||||
district: selectDiscrit,
|
||||
};
|
||||
|
||||
return object_api.list(params);
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const user_id = form.watch("user");
|
||||
|
||||
const { data: discrit, isLoading: discritLoading } = useQuery({
|
||||
queryKey: ["discrit_list", searchDiscrit, user_id],
|
||||
queryFn: () => {
|
||||
const params: {
|
||||
name?: string;
|
||||
user?: number;
|
||||
} = {
|
||||
name: searchDiscrit,
|
||||
};
|
||||
|
||||
if (user_id !== "") {
|
||||
params.user = Number(user_id);
|
||||
}
|
||||
|
||||
return discrit_api.list(params);
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: CreateDoctorReq) => doctor_api.create(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["doctor_list"] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: edit, isPending: editPending } = useMutation({
|
||||
mutationFn: ({ body, id }: { body: UpdateDoctorReq; id: number }) =>
|
||||
doctor_api.update({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["doctor_list"] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const [coords, setCoords] = useState({
|
||||
latitude: 41.311081,
|
||||
longitude: 69.240562,
|
||||
});
|
||||
const [polygonCoords, setPolygonCoords] = useState<
|
||||
[number, number][][] | null
|
||||
>(null);
|
||||
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const getCoords = async (name: string): Promise<CoordsData | null> => {
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(name)}&format=json&polygon_geojson=1&limit=1`,
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.length > 0 && data[0].geojson) {
|
||||
const lat = parseFloat(data[0].lat);
|
||||
const lon = parseFloat(data[0].lon);
|
||||
|
||||
let polygon: [number, number][][] = [];
|
||||
|
||||
if (data[0].geojson.type === "Polygon") {
|
||||
polygon = data[0].geojson.coordinates.map((ring: [number, number][]) =>
|
||||
ring.map((coord: [number, number]) => [coord[1], coord[0]]),
|
||||
);
|
||||
} else if (data[0].geojson.type === "MultiPolygon") {
|
||||
polygon = data[0].geojson.coordinates.map(
|
||||
(poly: [number, number][][]) =>
|
||||
poly[0].map((coord: [number, number]) => [coord[1], coord[0]]),
|
||||
);
|
||||
}
|
||||
|
||||
return { lat, lon, polygon };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
(async () => {
|
||||
const result = await getCoords(initialValues.district.name);
|
||||
if (result) {
|
||||
setCoords({
|
||||
latitude: Number(initialValues.latitude),
|
||||
longitude: Number(initialValues.longitude),
|
||||
});
|
||||
setPolygonCoords(result.polygon);
|
||||
form.setValue("lat", String(result.lat));
|
||||
form.setValue("long", String(result.lon));
|
||||
setCircleCoords([
|
||||
Number(initialValues.latitude),
|
||||
Number(initialValues.longitude),
|
||||
]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
const handleMapClick = (
|
||||
e: ymaps.IEvent<MouseEvent, { coords: [number, number] }>,
|
||||
) => {
|
||||
const [lat, lon] = e.get("coords");
|
||||
setCoords({ latitude: lat, longitude: lon });
|
||||
form.setValue("lat", String(lat));
|
||||
form.setValue("long", String(lon));
|
||||
};
|
||||
|
||||
function onSubmit(values: z.infer<typeof DoctorForm>) {
|
||||
setLoad(true);
|
||||
console.log(values);
|
||||
setDialogOpen(false);
|
||||
if (initialValues) {
|
||||
edit({
|
||||
id: initialValues.id,
|
||||
body: {
|
||||
description: values.desc,
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
first_name: values.first_name,
|
||||
last_name: values.last_name,
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
phone_number: onlyNumber(values.phone_number),
|
||||
sphere: values.spec,
|
||||
work_place: values.work,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
mutate({
|
||||
description: values.desc,
|
||||
district_id: Number(values.district),
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
first_name: values.first_name,
|
||||
last_name: values.last_name,
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
phone_number: onlyNumber(values.phone_number),
|
||||
place_id: Number(values.object),
|
||||
sphere: values.spec,
|
||||
user_id: Number(values.user),
|
||||
work_place: values.work,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -164,98 +387,349 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="district"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Tuman</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Tumanlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fakeDistrict.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="object"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Obyekt</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Obyekt" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ObjectListData.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Foydalanuvchi</Label>
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const selectedUser = user?.results.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Foydalanuvchi</Label>
|
||||
|
||||
<Popover open={openUser} onOpenChange={setOpenUser}>
|
||||
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchilar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FakeUserList.map((e) => (
|
||||
<SelectItem value={String(e.id)}>
|
||||
{e.firstName} {e.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openUser}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedUser
|
||||
? `${selectedUser.first_name} ${selectedUser.last_name}`
|
||||
: "Foydalanuvchi tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchUser}
|
||||
onValueChange={setSearchUser}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{isUserLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : user && user.results.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{user.results.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={() => {
|
||||
field.onChange(String(u.id));
|
||||
setOpenUser(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Foydalanuvchi topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="district"
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const selectedDiscrit = discrit?.results.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Tumanlar</Label>
|
||||
|
||||
<Popover open={openDiscrit} onOpenChange={setOpenDiscrit}>
|
||||
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||
<FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openDiscrit}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedDiscrit
|
||||
? `${selectedDiscrit.name}`
|
||||
: "Tuman tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchDiscrit}
|
||||
onValueChange={setSearchDiscrit}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{discritLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : discrit && discrit.results.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{discrit.results.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={async () => {
|
||||
field.onChange(String(u.id));
|
||||
const selectedDistrict =
|
||||
discrit.results?.find(
|
||||
(d) => d.id === Number(u.id),
|
||||
);
|
||||
setOpenUser(false);
|
||||
|
||||
if (!selectedDistrict) return;
|
||||
|
||||
setSelectedDiscrit(selectedDistrict.name);
|
||||
|
||||
const coordsData = await getCoords(
|
||||
selectedDistrict?.name,
|
||||
);
|
||||
if (!coordsData) return;
|
||||
|
||||
setCoords({
|
||||
latitude: coordsData.lat,
|
||||
longitude: coordsData.lon,
|
||||
});
|
||||
setPolygonCoords(coordsData.polygon);
|
||||
|
||||
form.setValue("lat", String(coordsData.lat));
|
||||
form.setValue("long", String(coordsData.lon));
|
||||
setOpenDiscrit(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Tuman topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="object"
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const selectedObject = object?.results.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Obyektlar</Label>
|
||||
|
||||
<Popover open={openObject} onOpenChange={setOpenObject}>
|
||||
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||
<FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openDiscrit}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedObject
|
||||
? `${selectedObject.name}`
|
||||
: "Obyekt tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchObject}
|
||||
onValueChange={setSearchObject}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{isObjectLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : object && object.results.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{object.results.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={async () => {
|
||||
field.onChange(String(u.id));
|
||||
const selectedObject = object.results?.find(
|
||||
(d) => d.id === Number(u.id),
|
||||
);
|
||||
setOpenUser(false);
|
||||
|
||||
if (!selectedObject) return;
|
||||
|
||||
setCircleCoords([
|
||||
selectedObject.latitude,
|
||||
selectedObject.longitude,
|
||||
]);
|
||||
setCoords({
|
||||
latitude: selectedObject.latitude,
|
||||
longitude: selectedObject.longitude,
|
||||
});
|
||||
|
||||
form.setValue(
|
||||
"lat",
|
||||
String(selectedObject.latitude),
|
||||
);
|
||||
form.setValue(
|
||||
"long",
|
||||
String(selectedObject.longitude),
|
||||
);
|
||||
setOpenObject(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Obyekt topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<YMaps query={{ lang: "en_RU" }}>
|
||||
<Map
|
||||
defaultState={{ center: [Number(lat), Number(long)], zoom: 16 }}
|
||||
defaultState={{
|
||||
center: [coords.latitude, coords.longitude],
|
||||
zoom: 12,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="100%"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[Number(lat), Number(long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(lat), Number(long)], 100]}
|
||||
<ZoomControl
|
||||
options={{
|
||||
fillColor: "rgba(0,150,255,0.2)",
|
||||
strokeColor: "rgba(0,150,255,0.8)",
|
||||
position: { right: "10px", bottom: "70px" },
|
||||
}}
|
||||
/>
|
||||
<Placemark geometry={[coords.latitude, coords.longitude]} />
|
||||
{polygonCoords && (
|
||||
<Polygon
|
||||
geometry={polygonCoords}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{circleCoords && (
|
||||
<Circle
|
||||
geometry={[circleCoords, 300]}
|
||||
options={{
|
||||
fillColor: "rgba(255, 100, 0, 0.3)",
|
||||
strokeColor: "rgba(255, 100, 0, 0.8)",
|
||||
strokeWidth: 2,
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
@@ -263,7 +737,7 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
|
||||
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||
type="submit"
|
||||
>
|
||||
{load ? (
|
||||
{isPending || editPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
|
||||
90
src/features/doctors/ui/DeleteDoctor.tsx
Normal file
90
src/features/doctors/ui/DeleteDoctor.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { doctor_api } from "@/features/doctors/lib/api";
|
||||
import type { DoctorListResData } from "@/features/doctors/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Loader2, Trash, X } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setDiscritDelete: Dispatch<SetStateAction<DoctorListResData | null>>;
|
||||
discrit: DoctorListResData | null;
|
||||
}
|
||||
|
||||
const DeleteDoctor = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
setDiscritDelete,
|
||||
discrit,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteDiscrict, isPending } = useMutation({
|
||||
mutationFn: (id: number) => doctor_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["doctor_list"] });
|
||||
toast.success(`Shifokor o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setDiscritDelete(null);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tumanni o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "}
|
||||
ga tegishli {discrit?.first_name} {discrit?.last_name} shifokorni
|
||||
o'chirmoqchimisiz
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
|
||||
onClick={() => setOpenDelete(false)}
|
||||
>
|
||||
<X />
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => discrit && deleteDiscrict(discrit.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteDoctor;
|
||||
@@ -37,7 +37,9 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => {
|
||||
|
||||
const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]);
|
||||
|
||||
const [circleCoords] = useState<[number, number] | null>(null);
|
||||
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const getCoords = async (name: string): Promise<CoordsData | null> => {
|
||||
try {
|
||||
@@ -84,12 +86,11 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => {
|
||||
}
|
||||
|
||||
setCoords([object.latitude, object.longitude]);
|
||||
// setCircleCoords([object.latitude, object.longitude]);
|
||||
setCircleCoords([object.latitude, object.longitude]);
|
||||
};
|
||||
|
||||
load();
|
||||
}, [object]);
|
||||
|
||||
if (!object) return null;
|
||||
|
||||
return (
|
||||
@@ -154,10 +155,9 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Radius circle (ish joyi atrofida) */}
|
||||
{circleCoords && (
|
||||
<Circle
|
||||
geometry={[circleCoords, 500]}
|
||||
geometry={[circleCoords, 300]}
|
||||
options={{
|
||||
fillColor: "rgba(255, 100, 0, 0.3)",
|
||||
strokeColor: "rgba(255, 100, 0, 0.8)",
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { doctor_api } from "@/features/doctors/lib/api";
|
||||
import {
|
||||
type DoctorListResData,
|
||||
type DoctorListType,
|
||||
} from "@/features/doctors/lib/data";
|
||||
import { type DoctorListResData } from "@/features/doctors/lib/data";
|
||||
import DeleteDoctor from "@/features/doctors/ui/DeleteDoctor";
|
||||
import DoctorDetailDialog from "@/features/doctors/ui/DoctorDetailDialog";
|
||||
import FilterDoctor from "@/features/doctors/ui/FilterDoctor";
|
||||
import PaginationDoctor from "@/features/doctors/ui/PaginationDoctor";
|
||||
import TableDoctor from "@/features/doctors/ui/TableDoctor";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const DoctorsList = () => {
|
||||
const [detail, setDetail] = useState<DoctorListResData | null>(null);
|
||||
const [detailDialog, setDetailDialog] = useState<boolean>(false);
|
||||
const [editingPlan, setEditingPlan] = useState<DoctorListType | null>(null);
|
||||
const [editingPlan, setEditingPlan] = useState<DoctorListResData | null>(
|
||||
null,
|
||||
);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
|
||||
@@ -24,6 +24,11 @@ const DoctorsList = () => {
|
||||
const [searchSpec, setSearchSpec] = useState("");
|
||||
const [searchUser, setSearchUser] = useState("");
|
||||
|
||||
const [disricDelete, setDiscritDelete] = useState<DoctorListResData | null>(
|
||||
null,
|
||||
);
|
||||
const [opneDelete, setOpenDelete] = useState<boolean>(false);
|
||||
|
||||
const limit = 20;
|
||||
|
||||
const {
|
||||
@@ -58,8 +63,10 @@ const DoctorsList = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// const handleDelete = (id: number) => {
|
||||
// };
|
||||
const handleDelete = (user: DoctorListResData) => {
|
||||
setDiscritDelete(user);
|
||||
setOpenDelete(true);
|
||||
};
|
||||
|
||||
const totalPages = doctor ? Math.ceil(doctor.count / limit) : 1;
|
||||
|
||||
@@ -79,7 +86,6 @@ const DoctorsList = () => {
|
||||
searchSpec={searchSpec}
|
||||
searchUser={searchUser}
|
||||
searchWork={searchWork}
|
||||
// setData={setData}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
setSearchDistrict={setSearchDistrict}
|
||||
@@ -103,15 +109,25 @@ const DoctorsList = () => {
|
||||
isLoading={isLoading}
|
||||
doctor={doctor ? doctor.results : []}
|
||||
setDetail={setDetail}
|
||||
handleDelete={handleDelete}
|
||||
isFetching={isFetching}
|
||||
setDetailDialog={setDetailDialog}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
/>
|
||||
|
||||
<PaginationDoctor
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
|
||||
<DeleteDoctor
|
||||
discrit={disricDelete}
|
||||
opneDelete={opneDelete}
|
||||
setDiscritDelete={setDiscritDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DoctorListType } from "@/features/doctors/lib/data";
|
||||
import type { DoctorListResData } from "@/features/doctors/lib/data";
|
||||
import AddedDoctor from "@/features/doctors/ui/AddedDoctor";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -27,8 +27,8 @@ interface Props {
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
searchObject: string;
|
||||
setSearchObject: Dispatch<SetStateAction<string>>;
|
||||
setEditingPlan: Dispatch<SetStateAction<DoctorListType | null>>;
|
||||
editingPlan: DoctorListType | null;
|
||||
setEditingPlan: Dispatch<SetStateAction<DoctorListResData | null>>;
|
||||
editingPlan: DoctorListResData | null;
|
||||
}
|
||||
|
||||
const FilterDoctor = ({
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
currentPage: number;
|
||||
setCurrentPage: Dispatch<SetStateAction<number>>;
|
||||
totalPages: number;
|
||||
}
|
||||
|
||||
const PaginationDoctor = ({
|
||||
currentPage,
|
||||
setCurrentPage,
|
||||
totalPages,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaginationDoctor;
|
||||
@@ -15,11 +15,14 @@ import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
setDetail: Dispatch<SetStateAction<DoctorListResData | null>>;
|
||||
setEditingPlan: Dispatch<SetStateAction<DoctorListResData | null>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setDetailDialog: Dispatch<SetStateAction<boolean>>;
|
||||
doctor: DoctorListResData[] | [];
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isFetching: boolean;
|
||||
handleDelete: (user: DoctorListResData) => void;
|
||||
}
|
||||
|
||||
const TableDoctor = ({
|
||||
@@ -27,7 +30,10 @@ const TableDoctor = ({
|
||||
setDetail,
|
||||
setDetailDialog,
|
||||
isError,
|
||||
setEditingPlan,
|
||||
isLoading,
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
isFetching,
|
||||
}: Props) => {
|
||||
return (
|
||||
@@ -100,8 +106,8 @@ const TableDoctor = ({
|
||||
size="icon"
|
||||
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
|
||||
onClick={() => {
|
||||
// setEditingPlan(item);
|
||||
// setDialogOpen(true);
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
@@ -110,7 +116,7 @@ const TableDoctor = ({
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
// onClick={() => handleDelete(item.id)}
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
|
||||
74
src/features/location/ui/LocationFilter.tsx
Normal file
74
src/features/location/ui/LocationFilter.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
dateFilter: Date | undefined;
|
||||
setDateFilter: Dispatch<SetStateAction<Date | undefined>>;
|
||||
searchUser: string;
|
||||
setSearchUser: Dispatch<SetStateAction<string>>;
|
||||
}
|
||||
|
||||
const LocationFilter = ({
|
||||
open,
|
||||
setOpen,
|
||||
dateFilter,
|
||||
setDateFilter,
|
||||
searchUser,
|
||||
setSearchUser,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-48 justify-between font-normal h-12"
|
||||
>
|
||||
{dateFilter ? dateFilter.toDateString() : "Sana"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateFilter}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDateFilter(date);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="p-2 border-t bg-white">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setDateFilter(undefined);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Tozalash
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Foydalanuvchi ismi"
|
||||
className="h-12"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationFilter;
|
||||
@@ -3,27 +3,9 @@ import {
|
||||
type LocationListType,
|
||||
} from "@/features/location/lib/data";
|
||||
import LocationDetailDialog from "@/features/location/ui/LocationDetailDialog";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import LocationFilter from "@/features/location/ui/LocationFilter";
|
||||
import LocationTable from "@/features/location/ui/LocationTable";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const LocationList = () => {
|
||||
@@ -62,54 +44,14 @@ const LocationList = () => {
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||
<h1 className="text-2xl font-bold">Jo'natilgan lokatsiyalar</h1>
|
||||
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-48 justify-between font-normal h-12"
|
||||
>
|
||||
{dateFilter ? dateFilter.toDateString() : "Sana"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateFilter}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDateFilter(date);
|
||||
setOpen(false);
|
||||
}}
|
||||
<LocationFilter
|
||||
dateFilter={dateFilter}
|
||||
open={open}
|
||||
searchUser={searchUser}
|
||||
setDateFilter={setDateFilter}
|
||||
setOpen={setOpen}
|
||||
setSearchUser={setSearchUser}
|
||||
/>
|
||||
<div className="p-2 border-t bg-white">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setDateFilter(undefined);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Tozalash
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Foydalanuvchi ismi"
|
||||
className="h-12"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LocationDetailDialog
|
||||
detail={detailDialog}
|
||||
@@ -118,105 +60,18 @@ const LocationList = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Jo'natgan foydalanuvchi</TableHead>
|
||||
<TableHead>Jo'natgan vaqti</TableHead>
|
||||
<TableHead>Qayerdan jo'natdi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(item.createdAt, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<LocationTable
|
||||
filtered={filtered}
|
||||
handleDelete={handleDelete}
|
||||
setDetail={setDetail}
|
||||
setDetailDialog={setDetailDialog}
|
||||
/>
|
||||
|
||||
<TableCell>
|
||||
{item.district
|
||||
? "Tuman"
|
||||
: item.object
|
||||
? "Obyekt"
|
||||
: item.doctor
|
||||
? "Shifokor"
|
||||
: item.pharmcies
|
||||
? "Dorixona"
|
||||
: "Turgan joyidan"}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
92
src/features/location/ui/LocationTable.tsx
Normal file
92
src/features/location/ui/LocationTable.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { LocationListType } from "@/features/location/lib/data";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { Eye, Trash2 } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
filtered: LocationListType[];
|
||||
setDetail: Dispatch<SetStateAction<LocationListType | null>>;
|
||||
setDetailDialog: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (id: number) => void;
|
||||
}
|
||||
|
||||
const LocationTable = ({
|
||||
filtered,
|
||||
setDetail,
|
||||
setDetailDialog,
|
||||
handleDelete,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Jo'natgan foydalanuvchi</TableHead>
|
||||
<TableHead>Jo'natgan vaqti</TableHead>
|
||||
<TableHead>Qayerdan jo'natdi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(item.createdAt, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{item.district
|
||||
? "Tuman"
|
||||
: item.object
|
||||
? "Obyekt"
|
||||
: item.doctor
|
||||
? "Shifokor"
|
||||
: item.pharmcies
|
||||
? "Dorixona"
|
||||
: "Turgan joyidan"}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationTable;
|
||||
36
src/features/objects/lib/api.ts
Normal file
36
src/features/objects/lib/api.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
ObjectCreate,
|
||||
ObjectListRes,
|
||||
ObjectUpdate,
|
||||
} from "@/features/objects/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { OBJECT } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const object_api = {
|
||||
async list(params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
name?: string;
|
||||
district?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<ObjectListRes>> {
|
||||
const res = await httpClient.get(`${OBJECT}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: ObjectCreate) {
|
||||
const res = await httpClient.post(`${OBJECT}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: ObjectUpdate }) {
|
||||
const res = await httpClient.patch(`${OBJECT}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${OBJECT}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -31,3 +31,52 @@ export const ObjectListData: ObjectListType[] = [
|
||||
moreLong: ["41.2851", "69.2043"],
|
||||
},
|
||||
];
|
||||
|
||||
export interface ObjectListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: string | null;
|
||||
previous: string | null;
|
||||
results: ObjectListData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ObjectListData {
|
||||
id: number;
|
||||
name: string;
|
||||
district: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
user: {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface ObjectCreate {
|
||||
district_id: number;
|
||||
user_id: number;
|
||||
name: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: { longitude: number; latitude: number };
|
||||
}
|
||||
|
||||
export interface ObjectUpdate {
|
||||
name: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: { longitude: number; latitude: number };
|
||||
}
|
||||
|
||||
@@ -1,8 +1,22 @@
|
||||
import { fakeDistrict } from "@/features/districts/lib/data";
|
||||
import type { ObjectListType } from "@/features/objects/lib/data";
|
||||
import { discrit_api } from "@/features/districts/lib/api";
|
||||
import { object_api } from "@/features/objects/lib/api";
|
||||
import type {
|
||||
ObjectCreate,
|
||||
ObjectListData,
|
||||
ObjectUpdate,
|
||||
} from "@/features/objects/lib/data";
|
||||
import { ObjectForm } from "@/features/objects/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/shared/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -12,77 +26,225 @@ import {
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import {
|
||||
Circle,
|
||||
Map,
|
||||
Placemark,
|
||||
Polygon,
|
||||
YMaps,
|
||||
ZoomControl,
|
||||
} from "@pbe/react-yandex-maps";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: ObjectListType | null;
|
||||
initialValues: ObjectListData | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setData: Dispatch<SetStateAction<ObjectListType[]>>;
|
||||
}
|
||||
|
||||
export default function AddedObject({
|
||||
initialValues,
|
||||
setDialogOpen,
|
||||
setData,
|
||||
}: Props) {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
interface CoordsData {
|
||||
lat: number;
|
||||
lon: number;
|
||||
polygon: [number, number][][];
|
||||
}
|
||||
|
||||
export default function AddedObject({ initialValues, setDialogOpen }: Props) {
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
const [searchDiscrit, setSearchDiscrit] = useState<string>("");
|
||||
const queryClient = useQueryClient();
|
||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||
const [openDiscrit, setOpenDiscrit] = useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof ObjectForm>>({
|
||||
resolver: zodResolver(ObjectForm),
|
||||
defaultValues: {
|
||||
lat: initialValues?.lat || "41.2949",
|
||||
long: initialValues?.long || "69.2361",
|
||||
lat: initialValues ? String(initialValues?.latitude) : "41.2949",
|
||||
long: initialValues ? String(initialValues?.longitude) : "69.2361",
|
||||
name: initialValues?.name || "",
|
||||
user: initialValues ? String(initialValues.user.id) : "",
|
||||
district: initialValues ? String(initialValues.district.id) : "",
|
||||
},
|
||||
});
|
||||
|
||||
const lat = form.watch("lat");
|
||||
const long = form.watch("long");
|
||||
|
||||
const handleMapClick = (e: { get: (key: string) => number[] }) => {
|
||||
const coords = e.get("coords");
|
||||
form.setValue("lat", coords[0].toString());
|
||||
form.setValue("long", coords[1].toString());
|
||||
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 user_id = form.watch("user");
|
||||
|
||||
const { data: discrit, isLoading: discritLoading } = useQuery({
|
||||
queryKey: ["discrit_list", searchDiscrit, user_id],
|
||||
queryFn: () => {
|
||||
const params: {
|
||||
name?: string;
|
||||
user?: number;
|
||||
} = {
|
||||
name: searchDiscrit,
|
||||
};
|
||||
|
||||
if (user_id !== "") {
|
||||
params.user = Number(user_id);
|
||||
}
|
||||
|
||||
return discrit_api.list(params);
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const [coords, setCoords] = useState({
|
||||
latitude: 41.311081,
|
||||
longitude: 69.240562,
|
||||
});
|
||||
const [polygonCoords, setPolygonCoords] = useState<
|
||||
[number, number][][] | null
|
||||
>(null);
|
||||
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const getCoords = async (name: string): Promise<CoordsData | null> => {
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(name)}&format=json&polygon_geojson=1&limit=1`,
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.length > 0 && data[0].geojson) {
|
||||
const lat = parseFloat(data[0].lat);
|
||||
const lon = parseFloat(data[0].lon);
|
||||
|
||||
let polygon: [number, number][][] = [];
|
||||
|
||||
if (data[0].geojson.type === "Polygon") {
|
||||
polygon = data[0].geojson.coordinates.map((ring: [number, number][]) =>
|
||||
ring.map((coord: [number, number]) => [coord[1], coord[0]]),
|
||||
);
|
||||
} else if (data[0].geojson.type === "MultiPolygon") {
|
||||
polygon = data[0].geojson.coordinates.map(
|
||||
(poly: [number, number][][]) =>
|
||||
poly[0].map((coord: [number, number]) => [coord[1], coord[0]]),
|
||||
);
|
||||
}
|
||||
|
||||
return { lat, lon, polygon };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
(async () => {
|
||||
const result = await getCoords(initialValues.district.name);
|
||||
if (result) {
|
||||
setCoords({
|
||||
latitude: initialValues.latitude,
|
||||
longitude: initialValues.longitude,
|
||||
});
|
||||
setPolygonCoords(result.polygon);
|
||||
form.setValue("lat", String(result.lat));
|
||||
form.setValue("long", String(result.lon));
|
||||
setCircleCoords([initialValues.latitude, initialValues.longitude]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
const handleMapClick = (
|
||||
e: ymaps.IEvent<MouseEvent, { coords: [number, number] }>,
|
||||
) => {
|
||||
const [lat, lon] = e.get("coords");
|
||||
setCoords({ latitude: lat, longitude: lon });
|
||||
form.setValue("lat", String(lat));
|
||||
form.setValue("long", String(lon));
|
||||
};
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: ObjectCreate) => object_api.create(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["object_list"] });
|
||||
toast.success(`Obyekt qo'shildi`);
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: edit, isPending: editPending } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: number; body: ObjectUpdate }) =>
|
||||
object_api.update({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["object_list"] });
|
||||
toast.success(`Obyekt qo'shildi`);
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof ObjectForm>) {
|
||||
setLoad(true);
|
||||
const newObject: ObjectListType = {
|
||||
id: initialValues ? initialValues.id : Date.now(),
|
||||
name: values.name,
|
||||
lat: values.lat,
|
||||
long: values.long,
|
||||
moreLong: [values.long, values.lat],
|
||||
user: FakeUserList.find((u) => u.id === Number(values.user))!,
|
||||
district: fakeDistrict.find((d) => d.id === Number(values.district))!,
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setData((prev) => {
|
||||
if (initialValues) {
|
||||
return prev.map((item) =>
|
||||
item.id === initialValues.id ? newObject : item,
|
||||
);
|
||||
} else {
|
||||
return [...prev, newObject];
|
||||
}
|
||||
edit({
|
||||
body: {
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
name: values.name,
|
||||
},
|
||||
id: initialValues.id,
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
mutate({
|
||||
district_id: Number(values.district),
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
name: values.name,
|
||||
user_id: Number(values.user),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -103,69 +265,217 @@ export default function AddedObject({
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="user"
|
||||
control={form.control}
|
||||
name="district"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Tuman</Label>
|
||||
render={({ field }) => {
|
||||
const selectedUser = user?.results.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Foydalanuvchi</Label>
|
||||
|
||||
<Popover open={openUser} onOpenChange={setOpenUser}>
|
||||
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Tumanlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fakeDistrict.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openUser}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedUser
|
||||
? `${selectedUser.first_name} ${selectedUser.last_name}`
|
||||
: "Foydalanuvchi tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchUser}
|
||||
onValueChange={setSearchUser}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{isUserLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : user && user.results.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{user.results.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={() => {
|
||||
field.onChange(String(u.id));
|
||||
setOpenUser(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Foydalanuvchi topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="district"
|
||||
control={form.control}
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Foydalanuvchi</Label>
|
||||
render={({ field }) => {
|
||||
const selectedDiscrit = discrit?.results.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Tumanlar</Label>
|
||||
|
||||
<Popover open={openDiscrit} onOpenChange={setOpenDiscrit}>
|
||||
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchilar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FakeUserList.map((e) => (
|
||||
<SelectItem value={String(e.id)}>
|
||||
{e.firstName} {e.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openDiscrit}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedDiscrit
|
||||
? `${selectedDiscrit.name}`
|
||||
: "Tuman tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchDiscrit}
|
||||
onValueChange={setSearchDiscrit}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{discritLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : discrit && discrit.results.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{discrit.results.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={async () => {
|
||||
field.onChange(String(u.id));
|
||||
const selectedDistrict =
|
||||
discrit.results?.find(
|
||||
(d) => d.id === Number(u.id),
|
||||
);
|
||||
setOpenUser(false);
|
||||
|
||||
if (!selectedDistrict) return;
|
||||
|
||||
const coordsData = await getCoords(
|
||||
selectedDistrict?.name,
|
||||
);
|
||||
if (!coordsData) return;
|
||||
|
||||
setCoords({
|
||||
latitude: coordsData.lat,
|
||||
longitude: coordsData.lon,
|
||||
});
|
||||
setPolygonCoords(coordsData.polygon);
|
||||
|
||||
form.setValue("lat", String(coordsData.lat));
|
||||
form.setValue("long", String(coordsData.lon));
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Tuman topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<YMaps query={{ lang: "en_RU" }}>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(lat), Number(long)],
|
||||
zoom: 16,
|
||||
center: [coords.latitude, coords.longitude],
|
||||
zoom: 12,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="100%"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[Number(lat), Number(long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(lat), Number(long)], 100]}
|
||||
<ZoomControl
|
||||
options={{
|
||||
position: { right: "10px", bottom: "70px" },
|
||||
}}
|
||||
/>
|
||||
<Placemark geometry={[coords.latitude, coords.longitude]} />
|
||||
{polygonCoords && (
|
||||
<Polygon
|
||||
geometry={polygonCoords}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
@@ -173,6 +483,18 @@ export default function AddedObject({
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{circleCoords && (
|
||||
<Circle
|
||||
geometry={[circleCoords, 300]}
|
||||
options={{
|
||||
fillColor: "rgba(255, 100, 0, 0.3)",
|
||||
strokeColor: "rgba(255, 100, 0, 0.8)",
|
||||
strokeWidth: 2,
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
@@ -180,7 +502,7 @@ export default function AddedObject({
|
||||
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||
type="submit"
|
||||
>
|
||||
{load ? (
|
||||
{isPending || editPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
|
||||
89
src/features/objects/ui/DeleteObject.tsx
Normal file
89
src/features/objects/ui/DeleteObject.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { object_api } from "@/features/objects/lib/api";
|
||||
import type { ObjectListData } from "@/features/objects/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Loader2, Trash, X } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setDiscritDelete: Dispatch<SetStateAction<ObjectListData | null>>;
|
||||
discrit: ObjectListData | null;
|
||||
}
|
||||
|
||||
const DeleteObject = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
setDiscritDelete,
|
||||
discrit,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteDiscrict, isPending } = useMutation({
|
||||
mutationFn: (id: number) => object_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["object_list"] });
|
||||
toast.success(`Tuman o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setDiscritDelete(null);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tumanni o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "}
|
||||
ga tegishli {discrit?.name} obyektni o'chirmoqchimisiz
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
|
||||
onClick={() => setOpenDelete(false)}
|
||||
>
|
||||
<X />
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => discrit && deleteDiscrict(discrit.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteObject;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ObjectListType } from "@/features/objects/lib/data";
|
||||
import type { ObjectListData } from "@/features/objects/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent } from "@/shared/ui/card";
|
||||
import {
|
||||
@@ -8,16 +8,89 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import {
|
||||
Circle,
|
||||
Map,
|
||||
Placemark,
|
||||
Polygon,
|
||||
YMaps,
|
||||
ZoomControl,
|
||||
} from "@pbe/react-yandex-maps";
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
object: ObjectListType | null;
|
||||
object: ObjectListData | null;
|
||||
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||
detail: boolean;
|
||||
}
|
||||
|
||||
interface CoordsData {
|
||||
lat: number;
|
||||
lon: number;
|
||||
polygon: [number, number][][];
|
||||
}
|
||||
|
||||
const ObjectDetailDialog = ({ object, detail, setDetail }: Props) => {
|
||||
const [coords, setCoords] = useState<[number, number]>([
|
||||
41.311081, 69.240562,
|
||||
]);
|
||||
|
||||
const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]);
|
||||
|
||||
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const getCoords = async (name: string): Promise<CoordsData | null> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
|
||||
name,
|
||||
)}&format=json&polygon_geojson=1&limit=1`,
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.length || !data[0].geojson) return null;
|
||||
|
||||
const lat = parseFloat(data[0].lat);
|
||||
const lon = parseFloat(data[0].lon);
|
||||
|
||||
let polygon: [number, number][][] = [];
|
||||
|
||||
if (data[0].geojson.type === "Polygon") {
|
||||
polygon = data[0].geojson.coordinates.map((ring: []) =>
|
||||
ring.map((c) => [c[1], c[0]]),
|
||||
);
|
||||
}
|
||||
|
||||
if (data[0].geojson.type === "MultiPolygon") {
|
||||
polygon = data[0].geojson.coordinates[0].map((ring: []) =>
|
||||
ring.map((c) => [c[1], c[0]]),
|
||||
);
|
||||
}
|
||||
|
||||
return { lat, lon, polygon };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!object) return;
|
||||
|
||||
const load = async () => {
|
||||
const district = await getCoords(object.district.name);
|
||||
|
||||
if (district) {
|
||||
setPolygonCoords(district.polygon);
|
||||
}
|
||||
|
||||
setCoords([object.latitude, object.longitude]);
|
||||
setCircleCoords([object.latitude, object.longitude]);
|
||||
};
|
||||
|
||||
load();
|
||||
}, [object]);
|
||||
if (!object) return null;
|
||||
|
||||
return (
|
||||
@@ -35,30 +108,52 @@ const ObjectDetailDialog = ({ object, detail, setDetail }: Props) => {
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Foydalanuvchi:</span>{" "}
|
||||
{object.user.firstName} {object.user.lastName}
|
||||
{object.user.first_name} {object.user.last_name}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<YMaps query={{ lang: "en_RU" }}>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(object.lat), Number(object.long)],
|
||||
zoom: 16,
|
||||
state={{
|
||||
center: coords,
|
||||
zoom: 12,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="100%"
|
||||
>
|
||||
<Placemark geometry={[Number(object.lat), Number(object.long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(object.lat), Number(object.long)], 100]}
|
||||
<ZoomControl
|
||||
options={{
|
||||
position: { right: "10px", bottom: "70px" },
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Ish joyining markazi */}
|
||||
<Placemark geometry={coords} />
|
||||
|
||||
{/* Tuman polygon */}
|
||||
{polygonCoords.length > 0 && (
|
||||
<Polygon
|
||||
geometry={polygonCoords}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{circleCoords && (
|
||||
<Circle
|
||||
geometry={[circleCoords, 300]}
|
||||
options={{
|
||||
fillColor: "rgba(255, 100, 0, 0.3)",
|
||||
strokeColor: "rgba(255, 100, 0, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
|
||||
88
src/features/objects/ui/ObjectFilter.tsx
Normal file
88
src/features/objects/ui/ObjectFilter.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { ObjectListData } from "@/features/objects/lib/data";
|
||||
import AddedObject from "@/features/objects/ui/AddedObject";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Plus } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
searchName: string;
|
||||
setSearchName: Dispatch<SetStateAction<string>>;
|
||||
searchDistrict: string;
|
||||
setSearchDistrict: Dispatch<SetStateAction<string>>;
|
||||
searchUser: string;
|
||||
setSearchUser: Dispatch<SetStateAction<string>>;
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setEditingPlan: Dispatch<SetStateAction<ObjectListData | null>>;
|
||||
editingPlan: ObjectListData | null;
|
||||
}
|
||||
|
||||
const ObjectFilter = ({
|
||||
searchName,
|
||||
setSearchName,
|
||||
searchDistrict,
|
||||
setSearchDistrict,
|
||||
searchUser,
|
||||
setSearchUser,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
editingPlan,
|
||||
setEditingPlan,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex gap-2 flex-wrap w-full md:w-auto">
|
||||
<Input
|
||||
placeholder="Obyekt nomi"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Tuman"
|
||||
value={searchDistrict}
|
||||
onChange={(e) => setSearchDistrict(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Foydalanuvchi"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan ? "Obyektni tahrirlash" : "Yangi obyekt qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddedObject
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectFilter;
|
||||
@@ -1,126 +1,78 @@
|
||||
import {
|
||||
ObjectListData,
|
||||
type ObjectListType,
|
||||
} from "@/features/objects/lib/data";
|
||||
import AddedObject from "@/features/objects/ui/AddedObject";
|
||||
import { object_api } from "@/features/objects/lib/api";
|
||||
import { ObjectListData } from "@/features/objects/lib/data";
|
||||
import DeleteObject from "@/features/objects/ui/DeleteObject";
|
||||
import ObjectDetailDialog from "@/features/objects/ui/ObjectDetail";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import ObjectFilter from "@/features/objects/ui/ObjectFilter";
|
||||
import ObjectTable from "@/features/objects/ui/ObjectTable";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function ObjectList() {
|
||||
const [data, setData] = useState<ObjectListType[]>(ObjectListData);
|
||||
const [detail, setDetail] = useState<ObjectListType | null>(null);
|
||||
const [detail, setDetail] = useState<ObjectListData | null>(null);
|
||||
const [detailDialog, setDetailDialog] = useState<boolean>(false);
|
||||
const [editingPlan, setEditingPlan] = useState<ObjectListType | null>(null);
|
||||
const [editingPlan, setEditingPlan] = useState<ObjectListData | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const limit = 20;
|
||||
|
||||
// Filter state
|
||||
const [searchName, setSearchName] = useState("");
|
||||
const [searchDistrict, setSearchDistrict] = useState("");
|
||||
const [searchUser, setSearchUser] = useState("");
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setData((prev) => prev.filter((e) => e.id !== id));
|
||||
const [disricDelete, setDiscritDelete] = useState<ObjectListData | null>(
|
||||
null,
|
||||
);
|
||||
const [opneDelete, setOpenDelete] = useState<boolean>(false);
|
||||
|
||||
const handleDelete = (user: ObjectListData) => {
|
||||
setDiscritDelete(user);
|
||||
setOpenDelete(true);
|
||||
};
|
||||
|
||||
// Filtered data
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter((item) => {
|
||||
const nameMatch = item.name
|
||||
.toLowerCase()
|
||||
.includes(searchName.toLowerCase());
|
||||
const districtMatch = item.district.name
|
||||
.toLowerCase()
|
||||
.includes(searchDistrict.toLowerCase());
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return nameMatch && districtMatch && userMatch;
|
||||
const {
|
||||
data: object,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: [
|
||||
"object_list",
|
||||
searchDistrict,
|
||||
currentPage,
|
||||
searchName,
|
||||
searchUser,
|
||||
],
|
||||
queryFn: () =>
|
||||
object_api.list({
|
||||
district: searchDistrict,
|
||||
limit,
|
||||
offset: (currentPage - 1) * limit,
|
||||
name: searchName,
|
||||
user: searchUser,
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
}, [data, searchName, searchDistrict, searchUser]);
|
||||
|
||||
const totalPages = object ? Math.ceil(object.count / 20) : 1;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||
<h1 className="text-2xl font-bold">Obyektlarni boshqarish</h1>
|
||||
|
||||
<div className="flex gap-2 flex-wrap w-full md:w-auto">
|
||||
<Input
|
||||
placeholder="Obyekt nomi"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Tuman"
|
||||
value={searchDistrict}
|
||||
onChange={(e) => setSearchDistrict(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Foydalanuvchi"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan
|
||||
? "Obyektni tahrirlash"
|
||||
: "Yangi obyekt qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddedObject
|
||||
initialValues={editingPlan}
|
||||
<ObjectFilter
|
||||
dialogOpen={dialogOpen}
|
||||
editingPlan={editingPlan}
|
||||
searchDistrict={searchDistrict}
|
||||
searchName={searchName}
|
||||
searchUser={searchUser}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setData={setData}
|
||||
setEditingPlan={setEditingPlan}
|
||||
setSearchDistrict={setSearchDistrict}
|
||||
setSearchName={setSearchName}
|
||||
setSearchUser={setSearchUser}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<ObjectDetailDialog
|
||||
detail={detailDialog}
|
||||
setDetail={setDetailDialog}
|
||||
@@ -128,104 +80,29 @@ export default function ObjectList() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Obyekt nomi</TableHead>
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Foydalanuvchi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell className="font-medium">{item.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{item.district.name}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<ObjectTable
|
||||
filteredData={object ? object.results : []}
|
||||
handleDelete={handleDelete}
|
||||
isError={isError}
|
||||
isLoading={isLoading}
|
||||
setDetail={setDetail}
|
||||
setDetailDialog={setDetailDialog}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
/>
|
||||
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
|
||||
<DeleteObject
|
||||
discrit={disricDelete}
|
||||
opneDelete={opneDelete}
|
||||
setDiscritDelete={setDiscritDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
124
src/features/objects/ui/ObjectTable.tsx
Normal file
124
src/features/objects/ui/ObjectTable.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import type { ObjectListData } from "@/features/objects/lib/data";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { Eye, Loader2, Pencil, Trash2 } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
filteredData: ObjectListData[] | [];
|
||||
setDetail: Dispatch<SetStateAction<ObjectListData | null>>;
|
||||
setDetailDialog: Dispatch<SetStateAction<boolean>>;
|
||||
setEditingPlan: Dispatch<SetStateAction<ObjectListData | null>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (object: ObjectListData) => void;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
}
|
||||
|
||||
const ObjectTable = ({
|
||||
filteredData,
|
||||
setDetail,
|
||||
setDetailDialog,
|
||||
setEditingPlan,
|
||||
handleDelete,
|
||||
setDialogOpen,
|
||||
isError,
|
||||
isLoading,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{isLoading && (
|
||||
<div className="h-full flex items-center justify-center bg-white/70 z-10">
|
||||
<span className="text-lg font-medium">
|
||||
<Loader2 className="animate-spin" />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<div className="h-full flex items-center justify-center z-10">
|
||||
<span className="text-lg font-medium text-red-600">
|
||||
Ma'lumotlarni olishda xatolik yuz berdi.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{!isError && !isLoading && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Obyekt nomi</TableHead>
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Foydalanuvchi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData && filteredData.length > 0 ? (
|
||||
filteredData.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell className="font-medium">{item.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{item.district.name}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.user.first_name} {item.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center py-4 text-lg">
|
||||
Obyekt topilmadi.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectTable;
|
||||
37
src/features/pharmacies/lib/api.ts
Normal file
37
src/features/pharmacies/lib/api.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type {
|
||||
CreatePharmaciesReq,
|
||||
PharmaciesListRes,
|
||||
UpdatePharmaciesReq,
|
||||
} from "@/features/pharmacies/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { PHARMACIES } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const pharmacies_api = {
|
||||
async list(params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
name?: string;
|
||||
place?: string;
|
||||
district?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<PharmaciesListRes>> {
|
||||
const res = await httpClient.get(`${PHARMACIES}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: CreatePharmaciesReq) {
|
||||
const res = await httpClient.post(`${PHARMACIES}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: UpdatePharmaciesReq }) {
|
||||
const res = await httpClient.patch(`${PHARMACIES}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${PHARMACIES}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -80,3 +80,66 @@ export const PharmciesData: PharmciesType[] = [
|
||||
lat: "41.3",
|
||||
},
|
||||
];
|
||||
|
||||
export interface PharmaciesListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: PharmaciesListData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PharmaciesListData {
|
||||
id: number;
|
||||
name: string;
|
||||
inn: string;
|
||||
owner_phone: string;
|
||||
responsible_phone: string;
|
||||
district: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
place: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
user: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CreatePharmaciesReq {
|
||||
name: string;
|
||||
inn: string;
|
||||
owner_phone: string;
|
||||
responsible_phone: string;
|
||||
district_id: number;
|
||||
place_id: number;
|
||||
user_id: number;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: { longitude: number; latitude: number };
|
||||
}
|
||||
|
||||
export interface UpdatePharmaciesReq {
|
||||
name: string;
|
||||
inn: string;
|
||||
owner_phone: string;
|
||||
responsible_phone: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: { longitude: number; latitude: number };
|
||||
}
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
import { fakeDistrict } from "@/features/districts/lib/data";
|
||||
import { ObjectListData } from "@/features/objects/lib/data";
|
||||
import type { PharmciesType } from "@/features/pharmacies/lib/data";
|
||||
import { discrit_api } from "@/features/districts/lib/api";
|
||||
import { object_api } from "@/features/objects/lib/api";
|
||||
import { pharmacies_api } from "@/features/pharmacies/lib/api";
|
||||
import type {
|
||||
CreatePharmaciesReq,
|
||||
PharmaciesListData,
|
||||
UpdatePharmaciesReq,
|
||||
} from "@/features/pharmacies/lib/data";
|
||||
import { PharmForm } from "@/features/pharmacies/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import onlyNumber from "@/shared/lib/onlyNumber";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/shared/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -15,80 +29,258 @@ import {
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import {
|
||||
Circle,
|
||||
Map,
|
||||
Placemark,
|
||||
Polygon,
|
||||
YMaps,
|
||||
ZoomControl,
|
||||
} from "@pbe/react-yandex-maps";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: PharmciesType | null;
|
||||
initialValues: PharmaciesListData | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setData: Dispatch<SetStateAction<PharmciesType[]>>;
|
||||
}
|
||||
|
||||
const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
interface CoordsData {
|
||||
lat: number;
|
||||
lon: number;
|
||||
polygon: [number, number][][];
|
||||
}
|
||||
|
||||
const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
const [searchObject, setSearchObject] = useState<string>("");
|
||||
const [selectDiscrit, setSelectedDiscrit] = useState<string>("");
|
||||
const [searchDiscrit, setSearchDiscrit] = useState<string>("");
|
||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||
const [openDiscrit, setOpenDiscrit] = useState<boolean>(false);
|
||||
const [openObject, setOpenObject] = useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof PharmForm>>({
|
||||
resolver: zodResolver(PharmForm),
|
||||
defaultValues: {
|
||||
additional_phone: initialValues?.additional_phone || "+998",
|
||||
additional_phone: initialValues?.responsible_phone || "+998",
|
||||
district: initialValues?.district.id.toString() || "",
|
||||
inn: initialValues?.inn || "",
|
||||
lat: initialValues?.lat || "41.2949",
|
||||
long: initialValues?.long || "69.2361",
|
||||
lat: String(initialValues?.latitude) || "41.2949",
|
||||
long: String(initialValues?.longitude) || "69.2361",
|
||||
name: initialValues?.name || "",
|
||||
object: initialValues?.object.id.toString() || "",
|
||||
phone_number: initialValues?.phone_number || "+998",
|
||||
object: initialValues?.place.id.toString() || "",
|
||||
phone_number: initialValues?.owner_phone || "+998",
|
||||
user: initialValues?.user.id.toString() || "",
|
||||
},
|
||||
});
|
||||
|
||||
const lat = form.watch("lat");
|
||||
const long = form.watch("long");
|
||||
|
||||
const handleMapClick = (e: { get: (key: string) => number[] }) => {
|
||||
const coords = e.get("coords");
|
||||
form.setValue("lat", coords[0].toString());
|
||||
form.setValue("long", coords[1].toString());
|
||||
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 { data: object, isLoading: isObjectLoading } = useQuery({
|
||||
queryKey: ["object_list", searchUser, selectDiscrit],
|
||||
queryFn: () => {
|
||||
const params: {
|
||||
name?: string;
|
||||
district?: string;
|
||||
} = {
|
||||
name: searchUser,
|
||||
district: selectDiscrit,
|
||||
};
|
||||
|
||||
return object_api.list(params);
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const user_id = form.watch("user");
|
||||
|
||||
const { data: discrit, isLoading: discritLoading } = useQuery({
|
||||
queryKey: ["discrit_list", searchDiscrit, user_id],
|
||||
queryFn: () => {
|
||||
const params: {
|
||||
name?: string;
|
||||
user?: number;
|
||||
} = {
|
||||
name: searchDiscrit,
|
||||
};
|
||||
|
||||
if (user_id !== "") {
|
||||
params.user = Number(user_id);
|
||||
}
|
||||
|
||||
return discrit_api.list(params);
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const [coords, setCoords] = useState({
|
||||
latitude: 41.311081,
|
||||
longitude: 69.240562,
|
||||
});
|
||||
const [polygonCoords, setPolygonCoords] = useState<
|
||||
[number, number][][] | null
|
||||
>(null);
|
||||
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const getCoords = async (name: string): Promise<CoordsData | null> => {
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(name)}&format=json&polygon_geojson=1&limit=1`,
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (data.length > 0 && data[0].geojson) {
|
||||
const lat = parseFloat(data[0].lat);
|
||||
const lon = parseFloat(data[0].lon);
|
||||
|
||||
let polygon: [number, number][][] = [];
|
||||
|
||||
if (data[0].geojson.type === "Polygon") {
|
||||
polygon = data[0].geojson.coordinates.map((ring: [number, number][]) =>
|
||||
ring.map((coord: [number, number]) => [coord[1], coord[0]]),
|
||||
);
|
||||
} else if (data[0].geojson.type === "MultiPolygon") {
|
||||
polygon = data[0].geojson.coordinates.map(
|
||||
(poly: [number, number][][]) =>
|
||||
poly[0].map((coord: [number, number]) => [coord[1], coord[0]]),
|
||||
);
|
||||
}
|
||||
|
||||
return { lat, lon, polygon };
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
(async () => {
|
||||
const result = await getCoords(initialValues.district.name);
|
||||
if (result) {
|
||||
setCoords({
|
||||
latitude: Number(initialValues.latitude),
|
||||
longitude: Number(initialValues.longitude),
|
||||
});
|
||||
setPolygonCoords(result.polygon);
|
||||
form.setValue("lat", String(result.lat));
|
||||
form.setValue("long", String(result.lon));
|
||||
setCircleCoords([
|
||||
Number(initialValues.latitude),
|
||||
Number(initialValues.longitude),
|
||||
]);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
const handleMapClick = (
|
||||
e: ymaps.IEvent<MouseEvent, { coords: [number, number] }>,
|
||||
) => {
|
||||
const [lat, lon] = e.get("coords");
|
||||
setCoords({ latitude: lat, longitude: lon });
|
||||
form.setValue("lat", String(lat));
|
||||
form.setValue("long", String(lon));
|
||||
};
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: CreatePharmaciesReq) => pharmacies_api.create(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["pharmacies_list"] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: edit, isPending: editPending } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: number; body: UpdatePharmaciesReq }) =>
|
||||
pharmacies_api.update({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["pharmacies_list"] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof PharmForm>) {
|
||||
setLoad(true);
|
||||
const newObject: PharmciesType = {
|
||||
id: initialValues ? initialValues.id : Date.now(),
|
||||
name: values.name,
|
||||
lat: values.lat,
|
||||
long: values.long,
|
||||
user: FakeUserList.find((u) => u.id === Number(values.user))!,
|
||||
district: fakeDistrict.find((d) => d.id === Number(values.district))!,
|
||||
additional_phone: onlyNumber(values.additional_phone),
|
||||
inn: values.inn,
|
||||
object: ObjectListData.find((o) => o.id === Number(values.object))!,
|
||||
phone_number: onlyNumber(values.phone_number),
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setData((prev) => {
|
||||
if (initialValues) {
|
||||
return prev.map((item) =>
|
||||
item.id === initialValues.id ? newObject : item,
|
||||
);
|
||||
} else {
|
||||
return [...prev, newObject];
|
||||
}
|
||||
edit({
|
||||
id: initialValues.id,
|
||||
body: {
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
inn: values.inn,
|
||||
name: values.name,
|
||||
owner_phone: onlyNumber(values.phone_number),
|
||||
responsible_phone: onlyNumber(values.additional_phone),
|
||||
},
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
mutate({
|
||||
district_id: Number(values.district),
|
||||
extra_location: {
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
},
|
||||
latitude: Number(values.lat),
|
||||
longitude: Number(values.long),
|
||||
inn: values.inn,
|
||||
name: values.name,
|
||||
owner_phone: onlyNumber(values.phone_number),
|
||||
place_id: Number(values.object),
|
||||
responsible_phone: onlyNumber(values.additional_phone),
|
||||
user_id: Number(values.user),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -159,94 +351,330 @@ const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => {
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="district"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Tuman</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Tumanlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fakeDistrict.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="object"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Obyekt</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Obyektlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ObjectListData.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Foydalanuvchi</Label>
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const selectedUser = user?.results.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Foydalanuvchi</Label>
|
||||
|
||||
<Popover open={openUser} onOpenChange={setOpenUser}>
|
||||
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchilar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FakeUserList.map((e) => (
|
||||
<SelectItem value={String(e.id)}>
|
||||
{e.firstName} {e.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openUser}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedUser
|
||||
? `${selectedUser.first_name} ${selectedUser.last_name}`
|
||||
: "Foydalanuvchi tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchUser}
|
||||
onValueChange={setSearchUser}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{isUserLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : user && user.results.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{user.results.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={() => {
|
||||
field.onChange(String(u.id));
|
||||
setOpenUser(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.first_name} {u.last_name} {u.region.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Foydalanuvchi topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="district"
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const selectedDiscrit = discrit?.results.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Tumanlar</Label>
|
||||
|
||||
<Popover open={openDiscrit} onOpenChange={setOpenDiscrit}>
|
||||
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||
<FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openDiscrit}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedDiscrit
|
||||
? `${selectedDiscrit.name}`
|
||||
: "Tuman tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchDiscrit}
|
||||
onValueChange={setSearchDiscrit}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{discritLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : discrit && discrit.results.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{discrit.results.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={async () => {
|
||||
field.onChange(String(u.id));
|
||||
const selectedDistrict =
|
||||
discrit.results?.find(
|
||||
(d) => d.id === Number(u.id),
|
||||
);
|
||||
setOpenUser(false);
|
||||
|
||||
if (!selectedDistrict) return;
|
||||
|
||||
setSelectedDiscrit(selectedDistrict.name);
|
||||
|
||||
const coordsData = await getCoords(
|
||||
selectedDistrict?.name,
|
||||
);
|
||||
if (!coordsData) return;
|
||||
|
||||
setCoords({
|
||||
latitude: coordsData.lat,
|
||||
longitude: coordsData.lon,
|
||||
});
|
||||
setPolygonCoords(coordsData.polygon);
|
||||
|
||||
form.setValue("lat", String(coordsData.lat));
|
||||
form.setValue("long", String(coordsData.lon));
|
||||
setOpenDiscrit(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Tuman topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="object"
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const selectedObject = object?.results.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Obyektlar</Label>
|
||||
|
||||
<Popover open={openObject} onOpenChange={setOpenObject}>
|
||||
<PopoverTrigger asChild disabled={initialValues !== null}>
|
||||
<FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openDiscrit}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedObject
|
||||
? `${selectedObject.name}`
|
||||
: "Obyekt tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</FormControl>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchObject}
|
||||
onValueChange={setSearchObject}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{isObjectLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : object && object.results.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{object.results.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={async () => {
|
||||
field.onChange(String(u.id));
|
||||
const selectedObject = object.results?.find(
|
||||
(d) => d.id === Number(u.id),
|
||||
);
|
||||
setOpenUser(false);
|
||||
|
||||
if (!selectedObject) return;
|
||||
|
||||
setCircleCoords([
|
||||
selectedObject.latitude,
|
||||
selectedObject.longitude,
|
||||
]);
|
||||
setCoords({
|
||||
latitude: selectedObject.latitude,
|
||||
longitude: selectedObject.longitude,
|
||||
});
|
||||
|
||||
form.setValue(
|
||||
"lat",
|
||||
String(selectedObject.latitude),
|
||||
);
|
||||
form.setValue(
|
||||
"long",
|
||||
String(selectedObject.longitude),
|
||||
);
|
||||
setOpenObject(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Obyekt topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<YMaps query={{ lang: "en_RU" }}>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(lat), Number(long)],
|
||||
zoom: 16,
|
||||
center: [coords.latitude, coords.longitude],
|
||||
zoom: 12,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="100%"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[Number(lat), Number(long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(lat), Number(long)], 100]}
|
||||
<ZoomControl
|
||||
options={{
|
||||
position: { right: "10px", bottom: "70px" },
|
||||
}}
|
||||
/>
|
||||
<Placemark geometry={[coords.latitude, coords.longitude]} />
|
||||
{polygonCoords && (
|
||||
<Polygon
|
||||
geometry={polygonCoords}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
@@ -254,6 +682,18 @@ const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => {
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{circleCoords && (
|
||||
<Circle
|
||||
geometry={[circleCoords, 300]}
|
||||
options={{
|
||||
fillColor: "rgba(255, 100, 0, 0.3)",
|
||||
strokeColor: "rgba(255, 100, 0, 0.8)",
|
||||
strokeWidth: 2,
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
@@ -261,7 +701,7 @@ const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => {
|
||||
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||
type="submit"
|
||||
>
|
||||
{load ? (
|
||||
{isPending || editPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
|
||||
89
src/features/pharmacies/ui/DeletePharmacies.tsx
Normal file
89
src/features/pharmacies/ui/DeletePharmacies.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { pharmacies_api } from "@/features/pharmacies/lib/api";
|
||||
import type { PharmaciesListData } from "@/features/pharmacies/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Loader2, Trash, X } from "lucide-react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setDiscritDelete: Dispatch<SetStateAction<PharmaciesListData | null>>;
|
||||
discrit: PharmaciesListData | null;
|
||||
}
|
||||
|
||||
const DeletePharmacies = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
setDiscritDelete,
|
||||
discrit,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteDiscrict, isPending } = useMutation({
|
||||
mutationFn: (id: number) => pharmacies_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["pharmacies_list"] });
|
||||
toast.success(`Dorixona o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setDiscritDelete(null);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Tumanni o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "}
|
||||
ga tegishli {discrit?.name} dorixonani o'chirmoqchimisiz
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
|
||||
onClick={() => setOpenDelete(false)}
|
||||
>
|
||||
<X />
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button
|
||||
variant={"destructive"}
|
||||
onClick={() => discrit && deleteDiscrict(discrit.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeletePharmacies;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { PharmciesType } from "@/features/pharmacies/lib/data";
|
||||
import type { PharmaciesListData } from "@/features/pharmacies/lib/data";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -8,30 +8,93 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import {
|
||||
Circle,
|
||||
Map,
|
||||
Placemark,
|
||||
Polygon,
|
||||
YMaps,
|
||||
ZoomControl,
|
||||
} from "@pbe/react-yandex-maps";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
detail: boolean;
|
||||
setDetail: (value: boolean) => void;
|
||||
object: PharmciesType | null;
|
||||
object: PharmaciesListData | null;
|
||||
}
|
||||
|
||||
interface CoordsData {
|
||||
lat: number;
|
||||
lon: number;
|
||||
polygon: [number, number][][];
|
||||
}
|
||||
|
||||
const PharmDetailDialog = ({ detail, setDetail, object }: Props) => {
|
||||
const [open, setOpen] = useState(detail);
|
||||
const [coords, setCoords] = useState<[number, number]>([
|
||||
41.311081, 69.240562,
|
||||
]);
|
||||
|
||||
const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]);
|
||||
|
||||
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const getCoords = async (name: string): Promise<CoordsData | null> => {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
|
||||
name,
|
||||
)}&format=json&polygon_geojson=1&limit=1`,
|
||||
);
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.length || !data[0].geojson) return null;
|
||||
|
||||
const lat = parseFloat(data[0].lat);
|
||||
const lon = parseFloat(data[0].lon);
|
||||
|
||||
let polygon: [number, number][][] = [];
|
||||
|
||||
if (data[0].geojson.type === "Polygon") {
|
||||
polygon = data[0].geojson.coordinates.map((ring: []) =>
|
||||
ring.map((c) => [c[1], c[0]]),
|
||||
);
|
||||
}
|
||||
|
||||
if (data[0].geojson.type === "MultiPolygon") {
|
||||
polygon = data[0].geojson.coordinates[0].map((ring: []) =>
|
||||
ring.map((c) => [c[1], c[0]]),
|
||||
);
|
||||
}
|
||||
|
||||
return { lat, lon, polygon };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(detail);
|
||||
}, [detail]);
|
||||
if (!object) return;
|
||||
|
||||
const load = async () => {
|
||||
const district = await getCoords(object.district.name);
|
||||
|
||||
if (district) {
|
||||
setPolygonCoords(district.polygon);
|
||||
}
|
||||
|
||||
setCoords([object.latitude, object.longitude]);
|
||||
setCircleCoords([object.latitude, object.longitude]);
|
||||
};
|
||||
|
||||
load();
|
||||
}, [object]);
|
||||
if (!object) return null;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
setDetail(val);
|
||||
}}
|
||||
>
|
||||
<Dialog open={detail} onOpenChange={setDetail}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Farmatsiya tafsilotlari</DialogTitle>
|
||||
@@ -46,44 +109,64 @@ const PharmDetailDialog = ({ detail, setDetail, object }: Props) => {
|
||||
<strong>INN:</strong> {object.inn}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Telefon:</strong> {formatPhone(object.phone_number)}
|
||||
<strong>Telefon:</strong> {formatPhone(object.owner_phone)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Qo‘shimcha telefon:</strong>{" "}
|
||||
{formatPhone(object.additional_phone)}
|
||||
{formatPhone(object.responsible_phone)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Tuman:</strong> {object.district.name}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Obyekt:</strong> {object.object.name}
|
||||
<strong>Obyekt:</strong> {object.place.name}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Kimga tegishli:</strong> {object.user.firstName}{" "}
|
||||
{object.user.lastName}
|
||||
<strong>Kimga tegishli:</strong> {object.user.first_name}{" "}
|
||||
{object.user.last_name}
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<YMaps query={{ lang: "en_RU" }}>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(object.lat), Number(object.long)],
|
||||
zoom: 16,
|
||||
state={{
|
||||
center: coords,
|
||||
zoom: 12,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
height="100%"
|
||||
>
|
||||
<Placemark
|
||||
geometry={[Number(object.lat), Number(object.long)]}
|
||||
<ZoomControl
|
||||
options={{
|
||||
position: { right: "10px", bottom: "70px" },
|
||||
}}
|
||||
/>
|
||||
<Circle
|
||||
geometry={[[Number(object.lat), Number(object.long)], 100]}
|
||||
|
||||
{/* Ish joyining markazi */}
|
||||
<Placemark geometry={coords} />
|
||||
|
||||
{/* Tuman polygon */}
|
||||
{polygonCoords.length > 0 && (
|
||||
<Polygon
|
||||
geometry={polygonCoords}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{circleCoords && (
|
||||
<Circle
|
||||
geometry={[circleCoords, 300]}
|
||||
options={{
|
||||
fillColor: "rgba(255, 100, 0, 0.3)",
|
||||
strokeColor: "rgba(255, 100, 0, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
|
||||
98
src/features/pharmacies/ui/PharmaciesFilter.tsx
Normal file
98
src/features/pharmacies/ui/PharmaciesFilter.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { PharmaciesListData } from "@/features/pharmacies/lib/data";
|
||||
import AddedPharmacies from "@/features/pharmacies/ui/AddedPharmacies";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Plus } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
searchName: string;
|
||||
setSearchName: Dispatch<SetStateAction<string>>;
|
||||
searchDistrict: string;
|
||||
setSearchDistrict: Dispatch<SetStateAction<string>>;
|
||||
searchObject: string;
|
||||
setSearchObject: Dispatch<SetStateAction<string>>;
|
||||
searchUser: string;
|
||||
setSearchUser: Dispatch<SetStateAction<string>>;
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setEditingPlan: Dispatch<SetStateAction<PharmaciesListData | null>>;
|
||||
editingPlan: PharmaciesListData | null;
|
||||
}
|
||||
|
||||
const PharmaciesFilter = ({
|
||||
searchName,
|
||||
setSearchName,
|
||||
searchDistrict,
|
||||
setSearchDistrict,
|
||||
searchObject,
|
||||
searchUser,
|
||||
setSearchUser,
|
||||
setSearchObject,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
setEditingPlan,
|
||||
editingPlan,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex justify-end gap-2 w-full">
|
||||
<Input
|
||||
placeholder="Dorixona nomi"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Tuman"
|
||||
value={searchDistrict}
|
||||
onChange={(e) => setSearchDistrict(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Obyekt"
|
||||
value={searchObject}
|
||||
onChange={(e) => setSearchObject(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Kim qo'shgan"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-x-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan
|
||||
? "Dorixonani tahrirlash"
|
||||
: "Yangi dorixona qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AddedPharmacies
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PharmaciesFilter;
|
||||
@@ -1,132 +1,82 @@
|
||||
import {
|
||||
PharmciesData,
|
||||
type PharmciesType,
|
||||
} from "@/features/pharmacies/lib/data";
|
||||
import AddedPharmacies from "@/features/pharmacies/ui/AddedPharmacies";
|
||||
import { pharmacies_api } from "@/features/pharmacies/lib/api";
|
||||
import { type PharmaciesListData } from "@/features/pharmacies/lib/data";
|
||||
import DeletePharmacies from "@/features/pharmacies/ui/DeletePharmacies";
|
||||
import PharmaciesFilter from "@/features/pharmacies/ui/PharmaciesFilter";
|
||||
import PharmaciesTable from "@/features/pharmacies/ui/PharmaciesTable";
|
||||
import PharmDetailDialog from "@/features/pharmacies/ui/PharmDetailDialog";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const PharmaciesList = () => {
|
||||
const [data, setData] = useState<PharmciesType[]>(PharmciesData);
|
||||
const [detail, setDetail] = useState<PharmciesType | null>(null);
|
||||
const [detail, setDetail] = useState<PharmaciesListData | null>(null);
|
||||
const [detailDialog, setDetailDialog] = useState<boolean>(false);
|
||||
const [editingPlan, setEditingPlan] = useState<PharmciesType | null>(null);
|
||||
const [editingPlan, setEditingPlan] = useState<PharmaciesListData | null>(
|
||||
null,
|
||||
);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [disricDelete, setDiscritDelete] = useState<PharmaciesListData | null>(
|
||||
null,
|
||||
);
|
||||
const [opneDelete, setOpenDelete] = useState<boolean>(false);
|
||||
const limit = 20;
|
||||
|
||||
const [searchName, setSearchName] = useState("");
|
||||
const [searchDistrict, setSearchDistrict] = useState("");
|
||||
const [searchObject, setSearchObject] = useState("");
|
||||
const [searchUser, setSearchUser] = useState("");
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setData((prev) => prev.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter((item) => {
|
||||
const nameMatch = `${item.name}`
|
||||
.toLowerCase()
|
||||
.includes(searchName.toLowerCase());
|
||||
const districtMatch = item.district.name
|
||||
.toLowerCase()
|
||||
.includes(searchDistrict.toLowerCase());
|
||||
const objectMatch = item.object.name
|
||||
.toLowerCase()
|
||||
.includes(searchObject.toLowerCase());
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return nameMatch && districtMatch && objectMatch && userMatch;
|
||||
const { data: pharmacies } = useQuery({
|
||||
queryKey: [
|
||||
"pharmacies_list",
|
||||
currentPage,
|
||||
searchDistrict,
|
||||
searchName,
|
||||
searchObject,
|
||||
searchUser,
|
||||
],
|
||||
queryFn: () =>
|
||||
pharmacies_api.list({
|
||||
district: searchDistrict,
|
||||
offset: (currentPage - 1) * limit,
|
||||
limit: limit,
|
||||
name: searchName,
|
||||
place: searchObject,
|
||||
user: searchUser,
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
}, [data, searchName, searchDistrict, searchObject, searchUser]);
|
||||
|
||||
const totalPages = pharmacies ? Math.ceil(pharmacies.count / limit) : 1;
|
||||
|
||||
const handleDelete = (user: PharmaciesListData) => {
|
||||
setDiscritDelete(user);
|
||||
setOpenDelete(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<h1 className="text-2xl font-bold">Dorixonalrni boshqarish</h1>
|
||||
<h1 className="text-2xl font-bold">Dorixonalarni boshqarish</h1>
|
||||
|
||||
<div className="flex justify-end gap-2 w-full">
|
||||
<Input
|
||||
placeholder="Dorixona nomi"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Tuman"
|
||||
value={searchDistrict}
|
||||
onChange={(e) => setSearchDistrict(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Obyekt"
|
||||
value={searchObject}
|
||||
onChange={(e) => setSearchObject(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Kim qo'shgan"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-x-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan
|
||||
? "Dorixonani tahrirlash"
|
||||
: "Yangi dorixona qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AddedPharmacies
|
||||
initialValues={editingPlan}
|
||||
<PharmaciesFilter
|
||||
dialogOpen={dialogOpen}
|
||||
editingPlan={editingPlan}
|
||||
searchDistrict={searchDistrict}
|
||||
searchName={searchName}
|
||||
searchObject={searchObject}
|
||||
searchUser={searchUser}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setData={setData}
|
||||
setEditingPlan={setEditingPlan}
|
||||
setSearchDistrict={setSearchDistrict}
|
||||
setSearchName={setSearchName}
|
||||
setSearchObject={setSearchObject}
|
||||
setSearchUser={setSearchUser}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PharmDetailDialog
|
||||
@@ -136,111 +86,27 @@ const PharmaciesList = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Dorixona nomi</TableHead>
|
||||
<TableHead>Inn</TableHead>
|
||||
<TableHead>Egasining nomeri</TableHead>
|
||||
<TableHead>Ma'sul shaxsning nomeri</TableHead>
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Obyekt</TableHead>
|
||||
<TableHead>Kim qo'shgan</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{item.inn}</TableCell>
|
||||
<TableCell>{formatPhone(item.phone_number)}</TableCell>
|
||||
<TableCell>{formatPhone(item.additional_phone)}</TableCell>
|
||||
<TableCell>{item.district.name}</TableCell>
|
||||
<TableCell>{item.object.name}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
<PharmaciesTable
|
||||
filteredData={pharmacies ? pharmacies.results : []}
|
||||
handleDelete={handleDelete}
|
||||
setDetail={setDetail}
|
||||
setDetailDialog={setDetailDialog}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
/>
|
||||
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
<DeletePharmacies
|
||||
discrit={disricDelete}
|
||||
opneDelete={opneDelete}
|
||||
setDiscritDelete={setDiscritDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
102
src/features/pharmacies/ui/PharmaciesTable.tsx
Normal file
102
src/features/pharmacies/ui/PharmaciesTable.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import type { PharmaciesListData } from "@/features/pharmacies/lib/data";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { Eye, Pencil, Trash2 } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
filteredData: PharmaciesListData[];
|
||||
setDetail: Dispatch<SetStateAction<PharmaciesListData | null>>;
|
||||
setEditingPlan: Dispatch<SetStateAction<PharmaciesListData | null>>;
|
||||
setDetailDialog: Dispatch<SetStateAction<boolean>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (pharmacies: PharmaciesListData) => void;
|
||||
}
|
||||
|
||||
const PharmaciesTable = ({
|
||||
filteredData,
|
||||
setDetail,
|
||||
setEditingPlan,
|
||||
setDetailDialog,
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Dorixona nomi</TableHead>
|
||||
<TableHead>Inn</TableHead>
|
||||
<TableHead>Egasining nomeri</TableHead>
|
||||
<TableHead>Ma'sul shaxsning nomeri</TableHead>
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Obyekt</TableHead>
|
||||
<TableHead>Kim qo'shgan</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{item.inn}</TableCell>
|
||||
<TableCell>{formatPhone(item.owner_phone)}</TableCell>
|
||||
<TableCell>{formatPhone(item.responsible_phone)}</TableCell>
|
||||
<TableCell>{item.district.name}</TableCell>
|
||||
<TableCell>{item.place.name}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.first_name} {item.user.last_name}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PharmaciesTable;
|
||||
139
src/features/plans/ui/FilterPlans.tsx
Normal file
139
src/features/plans/ui/FilterPlans.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import AddedPlan from "@/features/plans/ui/AddedPlan";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { ChevronDownIcon, Plus } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
statusFilter: string;
|
||||
setStatusFilter: Dispatch<SetStateAction<string>>;
|
||||
open: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
dateFilter: Date | undefined;
|
||||
setDateFilter: Dispatch<SetStateAction<Date | undefined>>;
|
||||
searchUser: string;
|
||||
setSearchUser: Dispatch<SetStateAction<string>>;
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
editingPlan: Plan | null;
|
||||
setEditingPlan: Dispatch<SetStateAction<Plan | null>>;
|
||||
setPlans: Dispatch<SetStateAction<Plan[]>>;
|
||||
}
|
||||
|
||||
const FilterPlans = ({
|
||||
setStatusFilter,
|
||||
statusFilter,
|
||||
open,
|
||||
setOpen,
|
||||
dateFilter,
|
||||
setDateFilter,
|
||||
searchUser,
|
||||
setSearchUser,
|
||||
dialogOpen,
|
||||
setDialogOpen,
|
||||
setEditingPlan,
|
||||
editingPlan,
|
||||
setPlans,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="foydalanuvchi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Barchasi</SelectItem>
|
||||
<SelectItem value="Bajarildi">Bajarildi</SelectItem>
|
||||
<SelectItem value="Bajarilmagan">Bajarilmagan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-48 justify-between font-normal h-12"
|
||||
>
|
||||
{dateFilter ? dateFilter.toDateString() : "Sana"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateFilter}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDateFilter(date);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="p-2 border-t bg-white">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setDateFilter(undefined);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Tozalash
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Foydalanuvchi ismi"
|
||||
className="h-12"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500 h-12"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddedPlan
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterPlans;
|
||||
101
src/features/plans/ui/PalanTable.tsx
Normal file
101
src/features/plans/ui/PalanTable.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { Edit, Eye, Trash } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
filteredPlans: Plan[];
|
||||
setEditingPlan: Dispatch<SetStateAction<Plan | null>>;
|
||||
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (id: number) => void;
|
||||
}
|
||||
|
||||
const PalanTable = ({
|
||||
filteredPlans,
|
||||
setEditingPlan,
|
||||
setDetail,
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Reja nomi</TableHead>
|
||||
<TableHead className="text-start">Tavsifi</TableHead>
|
||||
<TableHead className="text-start">Kimga tegishli</TableHead>
|
||||
<TableHead className="text-start">Status</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPlans.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.name}</TableCell>
|
||||
<TableCell>{plan.description}</TableCell>
|
||||
<TableCell>
|
||||
{plan.user.firstName + " " + plan.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={clsx(
|
||||
plan.status === "Bajarildi"
|
||||
? "text-green-500"
|
||||
: "text-red-500",
|
||||
)}
|
||||
>
|
||||
{plan.status}
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-green-500 text-white cursor-pointer hover:bg-green-500 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDetail(true);
|
||||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PalanTable;
|
||||
@@ -1,45 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import AddedPlan from "@/features/plans/ui/AddedPlan";
|
||||
import FilterPlans from "@/features/plans/ui/FilterPlans";
|
||||
import PalanTable from "@/features/plans/ui/PalanTable";
|
||||
import PlanDetail from "@/features/plans/ui/PlanDetail";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Edit,
|
||||
Eye,
|
||||
Plus,
|
||||
Trash,
|
||||
} from "lucide-react";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const PlansList = () => {
|
||||
@@ -79,16 +43,13 @@ const PlansList = () => {
|
||||
|
||||
const filteredPlans = useMemo(() => {
|
||||
return plans.filter((item) => {
|
||||
// 1) Status (agar all bo'lsa filtrlanmaydi)
|
||||
const statusMatch =
|
||||
statusFilter === "all" || item.status === statusFilter;
|
||||
|
||||
// 2) Sana filtri: createdAt === tanlangan sana
|
||||
const dateMatch = dateFilter
|
||||
? item.createdAt.toDateString() === dateFilter.toDateString()
|
||||
: true;
|
||||
|
||||
// 3) User ism familiya bo'yicha qidiruv
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
@@ -99,208 +60,40 @@ const PlansList = () => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||
<h1 className="text-2xl font-bold">Rejalarni boshqarish</h1>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
{/* Status filter */}
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="foydalanuvchi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Barchasi</SelectItem>
|
||||
<SelectItem value="Bajarildi">Bajarildi</SelectItem>
|
||||
<SelectItem value="Bajarilmagan">Bajarilmagan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Sana filter */}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-48 justify-between font-normal h-12"
|
||||
>
|
||||
{dateFilter ? dateFilter.toDateString() : "Sana"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateFilter}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDateFilter(date);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="p-2 border-t bg-white">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setDateFilter(undefined);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Tozalash
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Foydalanuvchi ismi"
|
||||
className="h-12"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500 h-12"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Form */}
|
||||
<AddedPlan
|
||||
initialValues={editingPlan}
|
||||
<FilterPlans
|
||||
dateFilter={dateFilter}
|
||||
dialogOpen={dialogOpen}
|
||||
editingPlan={editingPlan}
|
||||
open={open}
|
||||
searchUser={searchUser}
|
||||
setDateFilter={setDateFilter}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
setOpen={setOpen}
|
||||
setPlans={setPlans}
|
||||
setSearchUser={setSearchUser}
|
||||
setStatusFilter={setStatusFilter}
|
||||
statusFilter={statusFilter}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Deail plan */}
|
||||
<PlanDetail detail={detail} setDetail={setDetail} plan={editingPlan} />
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Reja nomi</TableHead>
|
||||
<TableHead className="text-start">Tavsifi</TableHead>
|
||||
<TableHead className="text-start">Kimga tegishli</TableHead>
|
||||
<TableHead className="text-start">Status</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPlans.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.name}</TableCell>
|
||||
<TableCell>{plan.description}</TableCell>
|
||||
<TableCell>
|
||||
{plan.user.firstName + " " + plan.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={clsx(
|
||||
plan.status === "Bajarildi"
|
||||
? "text-green-500"
|
||||
: "text-red-500",
|
||||
)}
|
||||
>
|
||||
{plan.status}
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-green-500 text-white cursor-pointer hover:bg-green-500 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDetail(true);
|
||||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<PalanTable
|
||||
filteredPlans={filteredPlans}
|
||||
handleDelete={handleDelete}
|
||||
setDetail={setDetail}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
/>
|
||||
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ReportsData, type ReportsTypeList } from "@/features/reports/lib/data";
|
||||
import AddedReport from "@/features/reports/ui/AddedReport";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import ReportsTable from "@/features/reports/ui/ReportsTable";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -10,16 +9,8 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const ReportsList = () => {
|
||||
@@ -37,7 +28,7 @@ const ReportsList = () => {
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||
<h1 className="text-2xl font-bold">Rejalarni boshqarish</h1>
|
||||
<h1 className="text-2xl font-bold">To'lovlar</h1>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
@@ -65,92 +56,18 @@ const ReportsList = () => {
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Dorixoan nomi</TableHead>
|
||||
<TableHead className="text-start">To'langan summa</TableHead>
|
||||
<TableHead className="text-start">To'langan sanasi</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plans.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.pharm_name}</TableCell>
|
||||
<TableCell>{formatPrice(plan.amount, true)}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.month, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<ReportsTable
|
||||
handleDelete={handleDelete}
|
||||
plans={plans}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
/>
|
||||
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
78
src/features/reports/ui/ReportsTable.tsx
Normal file
78
src/features/reports/ui/ReportsTable.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { ReportsTypeList } from "@/features/reports/lib/data";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { Edit, Trash } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
const ReportsTable = ({
|
||||
plans,
|
||||
setEditingPlan,
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
}: {
|
||||
plans: ReportsTypeList[];
|
||||
setEditingPlan: Dispatch<SetStateAction<ReportsTypeList | null>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (id: number) => void;
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Dorixoan nomi</TableHead>
|
||||
<TableHead className="text-start">To'langan summa</TableHead>
|
||||
<TableHead className="text-start">To'langan sanasi</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plans.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.pharm_name}</TableCell>
|
||||
<TableCell>{formatPrice(plan.amount, true)}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.month, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportsTable;
|
||||
@@ -1,11 +1,12 @@
|
||||
import type {
|
||||
BotUsers,
|
||||
UserCreateReq,
|
||||
UserListRes,
|
||||
UserUpdateReq,
|
||||
} from "@/features/users/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { USER } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
import axios, { type AxiosResponse } from "axios";
|
||||
|
||||
export const user_api = {
|
||||
async list(params: {
|
||||
@@ -38,4 +39,11 @@ export const user_api = {
|
||||
const res = await httpClient.delete(`${USER}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
|
||||
async bot_start(): Promise<AxiosResponse<BotUsers>> {
|
||||
const res = await axios.get(
|
||||
"https://api.telegram.org/bot8137312508:AAF37FpdHaWIUPQqkai9IqW3ob6Z500KnC0/getUpdates",
|
||||
);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -72,6 +72,7 @@ export interface UserListData {
|
||||
name: string;
|
||||
};
|
||||
is_active: boolean;
|
||||
telegram_id: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -87,4 +88,39 @@ export interface UserCreateReq {
|
||||
last_name: string;
|
||||
region_id: number;
|
||||
is_active: boolean;
|
||||
telegram_id: string;
|
||||
}
|
||||
|
||||
export interface BotUsers {
|
||||
ok: boolean;
|
||||
result: BotUsersData[];
|
||||
}
|
||||
|
||||
export interface BotUsersData {
|
||||
update_id: number;
|
||||
message: {
|
||||
message_id: number;
|
||||
from: {
|
||||
id: number;
|
||||
is_bot: boolean;
|
||||
first_name: string;
|
||||
language_code: string;
|
||||
username?: string;
|
||||
};
|
||||
chat: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
type: string;
|
||||
username?: string;
|
||||
};
|
||||
date: number;
|
||||
text: string;
|
||||
entities: [
|
||||
{
|
||||
offset: number;
|
||||
length: number;
|
||||
type: string;
|
||||
},
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export const AddedUser = z.object({
|
||||
lastName: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
region: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
isActive: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
telegram_id: z.string().min(1, { message: "Foydalnuvchini tanlang" }),
|
||||
});
|
||||
|
||||
@@ -26,7 +26,8 @@ import {
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { Loader2, Search } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type z from "zod";
|
||||
@@ -42,11 +43,13 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => {
|
||||
defaultValues: {
|
||||
firstName: initialData?.first_name || "",
|
||||
lastName: initialData?.last_name || "",
|
||||
region: initialData?.region.name || "",
|
||||
region: initialData ? String(initialData.region.id) : "",
|
||||
isActive: initialData ? String(initialData.is_active) : "true",
|
||||
telegram_id: initialData ? initialData.telegram_id : "",
|
||||
},
|
||||
});
|
||||
const queryClient = useQueryClient();
|
||||
const [bot, setBot] = useState<"select" | "input">("select");
|
||||
|
||||
const { mutate: update, isPending } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: number; body: UserUpdateReq }) =>
|
||||
@@ -94,12 +97,13 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => {
|
||||
},
|
||||
id: initialData.id,
|
||||
});
|
||||
} else if (initialData === null) {
|
||||
} else if (initialData === null && values.telegram_id) {
|
||||
create({
|
||||
first_name: values.firstName,
|
||||
is_active: values.isActive === "true" ? true : false,
|
||||
last_name: values.lastName,
|
||||
region_id: Number(values.region),
|
||||
telegram_id: values.telegram_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -110,9 +114,64 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => {
|
||||
select: (res) => res.data.data,
|
||||
});
|
||||
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ["bot_users"],
|
||||
queryFn: () => user_api.bot_start(),
|
||||
select: (res) => res.data.result,
|
||||
});
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="telegram_id"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md mr-4">Botga start bosganlar</Label>
|
||||
<FormControl>
|
||||
<div className="flex gap-1">
|
||||
{bot === "select" ? (
|
||||
<Select
|
||||
value={String(field.value)}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Botga start bosganlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{user?.map((e) => (
|
||||
<SelectItem value={String(e.message.chat.id)}>
|
||||
{e.message.chat.username
|
||||
? e.message.chat.username
|
||||
: e.message.chat.first_name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<Input
|
||||
className="h-12"
|
||||
placeholder="Telgram idni yozing"
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
className="h-12 !bg-blue-700 w-12 cursor-pointer"
|
||||
onClick={() =>
|
||||
bot === "select" ? setBot("input") : setBot("select")
|
||||
}
|
||||
>
|
||||
<Search />
|
||||
</Button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
|
||||
@@ -40,7 +40,6 @@ const DeleteUser = ({
|
||||
setUserDelete(null);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
console.log(err);
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
|
||||
@@ -4,8 +4,8 @@ import { user_api } from "@/features/users/lib/api";
|
||||
import type { UserListData } from "@/features/users/lib/data";
|
||||
import DeleteUser from "@/features/users/ui/DeleteUser";
|
||||
import Filter from "@/features/users/ui/Filter";
|
||||
import Pagination from "@/features/users/ui/Pagination";
|
||||
import UserTable from "@/features/users/ui/UserTable";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -7,5 +7,17 @@ const REGION = "/api/v1/admin/district/";
|
||||
const REGIONS = "/api/v1/admin/region/";
|
||||
const DISTRICT = "/api/v1/admin/district/";
|
||||
const DOCTOR = "/api/v1/admin/doctor/";
|
||||
const OBJECT = "/api/v1/admin/place/";
|
||||
const PHARMACIES = "/api/v1/admin/pharmacy/";
|
||||
|
||||
export { BASE_URL, DISTRICT, DOCTOR, LOGIN, REGION, REGIONS, USER };
|
||||
export {
|
||||
BASE_URL,
|
||||
DISTRICT,
|
||||
DOCTOR,
|
||||
LOGIN,
|
||||
OBJECT,
|
||||
PHARMACIES,
|
||||
REGION,
|
||||
REGIONS,
|
||||
USER,
|
||||
};
|
||||
|
||||
@@ -10,8 +10,6 @@ const httpClient = axios.create({
|
||||
|
||||
httpClient.interceptors.request.use(
|
||||
async (config) => {
|
||||
console.log(`API REQUEST to ${config.url}`, config);
|
||||
|
||||
// Language configs
|
||||
const language = i18n.language;
|
||||
config.headers["Accept-Language"] = language;
|
||||
|
||||
@@ -15,8 +15,8 @@ const Pagination = ({ currentPage, setCurrentPage, totalPages }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
@@ -40,9 +40,9 @@ const Pagination = ({ currentPage, setCurrentPage, totalPages }: Props) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
Reference in New Issue
Block a user