doctor and pharmacies crud
This commit is contained in:
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 { 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"
|
||||
|
||||
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)]}
|
||||
/>
|
||||
<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>
|
||||
|
||||
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 { 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>
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
Reference in New Issue
Block a user