doctor and pharmacies crud

This commit is contained in:
Samandar Turgunboyev
2025-11-29 19:19:40 +05:00
parent bcf9d7cd2b
commit 9bc4c3df1f
45 changed files with 3610 additions and 1469 deletions

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

View File

@@ -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 };
}

View File

@@ -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 { data: user, isLoading: isUserLoading } = useQuery({
queryKey: ["user_list", searchUser],
queryFn: () => {
const params: {
limit?: number;
offset?: number;
search?: string;
is_active?: boolean | string;
region_id?: number;
} = {
limit: 8,
search: searchUser,
};
const handleMapClick = (e: { get: (key: string) => number[] }) => {
const coords = e.get("coords");
form.setValue("lat", coords[0].toString());
form.setValue("long", coords[1].toString());
return user_api.list(params);
},
select(data) {
return data.data.data;
},
});
const { data: object, isLoading: isObjectLoading } = useQuery({
queryKey: ["object_list", searchUser, selectDiscrit],
queryFn: () => {
const params: {
name?: string;
district?: string;
} = {
name: searchUser,
district: selectDiscrit,
};
return object_api.list(params);
},
select(data) {
return data.data.data;
},
});
const user_id = form.watch("user");
const { data: discrit, isLoading: discritLoading } = useQuery({
queryKey: ["discrit_list", searchDiscrit, user_id],
queryFn: () => {
const params: {
name?: string;
user?: number;
} = {
name: searchDiscrit,
};
if (user_id !== "") {
params.user = Number(user_id);
}
return discrit_api.list(params);
},
select(data) {
return data.data.data;
},
});
const [coords, setCoords] = useState({
latitude: 41.311081,
longitude: 69.240562,
});
const [polygonCoords, setPolygonCoords] = useState<
[number, number][][] | null
>(null);
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
null,
);
const getCoords = async (name: string): Promise<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;
};
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];
useEffect(() => {
if (initialValues) {
(async () => {
const result = await getCoords(initialValues.district.name);
if (result) {
setCoords({
latitude: Number(initialValues.latitude),
longitude: Number(initialValues.longitude),
});
setPolygonCoords(result.polygon);
form.setValue("lat", String(result.lat));
form.setValue("long", String(result.lon));
setCircleCoords([
Number(initialValues.latitude),
Number(initialValues.longitude),
]);
}
});
setLoad(false);
})();
}
}, [initialValues]);
const handleMapClick = (
e: ymaps.IEvent<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);
}, 2000);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const { mutate: edit, isPending: editPending } = useMutation({
mutationFn: ({ body, id }: { id: number; body: UpdatePharmaciesReq }) =>
pharmacies_api.update({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["pharmacies_list"] });
setDialogOpen(false);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
function onSubmit(values: z.infer<typeof PharmForm>) {
if (initialValues) {
edit({
id: initialValues.id,
body: {
extra_location: {
latitude: Number(values.lat),
longitude: Number(values.long),
},
latitude: Number(values.lat),
longitude: Number(values.long),
inn: values.inn,
name: values.name,
owner_phone: onlyNumber(values.phone_number),
responsible_phone: onlyNumber(values.additional_phone),
},
});
} else {
mutate({
district_id: Number(values.district),
extra_location: {
latitude: Number(values.lat),
longitude: Number(values.long),
},
latitude: Number(values.lat),
longitude: Number(values.long),
inn: values.inn,
name: values.name,
owner_phone: onlyNumber(values.phone_number),
place_id: Number(values.object),
responsible_phone: onlyNumber(values.additional_phone),
user_id: Number(values.user),
});
}
}
return (
@@ -159,101 +351,349 @@ const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => {
/>
<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>
<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>
</FormControl>
<FormMessage />
</FormItem>
)}
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>
<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={{
fillColor: "rgba(0, 150, 255, 0.2)",
strokeColor: "rgba(0, 150, 255, 0.8)",
strokeWidth: 2,
interactivityModel: "default#transparent",
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>
@@ -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"

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

View File

@@ -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>Qoshimcha 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)]}
/>
<Circle
geometry={[[Number(object.lat), Number(object.long)], 100]}
<ZoomControl
options={{
fillColor: "rgba(0, 150, 255, 0.2)",
strokeColor: "rgba(0, 150, 255, 0.8)",
strokeWidth: 2,
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>

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

View File

@@ -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 { data: pharmacies } = useQuery({
queryKey: [
"pharmacies_list",
currentPage,
searchDistrict,
searchName,
searchObject,
searchUser,
],
queryFn: () =>
pharmacies_api.list({
district: searchDistrict,
offset: (currentPage - 1) * limit,
limit: limit,
name: searchName,
place: searchObject,
user: searchUser,
}),
select(data) {
return data.data.data;
},
});
const totalPages = pharmacies ? Math.ceil(pharmacies.count / limit) : 1;
const handleDelete = (user: PharmaciesListData) => {
setDiscritDelete(user);
setOpenDelete(true);
};
const filteredData = useMemo(() => {
return data.filter((item) => {
const nameMatch = `${item.name}`
.toLowerCase()
.includes(searchName.toLowerCase());
const districtMatch = item.district.name
.toLowerCase()
.includes(searchDistrict.toLowerCase());
const objectMatch = item.object.name
.toLowerCase()
.includes(searchObject.toLowerCase());
const userMatch = `${item.user.firstName} ${item.user.lastName}`
.toLowerCase()
.includes(searchUser.toLowerCase());
return nameMatch && districtMatch && objectMatch && userMatch;
});
}, [data, searchName, searchDistrict, searchObject, searchUser]);
return (
<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}
setDialogOpen={setDialogOpen}
setData={setData}
/>
</DialogContent>
</Dialog>
</div>
<PharmaciesFilter
dialogOpen={dialogOpen}
editingPlan={editingPlan}
searchDistrict={searchDistrict}
searchName={searchName}
searchObject={searchObject}
searchUser={searchUser}
setDialogOpen={setDialogOpen}
setEditingPlan={setEditingPlan}
setSearchDistrict={setSearchDistrict}
setSearchName={setSearchName}
setSearchObject={setSearchObject}
setSearchUser={setSearchUser}
/>
</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>
);
};

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