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

@@ -8,6 +8,7 @@ export const discrit_api = {
limit?: number;
offset?: number;
name?: string;
user?: number;
}): Promise<AxiosResponse<DistrictListRes>> {
const res = await httpClient.get(`${DISTRICT}list/`, { params });
return res;

View File

@@ -2,8 +2,8 @@ import { discrit_api } from "@/features/districts/lib/api";
import { type DistrictListData } from "@/features/districts/lib/data";
import DeleteDiscrit from "@/features/districts/ui/DeleteDiscrit";
import Filter from "@/features/districts/ui/Filter";
import PaginationDistrict from "@/features/districts/ui/PaginationDistrict";
import TableDistrict from "@/features/districts/ui/TableDistrict";
import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
@@ -66,7 +66,7 @@ const DistrictsList = () => {
currentPage={currentPage}
/>
<PaginationDistrict
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={totalPages}

View File

@@ -1,57 +0,0 @@
import { Button } from "@/shared/ui/button";
import clsx from "clsx";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
interface Props {
currentPage: number;
setCurrentPage: Dispatch<SetStateAction<number>>;
totalPages: number;
}
const PaginationDistrict = ({
currentPage,
setCurrentPage,
totalPages,
}: Props) => {
return (
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
<Button
variant="outline"
size="icon"
disabled={currentPage === 1}
className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
>
<ChevronLeft />
</Button>
{Array.from({ length: totalPages }, (_, i) => (
<Button
key={i}
variant={currentPage === i + 1 ? "default" : "outline"}
size="icon"
className={clsx(
currentPage === i + 1
? "bg-blue-500 hover:bg-blue-500"
: " bg-none hover:bg-blue-200",
"cursor-pointer",
)}
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</Button>
))}
<Button
variant="outline"
size="icon"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
className="cursor-pointer"
>
<ChevronRight />
</Button>
</div>
);
};
export default PaginationDistrict;

View File

@@ -1,4 +1,8 @@
import type { DoctorListRes } from "@/features/doctors/lib/data";
import type {
CreateDoctorReq,
DoctorListRes,
UpdateDoctorReq,
} from "@/features/doctors/lib/data";
import httpClient from "@/shared/config/api/httpClient";
import { DOCTOR } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
@@ -17,4 +21,19 @@ export const doctor_api = {
const res = await httpClient.get(`${DOCTOR}list/`, { params });
return res;
},
async create(body: CreateDoctorReq) {
const res = await httpClient.post(`${DOCTOR}create/`, body);
return res;
},
async update({ body, id }: { id: number; body: UpdateDoctorReq }) {
const res = await httpClient.patch(`${DOCTOR}${id}/update/`, body);
return res;
},
async delete(id: number) {
const res = await httpClient.delete(`${DOCTOR}${id}/delete/`);
return res;
},
};

View File

@@ -92,3 +92,32 @@ export interface DoctorListResData {
};
created_at: string;
}
export interface CreateDoctorReq {
first_name: string;
last_name: string;
phone_number: string;
work_place: string;
sphere: string;
description: string;
district_id: number;
place_id: number;
user_id: number;
longitude: number;
latitude: number;
extra_location: {
longitude: number;
latitude: number;
};
}
export interface UpdateDoctorReq {
first_name: string;
last_name: string;
phone_number: string;
work_place: string;
sphere: string;
description: string;
longitude: number;
latitude: number;
extra_location: { longitude: number; latitude: number };
}

View File

@@ -1,10 +1,25 @@
import { fakeDistrict } from "@/features/districts/lib/data";
import type { DoctorListType } from "@/features/doctors/lib/data";
import { discrit_api } from "@/features/districts/lib/api";
import { doctor_api } from "@/features/doctors/lib/api";
import type {
CreateDoctorReq,
DoctorListResData,
UpdateDoctorReq,
} from "@/features/doctors/lib/data";
import { DoctorForm } from "@/features/doctors/lib/form";
import { ObjectListData } from "@/features/objects/lib/data";
import { FakeUserList } from "@/features/users/lib/data";
import { object_api } from "@/features/objects/lib/api";
import { user_api } from "@/features/users/lib/api";
import formatPhone from "@/shared/lib/formatPhone";
import onlyNumber from "@/shared/lib/onlyNumber";
import { cn } from "@/shared/lib/utils";
import { Button } from "@/shared/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/shared/ui/command";
import {
Form,
FormControl,
@@ -14,58 +29,266 @@ import {
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { Textarea } from "@/shared/ui/textarea";
import { zodResolver } from "@hookform/resolvers/zod";
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
import { Loader2 } from "lucide-react";
import { useState, type Dispatch, type SetStateAction } from "react";
import {
Circle,
Map,
Placemark,
Polygon,
YMaps,
ZoomControl,
} from "@pbe/react-yandex-maps";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type z from "zod";
interface Props {
initialValues: DoctorListType | null;
initialValues: DoctorListResData | null;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
}
interface CoordsData {
lat: number;
lon: number;
polygon: [number, number][][];
}
const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
const [load, setLoad] = useState<boolean>(false);
const queryClient = useQueryClient();
const [searchUser, setSearchUser] = useState<string>("");
const [searchObject, setSearchObject] = useState<string>("");
const [selectDiscrit, setSelectedDiscrit] = useState<string>("");
const [searchDiscrit, setSearchDiscrit] = useState<string>("");
const [openUser, setOpenUser] = useState<boolean>(false);
const [openDiscrit, setOpenDiscrit] = useState<boolean>(false);
const [openObject, setOpenObject] = useState<boolean>(false);
const form = useForm<z.infer<typeof DoctorForm>>({
resolver: zodResolver(DoctorForm),
defaultValues: {
desc: initialValues?.desc || "",
desc: initialValues?.description || "",
district: initialValues?.district.id.toString() || "",
first_name: initialValues?.first_name || "",
last_name: initialValues?.last_name || "",
lat: initialValues?.lat || "41.2949",
long: initialValues?.long || "69.2361",
object: initialValues?.object.id.toString() || "",
lat: String(initialValues?.latitude) || "41.2949",
long: String(initialValues?.longitude) || "69.2361",
object: initialValues?.place.id.toString() || "",
phone_number: initialValues?.phone_number || "+998",
spec: initialValues?.spec || "",
work: initialValues?.work || "",
spec: initialValues?.sphere || "",
work: initialValues?.work_place || "",
user: initialValues?.user.id.toString() || "",
},
});
const lat = form.watch("lat");
const long = form.watch("long");
const { data: user, isLoading: isUserLoading } = useQuery({
queryKey: ["user_list", searchUser],
queryFn: () => {
const params: {
limit?: number;
offset?: number;
search?: string;
is_active?: boolean | string;
region_id?: number;
} = {
limit: 8,
search: searchUser,
};
const handleMapClick = (e: { get: (key: string) => number[] }) => {
const coords = e.get("coords");
form.setValue("lat", coords[0].toString());
form.setValue("long", coords[1].toString());
return user_api.list(params);
},
select(data) {
return data.data.data;
},
});
const { data: object, isLoading: isObjectLoading } = useQuery({
queryKey: ["object_list", searchUser, selectDiscrit],
queryFn: () => {
const params: {
name?: string;
district?: string;
} = {
name: searchUser,
district: selectDiscrit,
};
return object_api.list(params);
},
select(data) {
return data.data.data;
},
});
const user_id = form.watch("user");
const { data: discrit, isLoading: discritLoading } = useQuery({
queryKey: ["discrit_list", searchDiscrit, user_id],
queryFn: () => {
const params: {
name?: string;
user?: number;
} = {
name: searchDiscrit,
};
if (user_id !== "") {
params.user = Number(user_id);
}
return discrit_api.list(params);
},
select(data) {
return data.data.data;
},
});
const { mutate, isPending } = useMutation({
mutationFn: (body: CreateDoctorReq) => doctor_api.create(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["doctor_list"] });
setDialogOpen(false);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const { mutate: edit, isPending: editPending } = useMutation({
mutationFn: ({ body, id }: { body: UpdateDoctorReq; id: number }) =>
doctor_api.update({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["doctor_list"] });
setDialogOpen(false);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const [coords, setCoords] = useState({
latitude: 41.311081,
longitude: 69.240562,
});
const [polygonCoords, setPolygonCoords] = useState<
[number, number][][] | null
>(null);
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
null,
);
const getCoords = async (name: string): Promise<CoordsData | null> => {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(name)}&format=json&polygon_geojson=1&limit=1`,
);
const data = await res.json();
if (data.length > 0 && data[0].geojson) {
const lat = parseFloat(data[0].lat);
const lon = parseFloat(data[0].lon);
let polygon: [number, number][][] = [];
if (data[0].geojson.type === "Polygon") {
polygon = data[0].geojson.coordinates.map((ring: [number, number][]) =>
ring.map((coord: [number, number]) => [coord[1], coord[0]]),
);
} else if (data[0].geojson.type === "MultiPolygon") {
polygon = data[0].geojson.coordinates.map(
(poly: [number, number][][]) =>
poly[0].map((coord: [number, number]) => [coord[1], coord[0]]),
);
}
return { lat, lon, polygon };
}
return null;
};
useEffect(() => {
if (initialValues) {
(async () => {
const result = await getCoords(initialValues.district.name);
if (result) {
setCoords({
latitude: Number(initialValues.latitude),
longitude: Number(initialValues.longitude),
});
setPolygonCoords(result.polygon);
form.setValue("lat", String(result.lat));
form.setValue("long", String(result.lon));
setCircleCoords([
Number(initialValues.latitude),
Number(initialValues.longitude),
]);
}
})();
}
}, [initialValues]);
const handleMapClick = (
e: ymaps.IEvent<MouseEvent, { coords: [number, number] }>,
) => {
const [lat, lon] = e.get("coords");
setCoords({ latitude: lat, longitude: lon });
form.setValue("lat", String(lat));
form.setValue("long", String(lon));
};
function onSubmit(values: z.infer<typeof DoctorForm>) {
setLoad(true);
console.log(values);
setDialogOpen(false);
if (initialValues) {
edit({
id: initialValues.id,
body: {
description: values.desc,
extra_location: {
latitude: Number(values.lat),
longitude: Number(values.long),
},
first_name: values.first_name,
last_name: values.last_name,
latitude: Number(values.lat),
longitude: Number(values.long),
phone_number: onlyNumber(values.phone_number),
sphere: values.spec,
work_place: values.work,
},
});
} else {
mutate({
description: values.desc,
district_id: Number(values.district),
extra_location: {
latitude: Number(values.lat),
longitude: Number(values.long),
},
first_name: values.first_name,
last_name: values.last_name,
latitude: Number(values.lat),
longitude: Number(values.long),
phone_number: onlyNumber(values.phone_number),
place_id: Number(values.object),
sphere: values.spec,
user_id: Number(values.user),
work_place: values.work,
});
}
}
return (
@@ -164,98 +387,349 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
/>
<FormField
control={form.control}
name="district"
render={({ field }) => (
<FormItem>
<Label>Tuman</Label>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger className="w-full !h-12">
<SelectValue placeholder="Tumanlar" />
</SelectTrigger>
<SelectContent>
{fakeDistrict.map((e) => (
<SelectItem key={e.id} value={String(e.id)}>
{e.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="object"
render={({ field }) => (
<FormItem>
<Label>Obyekt</Label>
<FormControl>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger className="w-full !h-12">
<SelectValue placeholder="Obyekt" />
</SelectTrigger>
<SelectContent>
{ObjectListData.map((e) => (
<SelectItem key={e.id} value={String(e.id)}>
{e.name}
</SelectItem>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="user"
render={({ field }) => (
<FormItem>
<Label>Foydalanuvchi</Label>
<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 }}
defaultState={{
center: [coords.latitude, coords.longitude],
zoom: 12,
}}
width="100%"
height="300px"
height="100%"
onClick={handleMapClick}
>
<Placemark geometry={[Number(lat), Number(long)]} />
<Circle
geometry={[[Number(lat), Number(long)], 100]}
<ZoomControl
options={{
fillColor: "rgba(0,150,255,0.2)",
strokeColor: "rgba(0,150,255,0.8)",
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>
@@ -263,7 +737,7 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
type="submit"
>
{load ? (
{isPending || editPending ? (
<Loader2 className="animate-spin" />
) : initialValues ? (
"Tahrirlash"

View File

@@ -0,0 +1,90 @@
import { doctor_api } from "@/features/doctors/lib/api";
import type { DoctorListResData } from "@/features/doctors/lib/data";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2, Trash, X } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
import { toast } from "sonner";
interface Props {
opneDelete: boolean;
setOpenDelete: Dispatch<SetStateAction<boolean>>;
setDiscritDelete: Dispatch<SetStateAction<DoctorListResData | null>>;
discrit: DoctorListResData | null;
}
const DeleteDoctor = ({
opneDelete,
setOpenDelete,
setDiscritDelete,
discrit,
}: Props) => {
const queryClient = useQueryClient();
const { mutate: deleteDiscrict, isPending } = useMutation({
mutationFn: (id: number) => doctor_api.delete(id),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["doctor_list"] });
toast.success(`Shifokor o'chirildi`);
setOpenDelete(false);
setDiscritDelete(null);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
return (
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
<DialogContent>
<DialogHeader>
<DialogTitle>Tumanni o'chirish</DialogTitle>
<DialogDescription className="text-md font-semibold">
Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "}
ga tegishli {discrit?.first_name} {discrit?.last_name} shifokorni
o'chirmoqchimisiz
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
onClick={() => setOpenDelete(false)}
>
<X />
Bekor qilish
</Button>
<Button
variant={"destructive"}
onClick={() => discrit && deleteDiscrict(discrit.id)}
>
{isPending ? (
<Loader2 className="animate-spin" />
) : (
<>
<Trash />
O'chirish
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default DeleteDoctor;

View File

@@ -37,7 +37,9 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => {
const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]);
const [circleCoords] = useState<[number, number] | null>(null);
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
null,
);
const getCoords = async (name: string): Promise<CoordsData | null> => {
try {
@@ -84,12 +86,11 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => {
}
setCoords([object.latitude, object.longitude]);
// setCircleCoords([object.latitude, object.longitude]);
setCircleCoords([object.latitude, object.longitude]);
};
load();
}, [object]);
if (!object) return null;
return (
@@ -154,10 +155,9 @@ const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => {
/>
)}
{/* Radius circle (ish joyi atrofida) */}
{circleCoords && (
<Circle
geometry={[circleCoords, 500]}
geometry={[circleCoords, 300]}
options={{
fillColor: "rgba(255, 100, 0, 0.3)",
strokeColor: "rgba(255, 100, 0, 0.8)",

View File

@@ -1,19 +1,19 @@
import { doctor_api } from "@/features/doctors/lib/api";
import {
type DoctorListResData,
type DoctorListType,
} from "@/features/doctors/lib/data";
import { type DoctorListResData } from "@/features/doctors/lib/data";
import DeleteDoctor from "@/features/doctors/ui/DeleteDoctor";
import DoctorDetailDialog from "@/features/doctors/ui/DoctorDetailDialog";
import FilterDoctor from "@/features/doctors/ui/FilterDoctor";
import PaginationDoctor from "@/features/doctors/ui/PaginationDoctor";
import TableDoctor from "@/features/doctors/ui/TableDoctor";
import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
const DoctorsList = () => {
const [detail, setDetail] = useState<DoctorListResData | null>(null);
const [detailDialog, setDetailDialog] = useState<boolean>(false);
const [editingPlan, setEditingPlan] = useState<DoctorListType | null>(null);
const [editingPlan, setEditingPlan] = useState<DoctorListResData | null>(
null,
);
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [currentPage, setCurrentPage] = useState(1);
@@ -24,6 +24,11 @@ const DoctorsList = () => {
const [searchSpec, setSearchSpec] = useState("");
const [searchUser, setSearchUser] = useState("");
const [disricDelete, setDiscritDelete] = useState<DoctorListResData | null>(
null,
);
const [opneDelete, setOpenDelete] = useState<boolean>(false);
const limit = 20;
const {
@@ -58,8 +63,10 @@ const DoctorsList = () => {
},
});
// const handleDelete = (id: number) => {
// };
const handleDelete = (user: DoctorListResData) => {
setDiscritDelete(user);
setOpenDelete(true);
};
const totalPages = doctor ? Math.ceil(doctor.count / limit) : 1;
@@ -79,7 +86,6 @@ const DoctorsList = () => {
searchSpec={searchSpec}
searchUser={searchUser}
searchWork={searchWork}
// setData={setData}
setDialogOpen={setDialogOpen}
setEditingPlan={setEditingPlan}
setSearchDistrict={setSearchDistrict}
@@ -103,15 +109,25 @@ const DoctorsList = () => {
isLoading={isLoading}
doctor={doctor ? doctor.results : []}
setDetail={setDetail}
handleDelete={handleDelete}
isFetching={isFetching}
setDetailDialog={setDetailDialog}
setDialogOpen={setDialogOpen}
setEditingPlan={setEditingPlan}
/>
<PaginationDoctor
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
/>
<DeleteDoctor
discrit={disricDelete}
opneDelete={opneDelete}
setDiscritDelete={setDiscritDelete}
setOpenDelete={setOpenDelete}
/>
</div>
);
};

View File

@@ -1,4 +1,4 @@
import type { DoctorListType } from "@/features/doctors/lib/data";
import type { DoctorListResData } from "@/features/doctors/lib/data";
import AddedDoctor from "@/features/doctors/ui/AddedDoctor";
import { Button } from "@/shared/ui/button";
import {
@@ -27,8 +27,8 @@ interface Props {
setDialogOpen: Dispatch<SetStateAction<boolean>>;
searchObject: string;
setSearchObject: Dispatch<SetStateAction<string>>;
setEditingPlan: Dispatch<SetStateAction<DoctorListType | null>>;
editingPlan: DoctorListType | null;
setEditingPlan: Dispatch<SetStateAction<DoctorListResData | null>>;
editingPlan: DoctorListResData | null;
}
const FilterDoctor = ({

View File

@@ -1,57 +0,0 @@
import { Button } from "@/shared/ui/button";
import clsx from "clsx";
import { ChevronLeft, ChevronRight } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
currentPage: number;
setCurrentPage: Dispatch<SetStateAction<number>>;
totalPages: number;
}
const PaginationDoctor = ({
currentPage,
setCurrentPage,
totalPages,
}: Props) => {
return (
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
<Button
variant="outline"
size="icon"
disabled={currentPage === 1}
className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
>
<ChevronLeft />
</Button>
{Array.from({ length: totalPages }, (_, i) => (
<Button
key={i}
variant={currentPage === i + 1 ? "default" : "outline"}
size="icon"
className={clsx(
currentPage === i + 1
? "bg-blue-500 hover:bg-blue-500"
: " bg-none hover:bg-blue-200",
"cursor-pointer",
)}
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</Button>
))}
<Button
variant="outline"
size="icon"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
className="cursor-pointer"
>
<ChevronRight />
</Button>
</div>
);
};
export default PaginationDoctor;

View File

@@ -15,11 +15,14 @@ import type { Dispatch, SetStateAction } from "react";
interface Props {
setDetail: Dispatch<SetStateAction<DoctorListResData | null>>;
setEditingPlan: Dispatch<SetStateAction<DoctorListResData | null>>;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
setDetailDialog: Dispatch<SetStateAction<boolean>>;
doctor: DoctorListResData[] | [];
isLoading: boolean;
isError: boolean;
isFetching: boolean;
handleDelete: (user: DoctorListResData) => void;
}
const TableDoctor = ({
@@ -27,7 +30,10 @@ const TableDoctor = ({
setDetail,
setDetailDialog,
isError,
setEditingPlan,
isLoading,
setDialogOpen,
handleDelete,
isFetching,
}: Props) => {
return (
@@ -100,8 +106,8 @@ const TableDoctor = ({
size="icon"
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
onClick={() => {
// setEditingPlan(item);
// setDialogOpen(true);
setEditingPlan(item);
setDialogOpen(true);
}}
>
<Pencil size={18} />
@@ -110,7 +116,7 @@ const TableDoctor = ({
variant="destructive"
size="icon"
className="cursor-pointer"
// onClick={() => handleDelete(item.id)}
onClick={() => handleDelete(item)}
>
<Trash2 size={18} />
</Button>

View File

@@ -0,0 +1,74 @@
import { Button } from "@/shared/ui/button";
import { Calendar } from "@/shared/ui/calendar";
import { Input } from "@/shared/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { ChevronDownIcon } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
dateFilter: Date | undefined;
setDateFilter: Dispatch<SetStateAction<Date | undefined>>;
searchUser: string;
setSearchUser: Dispatch<SetStateAction<string>>;
}
const LocationFilter = ({
open,
setOpen,
dateFilter,
setDateFilter,
searchUser,
setSearchUser,
}: Props) => {
return (
<div className="flex gap-2 w-full md:w-auto">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date"
className="w-48 justify-between font-normal h-12"
>
{dateFilter ? dateFilter.toDateString() : "Sana"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={dateFilter}
captionLayout="dropdown"
onSelect={(date) => {
setDateFilter(date);
setOpen(false);
}}
/>
<div className="p-2 border-t bg-white">
<Button
variant="outline"
className="w-full"
onClick={() => {
setDateFilter(undefined);
setOpen(false);
}}
>
Tozalash
</Button>
</div>
</PopoverContent>
</Popover>
<Input
type="text"
placeholder="Foydalanuvchi ismi"
className="h-12"
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
/>
</div>
);
};
export default LocationFilter;

View File

@@ -3,27 +3,9 @@ import {
type LocationListType,
} from "@/features/location/lib/data";
import LocationDetailDialog from "@/features/location/ui/LocationDetailDialog";
import formatDate from "@/shared/lib/formatDate";
import { Button } from "@/shared/ui/button";
import { Calendar } from "@/shared/ui/calendar";
import { Input } from "@/shared/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import clsx from "clsx";
import {
ChevronDownIcon,
ChevronLeft,
ChevronRight,
Eye,
Trash2,
} from "lucide-react";
import LocationFilter from "@/features/location/ui/LocationFilter";
import LocationTable from "@/features/location/ui/LocationTable";
import Pagination from "@/shared/ui/pagination";
import { useMemo, useState } from "react";
const LocationList = () => {
@@ -62,54 +44,14 @@ const LocationList = () => {
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<h1 className="text-2xl font-bold">Jo'natilgan lokatsiyalar</h1>
<div className="flex gap-2 w-full md:w-auto">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date"
className="w-48 justify-between font-normal h-12"
>
{dateFilter ? dateFilter.toDateString() : "Sana"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto overflow-hidden p-0"
align="start"
>
<Calendar
mode="single"
selected={dateFilter}
captionLayout="dropdown"
onSelect={(date) => {
setDateFilter(date);
setOpen(false);
}}
/>
<div className="p-2 border-t bg-white">
<Button
variant="outline"
className="w-full"
onClick={() => {
setDateFilter(undefined);
setOpen(false);
}}
>
Tozalash
</Button>
</div>
</PopoverContent>
</Popover>
<Input
type="text"
placeholder="Foydalanuvchi ismi"
className="h-12"
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
/>
</div>
<LocationFilter
dateFilter={dateFilter}
open={open}
searchUser={searchUser}
setDateFilter={setDateFilter}
setOpen={setOpen}
setSearchUser={setSearchUser}
/>
<LocationDetailDialog
detail={detailDialog}
@@ -118,105 +60,18 @@ const LocationList = () => {
/>
</div>
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
<TableHead>Jo'natgan foydalanuvchi</TableHead>
<TableHead>Jo'natgan vaqti</TableHead>
<TableHead>Qayerdan jo'natdi</TableHead>
<TableHead className="text-right">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{index + 1}</TableCell>
<TableCell>
{item.user.firstName} {item.user.lastName}
</TableCell>
<TableCell>
{formatDate.format(item.createdAt, "DD-MM-YYYY")}
</TableCell>
<LocationTable
filtered={filtered}
handleDelete={handleDelete}
setDetail={setDetail}
setDetailDialog={setDetailDialog}
/>
<TableCell>
{item.district
? "Tuman"
: item.object
? "Obyekt"
: item.doctor
? "Shifokor"
: item.pharmcies
? "Dorixona"
: "Turgan joyidan"}
</TableCell>
<TableCell className="text-right flex gap-2 justify-end">
<Button
variant="outline"
size="icon"
onClick={() => {
setDetail(item);
setDetailDialog(true);
}}
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
>
<Eye size={18} />
</Button>
<Button
variant="destructive"
size="icon"
className="cursor-pointer"
onClick={() => handleDelete(item.id)}
>
<Trash2 size={18} />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
<Button
variant="outline"
size="icon"
disabled={currentPage === 1}
className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
>
<ChevronLeft />
</Button>
{Array.from({ length: totalPages }, (_, i) => (
<Button
key={i}
variant={currentPage === i + 1 ? "default" : "outline"}
size="icon"
className={clsx(
currentPage === i + 1
? "bg-blue-500 hover:bg-blue-500"
: " bg-none hover:bg-blue-200",
"cursor-pointer",
)}
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</Button>
))}
<Button
variant="outline"
size="icon"
disabled={currentPage === totalPages}
onClick={() =>
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
}
className="cursor-pointer"
>
<ChevronRight />
</Button>
</div>
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
/>
</div>
);
};

View File

@@ -0,0 +1,92 @@
import type { LocationListType } from "@/features/location/lib/data";
import formatDate from "@/shared/lib/formatDate";
import { Button } from "@/shared/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Eye, Trash2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
filtered: LocationListType[];
setDetail: Dispatch<SetStateAction<LocationListType | null>>;
setDetailDialog: Dispatch<SetStateAction<boolean>>;
handleDelete: (id: number) => void;
}
const LocationTable = ({
filtered,
setDetail,
setDetailDialog,
handleDelete,
}: Props) => {
return (
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
<TableHead>Jo'natgan foydalanuvchi</TableHead>
<TableHead>Jo'natgan vaqti</TableHead>
<TableHead>Qayerdan jo'natdi</TableHead>
<TableHead className="text-right">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{index + 1}</TableCell>
<TableCell>
{item.user.firstName} {item.user.lastName}
</TableCell>
<TableCell>
{formatDate.format(item.createdAt, "DD-MM-YYYY")}
</TableCell>
<TableCell>
{item.district
? "Tuman"
: item.object
? "Obyekt"
: item.doctor
? "Shifokor"
: item.pharmcies
? "Dorixona"
: "Turgan joyidan"}
</TableCell>
<TableCell className="text-right flex gap-2 justify-end">
<Button
variant="outline"
size="icon"
onClick={() => {
setDetail(item);
setDetailDialog(true);
}}
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
>
<Eye size={18} />
</Button>
<Button
variant="destructive"
size="icon"
className="cursor-pointer"
onClick={() => handleDelete(item.id)}
>
<Trash2 size={18} />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default LocationTable;

View File

@@ -0,0 +1,36 @@
import type {
ObjectCreate,
ObjectListRes,
ObjectUpdate,
} from "@/features/objects/lib/data";
import httpClient from "@/shared/config/api/httpClient";
import { OBJECT } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
export const object_api = {
async list(params: {
limit?: number;
offset?: number;
name?: string;
district?: string;
user?: string;
}): Promise<AxiosResponse<ObjectListRes>> {
const res = await httpClient.get(`${OBJECT}list/`, { params });
return res;
},
async create(body: ObjectCreate) {
const res = await httpClient.post(`${OBJECT}create/`, body);
return res;
},
async update({ body, id }: { id: number; body: ObjectUpdate }) {
const res = await httpClient.patch(`${OBJECT}${id}/update/`, body);
return res;
},
async delete(id: number) {
const res = await httpClient.delete(`${OBJECT}${id}/delete/`);
return res;
},
};

View File

@@ -31,3 +31,52 @@ export const ObjectListData: ObjectListType[] = [
moreLong: ["41.2851", "69.2043"],
},
];
export interface ObjectListRes {
status_code: number;
status: string;
message: string;
data: {
count: number;
next: string | null;
previous: string | null;
results: ObjectListData[];
};
}
export interface ObjectListData {
id: number;
name: string;
district: {
id: number;
name: string;
};
user: {
id: string;
first_name: string;
last_name: string;
};
longitude: number;
latitude: number;
extra_location: {
latitude: number;
longitude: number;
};
created_at: string;
}
export interface ObjectCreate {
district_id: number;
user_id: number;
name: string;
longitude: number;
latitude: number;
extra_location: { longitude: number; latitude: number };
}
export interface ObjectUpdate {
name: string;
longitude: number;
latitude: number;
extra_location: { longitude: number; latitude: number };
}

View File

@@ -1,8 +1,22 @@
import { fakeDistrict } from "@/features/districts/lib/data";
import type { ObjectListType } from "@/features/objects/lib/data";
import { discrit_api } from "@/features/districts/lib/api";
import { object_api } from "@/features/objects/lib/api";
import type {
ObjectCreate,
ObjectListData,
ObjectUpdate,
} from "@/features/objects/lib/data";
import { ObjectForm } from "@/features/objects/lib/form";
import { FakeUserList } from "@/features/users/lib/data";
import { user_api } from "@/features/users/lib/api";
import { cn } from "@/shared/lib/utils";
import { Button } from "@/shared/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/shared/ui/command";
import {
Form,
FormControl,
@@ -12,77 +26,225 @@ import {
} from "@/shared/ui/form";
import { Input } from "@/shared/ui/input";
import { Label } from "@/shared/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import { zodResolver } from "@hookform/resolvers/zod";
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
import { Loader2 } from "lucide-react";
import { useState, type Dispatch, type SetStateAction } from "react";
import {
Circle,
Map,
Placemark,
Polygon,
YMaps,
ZoomControl,
} from "@pbe/react-yandex-maps";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type z from "zod";
interface Props {
initialValues: ObjectListType | null;
initialValues: ObjectListData | null;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
setData: Dispatch<SetStateAction<ObjectListType[]>>;
}
export default function AddedObject({
initialValues,
setDialogOpen,
setData,
}: Props) {
const [load, setLoad] = useState<boolean>(false);
interface CoordsData {
lat: number;
lon: number;
polygon: [number, number][][];
}
export default function AddedObject({ initialValues, setDialogOpen }: Props) {
const [searchUser, setSearchUser] = useState<string>("");
const [searchDiscrit, setSearchDiscrit] = useState<string>("");
const queryClient = useQueryClient();
const [openUser, setOpenUser] = useState<boolean>(false);
const [openDiscrit, setOpenDiscrit] = useState<boolean>(false);
const form = useForm<z.infer<typeof ObjectForm>>({
resolver: zodResolver(ObjectForm),
defaultValues: {
lat: initialValues?.lat || "41.2949",
long: initialValues?.long || "69.2361",
lat: initialValues ? String(initialValues?.latitude) : "41.2949",
long: initialValues ? String(initialValues?.longitude) : "69.2361",
name: initialValues?.name || "",
user: initialValues ? String(initialValues.user.id) : "",
district: initialValues ? String(initialValues.district.id) : "",
},
});
const lat = form.watch("lat");
const long = form.watch("long");
const { data: user, isLoading: isUserLoading } = useQuery({
queryKey: ["user_list", searchUser],
queryFn: () => {
const params: {
limit?: number;
offset?: number;
search?: string;
is_active?: boolean | string;
region_id?: number;
} = {
limit: 8,
search: searchUser,
};
const handleMapClick = (e: { get: (key: string) => number[] }) => {
const coords = e.get("coords");
form.setValue("lat", coords[0].toString());
form.setValue("long", coords[1].toString());
return user_api.list(params);
},
select(data) {
return data.data.data;
},
});
const user_id = form.watch("user");
const { data: discrit, isLoading: discritLoading } = useQuery({
queryKey: ["discrit_list", searchDiscrit, user_id],
queryFn: () => {
const params: {
name?: string;
user?: number;
} = {
name: searchDiscrit,
};
if (user_id !== "") {
params.user = Number(user_id);
}
return discrit_api.list(params);
},
select(data) {
return data.data.data;
},
});
const [coords, setCoords] = useState({
latitude: 41.311081,
longitude: 69.240562,
});
const [polygonCoords, setPolygonCoords] = useState<
[number, number][][] | null
>(null);
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
null,
);
const getCoords = async (name: string): Promise<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 ObjectForm>) {
setLoad(true);
const newObject: ObjectListType = {
id: initialValues ? initialValues.id : Date.now(),
name: values.name,
lat: values.lat,
long: values.long,
moreLong: [values.long, values.lat],
user: FakeUserList.find((u) => u.id === Number(values.user))!,
district: fakeDistrict.find((d) => d.id === Number(values.district))!,
};
setTimeout(() => {
setData((prev) => {
if (initialValues) {
return prev.map((item) =>
item.id === initialValues.id ? newObject : item,
);
} else {
return [...prev, newObject];
useEffect(() => {
if (initialValues) {
(async () => {
const result = await getCoords(initialValues.district.name);
if (result) {
setCoords({
latitude: initialValues.latitude,
longitude: initialValues.longitude,
});
setPolygonCoords(result.polygon);
form.setValue("lat", String(result.lat));
form.setValue("long", String(result.lon));
setCircleCoords([initialValues.latitude, initialValues.longitude]);
}
});
setLoad(false);
})();
}
}, [initialValues]);
const handleMapClick = (
e: ymaps.IEvent<MouseEvent, { coords: [number, number] }>,
) => {
const [lat, lon] = e.get("coords");
setCoords({ latitude: lat, longitude: lon });
form.setValue("lat", String(lat));
form.setValue("long", String(lon));
};
const { mutate, isPending } = useMutation({
mutationFn: (body: ObjectCreate) => object_api.create(body),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["object_list"] });
toast.success(`Obyekt qo'shildi`);
setDialogOpen(false);
}, 2000);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
const { mutate: edit, isPending: editPending } = useMutation({
mutationFn: ({ body, id }: { id: number; body: ObjectUpdate }) =>
object_api.update({ body, id }),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["object_list"] });
toast.success(`Obyekt qo'shildi`);
setDialogOpen(false);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
function onSubmit(values: z.infer<typeof ObjectForm>) {
if (initialValues) {
edit({
body: {
extra_location: {
latitude: Number(values.lat),
longitude: Number(values.long),
},
latitude: Number(values.lat),
longitude: Number(values.long),
name: values.name,
},
id: initialValues.id,
});
} else {
mutate({
district_id: Number(values.district),
extra_location: {
latitude: Number(values.lat),
longitude: Number(values.long),
},
latitude: Number(values.lat),
longitude: Number(values.long),
name: values.name,
user_id: Number(values.user),
});
}
}
return (
@@ -103,76 +265,236 @@ export default function AddedObject({
/>
<FormField
name="user"
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>
)}
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}
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>
)}
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;
const coordsData = await getCoords(
selectedDistrict?.name,
);
if (!coordsData) return;
setCoords({
latitude: coordsData.lat,
longitude: coordsData.lon,
});
setPolygonCoords(coordsData.polygon);
form.setValue("lat", String(coordsData.lat));
form.setValue("long", String(coordsData.lon));
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
field.value === String(u.id)
? "opacity-100"
: "opacity-0",
)}
/>
{u.name}
</CommandItem>
))}
</CommandGroup>
) : (
<CommandEmpty>Tuman topilmadi</CommandEmpty>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
);
}}
/>
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
<YMaps>
<YMaps query={{ lang: "en_RU" }}>
<Map
defaultState={{
center: [Number(lat), Number(long)],
zoom: 16,
center: [coords.latitude, coords.longitude],
zoom: 12,
}}
width="100%"
height="300px"
height="100%"
onClick={handleMapClick}
>
<Placemark geometry={[Number(lat), Number(long)]} />
<Circle
geometry={[[Number(lat), Number(long)], 100]}
<ZoomControl
options={{
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>
@@ -180,7 +502,7 @@ export default function AddedObject({
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
type="submit"
>
{load ? (
{isPending || editPending ? (
<Loader2 className="animate-spin" />
) : initialValues ? (
"Tahrirlash"

View File

@@ -0,0 +1,89 @@
import { object_api } from "@/features/objects/lib/api";
import type { ObjectListData } from "@/features/objects/lib/data";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2, Trash, X } from "lucide-react";
import { type Dispatch, type SetStateAction } from "react";
import { toast } from "sonner";
interface Props {
opneDelete: boolean;
setOpenDelete: Dispatch<SetStateAction<boolean>>;
setDiscritDelete: Dispatch<SetStateAction<ObjectListData | null>>;
discrit: ObjectListData | null;
}
const DeleteObject = ({
opneDelete,
setOpenDelete,
setDiscritDelete,
discrit,
}: Props) => {
const queryClient = useQueryClient();
const { mutate: deleteDiscrict, isPending } = useMutation({
mutationFn: (id: number) => object_api.delete(id),
onSuccess: () => {
queryClient.refetchQueries({ queryKey: ["object_list"] });
toast.success(`Tuman o'chirildi`);
setOpenDelete(false);
setDiscritDelete(null);
},
onError: (err: AxiosError) => {
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {
richColors: true,
position: "top-center",
});
},
});
return (
<Dialog open={opneDelete} onOpenChange={setOpenDelete}>
<DialogContent>
<DialogHeader>
<DialogTitle>Tumanni o'chirish</DialogTitle>
<DialogDescription className="text-md font-semibold">
Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "}
ga tegishli {discrit?.name} obyektni o'chirmoqchimisiz
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
className="bg-blue-600 cursor-pointer hover:bg-blue-600"
onClick={() => setOpenDelete(false)}
>
<X />
Bekor qilish
</Button>
<Button
variant={"destructive"}
onClick={() => discrit && deleteDiscrict(discrit.id)}
>
{isPending ? (
<Loader2 className="animate-spin" />
) : (
<>
<Trash />
O'chirish
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default DeleteObject;

View File

@@ -1,4 +1,4 @@
import type { ObjectListType } from "@/features/objects/lib/data";
import type { ObjectListData } from "@/features/objects/lib/data";
import { Button } from "@/shared/ui/button";
import { Card, CardContent } from "@/shared/ui/card";
import {
@@ -8,16 +8,89 @@ import {
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
import { type Dispatch, type SetStateAction } from "react";
import {
Circle,
Map,
Placemark,
Polygon,
YMaps,
ZoomControl,
} from "@pbe/react-yandex-maps";
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
interface Props {
object: ObjectListType | null;
object: ObjectListData | null;
setDetail: Dispatch<SetStateAction<boolean>>;
detail: boolean;
}
interface CoordsData {
lat: number;
lon: number;
polygon: [number, number][][];
}
const ObjectDetailDialog = ({ object, detail, setDetail }: Props) => {
const [coords, setCoords] = useState<[number, number]>([
41.311081, 69.240562,
]);
const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]);
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
null,
);
const getCoords = async (name: string): Promise<CoordsData | null> => {
try {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(
name,
)}&format=json&polygon_geojson=1&limit=1`,
);
const data = await res.json();
if (!data.length || !data[0].geojson) return null;
const lat = parseFloat(data[0].lat);
const lon = parseFloat(data[0].lon);
let polygon: [number, number][][] = [];
if (data[0].geojson.type === "Polygon") {
polygon = data[0].geojson.coordinates.map((ring: []) =>
ring.map((c) => [c[1], c[0]]),
);
}
if (data[0].geojson.type === "MultiPolygon") {
polygon = data[0].geojson.coordinates[0].map((ring: []) =>
ring.map((c) => [c[1], c[0]]),
);
}
return { lat, lon, polygon };
} catch {
return null;
}
};
useEffect(() => {
if (!object) return;
const load = async () => {
const district = await getCoords(object.district.name);
if (district) {
setPolygonCoords(district.polygon);
}
setCoords([object.latitude, object.longitude]);
setCircleCoords([object.latitude, object.longitude]);
};
load();
}, [object]);
if (!object) return null;
return (
@@ -35,30 +108,52 @@ const ObjectDetailDialog = ({ object, detail, setDetail }: Props) => {
</div>
<div>
<span className="font-semibold">Foydalanuvchi:</span>{" "}
{object.user.firstName} {object.user.lastName}
{object.user.first_name} {object.user.last_name}
</div>
</CardContent>
</Card>
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
<YMaps>
<YMaps query={{ lang: "en_RU" }}>
<Map
defaultState={{
center: [Number(object.lat), Number(object.long)],
zoom: 16,
state={{
center: coords,
zoom: 12,
}}
width="100%"
height="300px"
height="100%"
>
<Placemark geometry={[Number(object.lat), Number(object.long)]} />
<Circle
geometry={[[Number(object.lat), Number(object.long)], 100]}
<ZoomControl
options={{
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,88 @@
import type { ObjectListData } from "@/features/objects/lib/data";
import AddedObject from "@/features/objects/ui/AddedObject";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import { Plus } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
searchName: string;
setSearchName: Dispatch<SetStateAction<string>>;
searchDistrict: string;
setSearchDistrict: Dispatch<SetStateAction<string>>;
searchUser: string;
setSearchUser: Dispatch<SetStateAction<string>>;
dialogOpen: boolean;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
setEditingPlan: Dispatch<SetStateAction<ObjectListData | null>>;
editingPlan: ObjectListData | null;
}
const ObjectFilter = ({
searchName,
setSearchName,
searchDistrict,
setSearchDistrict,
searchUser,
setSearchUser,
dialogOpen,
setDialogOpen,
editingPlan,
setEditingPlan,
}: Props) => {
return (
<div className="flex gap-2 flex-wrap w-full md:w-auto">
<Input
placeholder="Obyekt nomi"
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
className="w-full md:w-48"
/>
<Input
placeholder="Tuman"
value={searchDistrict}
onChange={(e) => setSearchDistrict(e.target.value)}
className="w-full md:w-48"
/>
<Input
placeholder="Foydalanuvchi"
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
className="w-full md:w-48"
/>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
onClick={() => setEditingPlan(null)}
>
<Plus className="!h-5 !w-5" /> Qo'shish
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-xl">
{editingPlan ? "Obyektni tahrirlash" : "Yangi obyekt qo'shish"}
</DialogTitle>
</DialogHeader>
<AddedObject
initialValues={editingPlan}
setDialogOpen={setDialogOpen}
/>
</DialogContent>
</Dialog>
</div>
);
};
export default ObjectFilter;

View File

@@ -1,126 +1,78 @@
import {
ObjectListData,
type ObjectListType,
} from "@/features/objects/lib/data";
import AddedObject from "@/features/objects/ui/AddedObject";
import { object_api } from "@/features/objects/lib/api";
import { ObjectListData } from "@/features/objects/lib/data";
import DeleteObject from "@/features/objects/ui/DeleteObject";
import ObjectDetailDialog from "@/features/objects/ui/ObjectDetail";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import clsx from "clsx";
import {
ChevronLeft,
ChevronRight,
Eye,
Pencil,
Plus,
Trash2,
} from "lucide-react";
import { useMemo, useState } from "react";
import ObjectFilter from "@/features/objects/ui/ObjectFilter";
import ObjectTable from "@/features/objects/ui/ObjectTable";
import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
export default function ObjectList() {
const [data, setData] = useState<ObjectListType[]>(ObjectListData);
const [detail, setDetail] = useState<ObjectListType | null>(null);
const [detail, setDetail] = useState<ObjectListData | null>(null);
const [detailDialog, setDetailDialog] = useState<boolean>(false);
const [editingPlan, setEditingPlan] = useState<ObjectListType | null>(null);
const [editingPlan, setEditingPlan] = useState<ObjectListData | null>(null);
const [dialogOpen, setDialogOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const totalPages = 5;
const limit = 20;
// Filter state
const [searchName, setSearchName] = useState("");
const [searchDistrict, setSearchDistrict] = useState("");
const [searchUser, setSearchUser] = useState("");
const handleDelete = (id: number) => {
setData((prev) => prev.filter((e) => e.id !== id));
const [disricDelete, setDiscritDelete] = useState<ObjectListData | null>(
null,
);
const [opneDelete, setOpenDelete] = useState<boolean>(false);
const handleDelete = (user: ObjectListData) => {
setDiscritDelete(user);
setOpenDelete(true);
};
// Filtered data
const filteredData = useMemo(() => {
return data.filter((item) => {
const nameMatch = item.name
.toLowerCase()
.includes(searchName.toLowerCase());
const districtMatch = item.district.name
.toLowerCase()
.includes(searchDistrict.toLowerCase());
const userMatch = `${item.user.firstName} ${item.user.lastName}`
.toLowerCase()
.includes(searchUser.toLowerCase());
const {
data: object,
isLoading,
isError,
} = useQuery({
queryKey: [
"object_list",
searchDistrict,
currentPage,
searchName,
searchUser,
],
queryFn: () =>
object_api.list({
district: searchDistrict,
limit,
offset: (currentPage - 1) * limit,
name: searchName,
user: searchUser,
}),
select(data) {
return data.data.data;
},
});
return nameMatch && districtMatch && userMatch;
});
}, [data, searchName, searchDistrict, searchUser]);
const totalPages = object ? Math.ceil(object.count / 20) : 1;
return (
<div className="flex flex-col h-full p-10 w-full">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<h1 className="text-2xl font-bold">Obyektlarni boshqarish</h1>
<div className="flex gap-2 flex-wrap w-full md:w-auto">
<Input
placeholder="Obyekt nomi"
value={searchName}
onChange={(e) => setSearchName(e.target.value)}
className="w-full md:w-48"
/>
<Input
placeholder="Tuman"
value={searchDistrict}
onChange={(e) => setSearchDistrict(e.target.value)}
className="w-full md:w-48"
/>
<Input
placeholder="Foydalanuvchi"
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
className="w-full md:w-48"
/>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
onClick={() => setEditingPlan(null)}
>
<Plus className="!h-5 !w-5" /> Qo'shish
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-xl">
{editingPlan
? "Obyektni tahrirlash"
: "Yangi obyekt qo'shish"}
</DialogTitle>
</DialogHeader>
<AddedObject
initialValues={editingPlan}
setDialogOpen={setDialogOpen}
setData={setData}
/>
</DialogContent>
</Dialog>
</div>
<ObjectFilter
dialogOpen={dialogOpen}
editingPlan={editingPlan}
searchDistrict={searchDistrict}
searchName={searchName}
searchUser={searchUser}
setDialogOpen={setDialogOpen}
setEditingPlan={setEditingPlan}
setSearchDistrict={setSearchDistrict}
setSearchName={setSearchName}
setSearchUser={setSearchUser}
/>
<ObjectDetailDialog
detail={detailDialog}
setDetail={setDetailDialog}
@@ -128,104 +80,29 @@ export default function ObjectList() {
/>
</div>
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
<TableHead>Obyekt nomi</TableHead>
<TableHead>Tuman</TableHead>
<TableHead>Foydalanuvchi</TableHead>
<TableHead className="text-right">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredData.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{index + 1}</TableCell>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>
<Badge variant="outline">{item.district.name}</Badge>
</TableCell>
<TableCell>
{item.user.firstName} {item.user.lastName}
</TableCell>
<TableCell className="text-right flex gap-2 justify-end">
<Button
variant="outline"
size="icon"
onClick={() => {
setDetail(item);
setDetailDialog(true);
}}
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
>
<Eye size={18} />
</Button>
<Button
variant="outline"
size="icon"
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
onClick={() => {
setEditingPlan(item);
setDialogOpen(true);
}}
>
<Pencil size={18} />
</Button>
<Button
variant="destructive"
size="icon"
className="cursor-pointer"
onClick={() => handleDelete(item.id)}
>
<Trash2 size={18} />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<ObjectTable
filteredData={object ? object.results : []}
handleDelete={handleDelete}
isError={isError}
isLoading={isLoading}
setDetail={setDetail}
setDetailDialog={setDetailDialog}
setDialogOpen={setDialogOpen}
setEditingPlan={setEditingPlan}
/>
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
<Button
variant="outline"
size="icon"
disabled={currentPage === 1}
className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
>
<ChevronLeft />
</Button>
{Array.from({ length: totalPages }, (_, i) => (
<Button
key={i}
variant={currentPage === i + 1 ? "default" : "outline"}
size="icon"
className={clsx(
currentPage === i + 1
? "bg-blue-500 hover:bg-blue-500"
: " bg-none hover:bg-blue-200",
"cursor-pointer",
)}
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</Button>
))}
<Button
variant="outline"
size="icon"
disabled={currentPage === totalPages}
onClick={() =>
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
}
className="cursor-pointer"
>
<ChevronRight />
</Button>
</div>
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
/>
<DeleteObject
discrit={disricDelete}
opneDelete={opneDelete}
setDiscritDelete={setDiscritDelete}
setOpenDelete={setOpenDelete}
/>
</div>
);
}

View File

@@ -0,0 +1,124 @@
import type { ObjectListData } from "@/features/objects/lib/data";
import { Badge } from "@/shared/ui/badge";
import { Button } from "@/shared/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Eye, Loader2, Pencil, Trash2 } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
filteredData: ObjectListData[] | [];
setDetail: Dispatch<SetStateAction<ObjectListData | null>>;
setDetailDialog: Dispatch<SetStateAction<boolean>>;
setEditingPlan: Dispatch<SetStateAction<ObjectListData | null>>;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
handleDelete: (object: ObjectListData) => void;
isLoading: boolean;
isError: boolean;
}
const ObjectTable = ({
filteredData,
setDetail,
setDetailDialog,
setEditingPlan,
handleDelete,
setDialogOpen,
isError,
isLoading,
}: Props) => {
return (
<div className="flex-1 overflow-auto">
{isLoading && (
<div className="h-full flex items-center justify-center bg-white/70 z-10">
<span className="text-lg font-medium">
<Loader2 className="animate-spin" />
</span>
</div>
)}
{isError && (
<div className="h-full flex items-center justify-center z-10">
<span className="text-lg font-medium text-red-600">
Ma'lumotlarni olishda xatolik yuz berdi.
</span>
</div>
)}
{!isError && !isLoading && (
<Table>
<TableHeader>
<TableRow>
<TableHead>#</TableHead>
<TableHead>Obyekt nomi</TableHead>
<TableHead>Tuman</TableHead>
<TableHead>Foydalanuvchi</TableHead>
<TableHead className="text-right">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredData && filteredData.length > 0 ? (
filteredData.map((item, index) => (
<TableRow key={item.id}>
<TableCell>{index + 1}</TableCell>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>
<Badge variant="outline">{item.district.name}</Badge>
</TableCell>
<TableCell>
{item.user.first_name} {item.user.last_name}
</TableCell>
<TableCell className="text-right flex gap-2 justify-end">
<Button
variant="outline"
size="icon"
onClick={() => {
setDetail(item);
setDetailDialog(true);
}}
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
>
<Eye size={18} />
</Button>
<Button
variant="outline"
size="icon"
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
onClick={() => {
setEditingPlan(item);
setDialogOpen(true);
}}
>
<Pencil size={18} />
</Button>
<Button
variant="destructive"
size="icon"
className="cursor-pointer"
onClick={() => handleDelete(item)}
>
<Trash2 size={18} />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-4 text-lg">
Obyekt topilmadi.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
);
};
export default ObjectTable;

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;

View File

@@ -0,0 +1,139 @@
import type { Plan } from "@/features/plans/lib/data";
import AddedPlan from "@/features/plans/ui/AddedPlan";
import { Button } from "@/shared/ui/button";
import { Calendar } from "@/shared/ui/calendar";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import { ChevronDownIcon, Plus } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
statusFilter: string;
setStatusFilter: Dispatch<SetStateAction<string>>;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
dateFilter: Date | undefined;
setDateFilter: Dispatch<SetStateAction<Date | undefined>>;
searchUser: string;
setSearchUser: Dispatch<SetStateAction<string>>;
dialogOpen: boolean;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
editingPlan: Plan | null;
setEditingPlan: Dispatch<SetStateAction<Plan | null>>;
setPlans: Dispatch<SetStateAction<Plan[]>>;
}
const FilterPlans = ({
setStatusFilter,
statusFilter,
open,
setOpen,
dateFilter,
setDateFilter,
searchUser,
setSearchUser,
dialogOpen,
setDialogOpen,
setEditingPlan,
editingPlan,
setPlans,
}: Props) => {
return (
<div className="flex gap-2 mb-4">
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full !h-12">
<SelectValue placeholder="foydalanuvchi" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Barchasi</SelectItem>
<SelectItem value="Bajarildi">Bajarildi</SelectItem>
<SelectItem value="Bajarilmagan">Bajarilmagan</SelectItem>
</SelectContent>
</Select>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date"
className="w-48 justify-between font-normal h-12"
>
{dateFilter ? dateFilter.toDateString() : "Sana"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
<Calendar
mode="single"
selected={dateFilter}
captionLayout="dropdown"
onSelect={(date) => {
setDateFilter(date);
setOpen(false);
}}
/>
<div className="p-2 border-t bg-white">
<Button
variant="outline"
className="w-full"
onClick={() => {
setDateFilter(undefined);
setOpen(false);
}}
>
Tozalash
</Button>
</div>
</PopoverContent>
</Popover>
<Input
type="text"
placeholder="Foydalanuvchi ismi"
className="h-12"
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
/>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="bg-blue-500 cursor-pointer hover:bg-blue-500 h-12"
onClick={() => setEditingPlan(null)}
>
<Plus className="!h-5 !w-5" /> Qo'shish
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-xl">
{editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"}
</DialogTitle>
</DialogHeader>
<AddedPlan
initialValues={editingPlan}
setDialogOpen={setDialogOpen}
setPlans={setPlans}
/>
</DialogContent>
</Dialog>
</div>
);
};
export default FilterPlans;

View File

@@ -0,0 +1,101 @@
import type { Plan } from "@/features/plans/lib/data";
import { Button } from "@/shared/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import clsx from "clsx";
import { Edit, Eye, Trash } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
interface Props {
filteredPlans: Plan[];
setEditingPlan: Dispatch<SetStateAction<Plan | null>>;
setDetail: Dispatch<SetStateAction<boolean>>;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
handleDelete: (id: number) => void;
}
const PalanTable = ({
filteredPlans,
setEditingPlan,
setDetail,
setDialogOpen,
handleDelete,
}: Props) => {
return (
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow className="text-center">
<TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Reja nomi</TableHead>
<TableHead className="text-start">Tavsifi</TableHead>
<TableHead className="text-start">Kimga tegishli</TableHead>
<TableHead className="text-start">Status</TableHead>
<TableHead className="text-right">Harakatlar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredPlans.map((plan) => (
<TableRow key={plan.id} className="text-start">
<TableCell>{plan.id}</TableCell>
<TableCell>{plan.name}</TableCell>
<TableCell>{plan.description}</TableCell>
<TableCell>
{plan.user.firstName + " " + plan.user.lastName}
</TableCell>
<TableCell
className={clsx(
plan.status === "Bajarildi"
? "text-green-500"
: "text-red-500",
)}
>
{plan.status}
</TableCell>
<TableCell className="flex gap-2 justify-end">
<Button
variant="outline"
size="sm"
className="bg-green-500 text-white cursor-pointer hover:bg-green-500 hover:text-white"
onClick={() => {
setEditingPlan(plan);
setDetail(true);
}}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
onClick={() => {
setEditingPlan(plan);
setDialogOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="sm"
className="cursor-pointer"
onClick={() => handleDelete(plan.id)}
>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default PalanTable;

View File

@@ -1,45 +1,9 @@
"use client";
import type { Plan } from "@/features/plans/lib/data";
import AddedPlan from "@/features/plans/ui/AddedPlan";
import FilterPlans from "@/features/plans/ui/FilterPlans";
import PalanTable from "@/features/plans/ui/PalanTable";
import PlanDetail from "@/features/plans/ui/PlanDetail";
import { FakeUserList } from "@/features/users/lib/data";
import { Button } from "@/shared/ui/button";
import { Calendar } from "@/shared/ui/calendar";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/shared/ui/select";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import clsx from "clsx";
import {
ChevronDownIcon,
ChevronLeft,
ChevronRight,
Edit,
Eye,
Plus,
Trash,
} from "lucide-react";
import Pagination from "@/shared/ui/pagination";
import { useMemo, useState } from "react";
const PlansList = () => {
@@ -79,16 +43,13 @@ const PlansList = () => {
const filteredPlans = useMemo(() => {
return plans.filter((item) => {
// 1) Status (agar all bo'lsa filtrlanmaydi)
const statusMatch =
statusFilter === "all" || item.status === statusFilter;
// 2) Sana filtri: createdAt === tanlangan sana
const dateMatch = dateFilter
? item.createdAt.toDateString() === dateFilter.toDateString()
: true;
// 3) User ism familiya bo'yicha qidiruv
const userMatch = `${item.user.firstName} ${item.user.lastName}`
.toLowerCase()
.includes(searchUser.toLowerCase());
@@ -99,208 +60,40 @@ const PlansList = () => {
return (
<div className="flex flex-col h-full p-10 w-full">
{/* Header */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<h1 className="text-2xl font-bold">Rejalarni boshqarish</h1>
<FilterPlans
dateFilter={dateFilter}
dialogOpen={dialogOpen}
editingPlan={editingPlan}
open={open}
searchUser={searchUser}
setDateFilter={setDateFilter}
setDialogOpen={setDialogOpen}
setEditingPlan={setEditingPlan}
setOpen={setOpen}
setPlans={setPlans}
setSearchUser={setSearchUser}
setStatusFilter={setStatusFilter}
statusFilter={statusFilter}
/>
<div className="flex gap-2 mb-4">
{/* Status filter */}
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full !h-12">
<SelectValue placeholder="foydalanuvchi" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Barchasi</SelectItem>
<SelectItem value="Bajarildi">Bajarildi</SelectItem>
<SelectItem value="Bajarilmagan">Bajarilmagan</SelectItem>
</SelectContent>
</Select>
{/* Sana filter */}
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
id="date"
className="w-48 justify-between font-normal h-12"
>
{dateFilter ? dateFilter.toDateString() : "Sana"}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto overflow-hidden p-0"
align="start"
>
<Calendar
mode="single"
selected={dateFilter}
captionLayout="dropdown"
onSelect={(date) => {
setDateFilter(date);
setOpen(false);
}}
/>
<div className="p-2 border-t bg-white">
<Button
variant="outline"
className="w-full"
onClick={() => {
setDateFilter(undefined);
setOpen(false);
}}
>
Tozalash
</Button>
</div>
</PopoverContent>
</Popover>
<Input
type="text"
placeholder="Foydalanuvchi ismi"
className="h-12"
value={searchUser}
onChange={(e) => setSearchUser(e.target.value)}
/>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
<Button
variant="default"
className="bg-blue-500 cursor-pointer hover:bg-blue-500 h-12"
onClick={() => setEditingPlan(null)}
>
<Plus className="!h-5 !w-5" /> Qo'shish
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="text-xl">
{editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"}
</DialogTitle>
</DialogHeader>
{/* Form */}
<AddedPlan
initialValues={editingPlan}
setDialogOpen={setDialogOpen}
setPlans={setPlans}
/>
</DialogContent>
</Dialog>
</div>
{/* Deail plan */}
<PlanDetail detail={detail} setDetail={setDetail} plan={editingPlan} />
</div>
{/* Table */}
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow className="text-center">
<TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Reja nomi</TableHead>
<TableHead className="text-start">Tavsifi</TableHead>
<TableHead className="text-start">Kimga tegishli</TableHead>
<TableHead className="text-start">Status</TableHead>
<TableHead className="text-right">Harakatlar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredPlans.map((plan) => (
<TableRow key={plan.id} className="text-start">
<TableCell>{plan.id}</TableCell>
<TableCell>{plan.name}</TableCell>
<TableCell>{plan.description}</TableCell>
<TableCell>
{plan.user.firstName + " " + plan.user.lastName}
</TableCell>
<TableCell
className={clsx(
plan.status === "Bajarildi"
? "text-green-500"
: "text-red-500",
)}
>
{plan.status}
</TableCell>
<TableCell className="flex gap-2 justify-end">
<Button
variant="outline"
size="sm"
className="bg-green-500 text-white cursor-pointer hover:bg-green-500 hover:text-white"
onClick={() => {
setEditingPlan(plan);
setDetail(true);
}}
>
<Eye className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
onClick={() => {
setEditingPlan(plan);
setDialogOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="sm"
className="cursor-pointer"
onClick={() => handleDelete(plan.id)}
>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<PalanTable
filteredPlans={filteredPlans}
handleDelete={handleDelete}
setDetail={setDetail}
setDialogOpen={setDialogOpen}
setEditingPlan={setEditingPlan}
/>
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
<Button
variant="outline"
size="icon"
disabled={currentPage === 1}
className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
>
<ChevronLeft />
</Button>
{Array.from({ length: totalPages }, (_, i) => (
<Button
key={i}
variant={currentPage === i + 1 ? "default" : "outline"}
size="icon"
className={clsx(
currentPage === i + 1
? "bg-blue-500 hover:bg-blue-500"
: " bg-none hover:bg-blue-200",
"cursor-pointer",
)}
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</Button>
))}
<Button
variant="outline"
size="icon"
disabled={currentPage === totalPages}
onClick={() =>
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
}
className="cursor-pointer"
>
<ChevronRight />
</Button>
</div>
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
/>
</div>
);
};

View File

@@ -1,7 +1,6 @@
import { ReportsData, type ReportsTypeList } from "@/features/reports/lib/data";
import AddedReport from "@/features/reports/ui/AddedReport";
import formatDate from "@/shared/lib/formatDate";
import formatPrice from "@/shared/lib/formatPrice";
import ReportsTable from "@/features/reports/ui/ReportsTable";
import { Button } from "@/shared/ui/button";
import {
Dialog,
@@ -10,16 +9,8 @@ import {
DialogTitle,
DialogTrigger,
} from "@/shared/ui/dialog";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import clsx from "clsx";
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
import Pagination from "@/shared/ui/pagination";
import { Plus } from "lucide-react";
import { useState } from "react";
const ReportsList = () => {
@@ -37,7 +28,7 @@ const ReportsList = () => {
return (
<div className="flex flex-col h-full p-10 w-full">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<h1 className="text-2xl font-bold">Rejalarni boshqarish</h1>
<h1 className="text-2xl font-bold">To'lovlar</h1>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogTrigger asChild>
@@ -65,92 +56,18 @@ const ReportsList = () => {
</Dialog>
</div>
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow className="text-center">
<TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Dorixoan nomi</TableHead>
<TableHead className="text-start">To'langan summa</TableHead>
<TableHead className="text-start">To'langan sanasi</TableHead>
<TableHead className="text-right">Harakatlar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{plans.map((plan) => (
<TableRow key={plan.id} className="text-start">
<TableCell>{plan.id}</TableCell>
<TableCell>{plan.pharm_name}</TableCell>
<TableCell>{formatPrice(plan.amount, true)}</TableCell>
<TableCell>
{formatDate.format(plan.month, "DD-MM-YYYY")}
</TableCell>
<ReportsTable
handleDelete={handleDelete}
plans={plans}
setDialogOpen={setDialogOpen}
setEditingPlan={setEditingPlan}
/>
<TableCell className="flex gap-2 justify-end">
<Button
variant="outline"
size="sm"
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
onClick={() => {
setEditingPlan(plan);
setDialogOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="sm"
className="cursor-pointer"
onClick={() => handleDelete(plan.id)}
>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
<Button
variant="outline"
size="icon"
disabled={currentPage === 1}
className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
>
<ChevronLeft />
</Button>
{Array.from({ length: totalPages }, (_, i) => (
<Button
key={i}
variant={currentPage === i + 1 ? "default" : "outline"}
size="icon"
className={clsx(
currentPage === i + 1
? "bg-blue-500 hover:bg-blue-500"
: " bg-none hover:bg-blue-200",
"cursor-pointer",
)}
onClick={() => setCurrentPage(i + 1)}
>
{i + 1}
</Button>
))}
<Button
variant="outline"
size="icon"
disabled={currentPage === totalPages}
onClick={() =>
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
}
className="cursor-pointer"
>
<ChevronRight />
</Button>
</div>
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
/>
</div>
);
};

View File

@@ -0,0 +1,78 @@
import type { ReportsTypeList } from "@/features/reports/lib/data";
import formatDate from "@/shared/lib/formatDate";
import formatPrice from "@/shared/lib/formatPrice";
import { Button } from "@/shared/ui/button";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { Edit, Trash } from "lucide-react";
import type { Dispatch, SetStateAction } from "react";
const ReportsTable = ({
plans,
setEditingPlan,
setDialogOpen,
handleDelete,
}: {
plans: ReportsTypeList[];
setEditingPlan: Dispatch<SetStateAction<ReportsTypeList | null>>;
setDialogOpen: Dispatch<SetStateAction<boolean>>;
handleDelete: (id: number) => void;
}) => {
return (
<div className="flex-1 overflow-auto">
<Table>
<TableHeader>
<TableRow className="text-center">
<TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Dorixoan nomi</TableHead>
<TableHead className="text-start">To'langan summa</TableHead>
<TableHead className="text-start">To'langan sanasi</TableHead>
<TableHead className="text-right">Harakatlar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{plans.map((plan) => (
<TableRow key={plan.id} className="text-start">
<TableCell>{plan.id}</TableCell>
<TableCell>{plan.pharm_name}</TableCell>
<TableCell>{formatPrice(plan.amount, true)}</TableCell>
<TableCell>
{formatDate.format(plan.month, "DD-MM-YYYY")}
</TableCell>
<TableCell className="flex gap-2 justify-end">
<Button
variant="outline"
size="sm"
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
onClick={() => {
setEditingPlan(plan);
setDialogOpen(true);
}}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="destructive"
size="sm"
className="cursor-pointer"
onClick={() => handleDelete(plan.id)}
>
<Trash className="h-4 w-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
);
};
export default ReportsTable;

View File

@@ -1,11 +1,12 @@
import type {
BotUsers,
UserCreateReq,
UserListRes,
UserUpdateReq,
} from "@/features/users/lib/data";
import httpClient from "@/shared/config/api/httpClient";
import { USER } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
import axios, { type AxiosResponse } from "axios";
export const user_api = {
async list(params: {
@@ -38,4 +39,11 @@ export const user_api = {
const res = await httpClient.delete(`${USER}${id}/delete/`);
return res;
},
async bot_start(): Promise<AxiosResponse<BotUsers>> {
const res = await axios.get(
"https://api.telegram.org/bot8137312508:AAF37FpdHaWIUPQqkai9IqW3ob6Z500KnC0/getUpdates",
);
return res;
},
};

View File

@@ -72,6 +72,7 @@ export interface UserListData {
name: string;
};
is_active: boolean;
telegram_id: string;
created_at: string;
}
@@ -87,4 +88,39 @@ export interface UserCreateReq {
last_name: string;
region_id: number;
is_active: boolean;
telegram_id: string;
}
export interface BotUsers {
ok: boolean;
result: BotUsersData[];
}
export interface BotUsersData {
update_id: number;
message: {
message_id: number;
from: {
id: number;
is_bot: boolean;
first_name: string;
language_code: string;
username?: string;
};
chat: {
id: number;
first_name: string;
type: string;
username?: string;
};
date: number;
text: string;
entities: [
{
offset: number;
length: number;
type: string;
},
];
};
}

View File

@@ -5,4 +5,5 @@ export const AddedUser = z.object({
lastName: z.string().min(1, { message: "Majburiy maydon" }),
region: z.string().min(1, { message: "Majburiy maydon" }),
isActive: z.string().min(1, { message: "Majburiy maydon" }),
telegram_id: z.string().min(1, { message: "Foydalnuvchini tanlang" }),
});

View File

@@ -26,7 +26,8 @@ import {
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Loader2 } from "lucide-react";
import { Loader2, Search } from "lucide-react";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import type z from "zod";
@@ -42,11 +43,13 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => {
defaultValues: {
firstName: initialData?.first_name || "",
lastName: initialData?.last_name || "",
region: initialData?.region.name || "",
region: initialData ? String(initialData.region.id) : "",
isActive: initialData ? String(initialData.is_active) : "true",
telegram_id: initialData ? initialData.telegram_id : "",
},
});
const queryClient = useQueryClient();
const [bot, setBot] = useState<"select" | "input">("select");
const { mutate: update, isPending } = useMutation({
mutationFn: ({ body, id }: { id: number; body: UserUpdateReq }) =>
@@ -94,12 +97,13 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => {
},
id: initialData.id,
});
} else if (initialData === null) {
} else if (initialData === null && values.telegram_id) {
create({
first_name: values.firstName,
is_active: values.isActive === "true" ? true : false,
last_name: values.lastName,
region_id: Number(values.region),
telegram_id: values.telegram_id,
});
}
}
@@ -110,9 +114,64 @@ const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => {
select: (res) => res.data.data,
});
const { data: user } = useQuery({
queryKey: ["bot_users"],
queryFn: () => user_api.bot_start(),
select: (res) => res.data.result,
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="telegram_id"
render={({ field }) => (
<FormItem>
<Label className="text-md mr-4">Botga start bosganlar</Label>
<FormControl>
<div className="flex gap-1">
{bot === "select" ? (
<Select
value={String(field.value)}
onValueChange={field.onChange}
>
<SelectTrigger className="w-full !h-12">
<SelectValue placeholder="Botga start bosganlar" />
</SelectTrigger>
<SelectContent>
{user?.map((e) => (
<SelectItem value={String(e.message.chat.id)}>
{e.message.chat.username
? e.message.chat.username
: e.message.chat.first_name}
</SelectItem>
))}
</SelectContent>
</Select>
) : (
<Input
className="h-12"
placeholder="Telgram idni yozing"
{...field}
/>
)}
<Button
type="button"
className="h-12 !bg-blue-700 w-12 cursor-pointer"
onClick={() =>
bot === "select" ? setBot("input") : setBot("select")
}
>
<Search />
</Button>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="firstName"

View File

@@ -40,7 +40,6 @@ const DeleteUser = ({
setUserDelete(null);
},
onError: (err: AxiosError) => {
console.log(err);
const errMessage = err.response?.data as { message: string };
const messageText = errMessage.message;
toast.error(messageText || "Xatolik yuz berdi", {

View File

@@ -4,8 +4,8 @@ import { user_api } from "@/features/users/lib/api";
import type { UserListData } from "@/features/users/lib/data";
import DeleteUser from "@/features/users/ui/DeleteUser";
import Filter from "@/features/users/ui/Filter";
import Pagination from "@/features/users/ui/Pagination";
import UserTable from "@/features/users/ui/UserTable";
import Pagination from "@/shared/ui/pagination";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";

View File

@@ -7,5 +7,17 @@ const REGION = "/api/v1/admin/district/";
const REGIONS = "/api/v1/admin/region/";
const DISTRICT = "/api/v1/admin/district/";
const DOCTOR = "/api/v1/admin/doctor/";
const OBJECT = "/api/v1/admin/place/";
const PHARMACIES = "/api/v1/admin/pharmacy/";
export { BASE_URL, DISTRICT, DOCTOR, LOGIN, REGION, REGIONS, USER };
export {
BASE_URL,
DISTRICT,
DOCTOR,
LOGIN,
OBJECT,
PHARMACIES,
REGION,
REGIONS,
USER,
};

View File

@@ -10,8 +10,6 @@ const httpClient = axios.create({
httpClient.interceptors.request.use(
async (config) => {
console.log(`API REQUEST to ${config.url}`, config);
// Language configs
const language = i18n.language;
config.headers["Accept-Language"] = language;

View File

@@ -15,8 +15,8 @@ const Pagination = ({ currentPage, setCurrentPage, totalPages }: Props) => {
<Button
variant="outline"
size="icon"
className="cursor-pointer"
disabled={currentPage === 1}
className="cursor-pointer"
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
>
<ChevronLeft />
@@ -40,9 +40,9 @@ const Pagination = ({ currentPage, setCurrentPage, totalPages }: Props) => {
<Button
variant="outline"
size="icon"
className="cursor-pointer"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
className="cursor-pointer"
>
<ChevronRight />
</Button>