472 lines
15 KiB
TypeScript
472 lines
15 KiB
TypeScript
"use client";
|
||
|
||
import { district_api } from "@/features/district/lib/api";
|
||
import { object_api } from "@/features/object/lib/api";
|
||
import type { CreatePharmacyReq } from "@/features/phamarcy/lib/data";
|
||
import formatPhone from "@/shared/lib/formatPhone";
|
||
import onlyNumber from "@/shared/lib/onlyNumber";
|
||
import { Button } from "@/shared/ui/button";
|
||
import {
|
||
Form,
|
||
FormControl,
|
||
FormField,
|
||
FormItem,
|
||
FormLabel,
|
||
FormMessage,
|
||
} from "@/shared/ui/form";
|
||
import { Input } from "@/shared/ui/input";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "@/shared/ui/select";
|
||
import { DashboardLayout } from "@/widgets/dashboard-layout/ui/DashboardLayout";
|
||
import { zodResolver } from "@hookform/resolvers/zod";
|
||
import {
|
||
Circle,
|
||
Map,
|
||
Placemark,
|
||
Polygon,
|
||
YMaps,
|
||
ZoomControl,
|
||
} from "@pbe/react-yandex-maps";
|
||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||
import { AxiosError } from "axios";
|
||
import { Loader2, LocateFixed } from "lucide-react";
|
||
import { useRef, useState } from "react";
|
||
import { useForm } from "react-hook-form";
|
||
import { useNavigate } from "react-router-dom";
|
||
import { toast } from "sonner";
|
||
import z from "zod";
|
||
import { pharmacy_api } from "../lib/api";
|
||
import { objectForm } from "../lib/form";
|
||
|
||
interface CoordsData {
|
||
lat: number;
|
||
lon: number;
|
||
polygon: [number, number][][];
|
||
}
|
||
|
||
const CreatePharmacy = () => {
|
||
const queryClinent = useQueryClient();
|
||
const router = useNavigate();
|
||
const mapRef = useRef<ymaps.Map | null>(null);
|
||
const form = useForm<z.infer<typeof objectForm>>({
|
||
resolver: zodResolver(objectForm),
|
||
defaultValues: {
|
||
districts: "",
|
||
name: "",
|
||
inn: "",
|
||
phoneDirector: "+998",
|
||
phonePharmacy: "+998",
|
||
},
|
||
});
|
||
|
||
const { mutate, isPending } = useMutation({
|
||
mutationFn: (body: CreatePharmacyReq) => pharmacy_api.create(body),
|
||
onSuccess: () => {
|
||
router("/pharmacy");
|
||
queryClinent.refetchQueries({ queryKey: ["pharmacy_list"] });
|
||
},
|
||
onError: (error: AxiosError) => {
|
||
const data = error.response?.data as { message?: string };
|
||
const errorData = error.response?.data as {
|
||
messages?: {
|
||
token_class: string;
|
||
token_type: string;
|
||
message: string;
|
||
}[];
|
||
};
|
||
const errorName = error.response?.data as {
|
||
data?: {
|
||
name: string[];
|
||
};
|
||
};
|
||
|
||
toast.error(
|
||
errorName.data?.name[0] ||
|
||
data.message ||
|
||
errorData?.messages?.[0].message ||
|
||
"Xatolik yuz berdi",
|
||
);
|
||
},
|
||
});
|
||
|
||
const { data: districts } = useQuery({
|
||
queryKey: ["my_disctrict"],
|
||
queryFn: () => district_api.getDiscrict(),
|
||
select(data) {
|
||
return data.data.data;
|
||
},
|
||
});
|
||
|
||
const district_id = form.watch("districts");
|
||
|
||
const { data: streets } = useQuery({
|
||
queryKey: ["object_list", district_id],
|
||
queryFn: () => object_api.getAll({ district_id: Number(district_id) }),
|
||
select(data) {
|
||
return data.data.data;
|
||
},
|
||
});
|
||
|
||
const [coords, setCoords] = useState({
|
||
latitude: 41.311081,
|
||
longitude: 69.240562,
|
||
});
|
||
|
||
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;
|
||
};
|
||
|
||
const [polygonCoords, setPolygonCoords] = useState<
|
||
[number, number][][] | null
|
||
>(null);
|
||
const [circleCoords, setCircleCoords] = useState<[number, number] | null>(
|
||
null,
|
||
);
|
||
|
||
const handleStreetChange = (streetId: string) => {
|
||
form.setValue("streets", streetId);
|
||
|
||
const selectedStreet = streets?.find((s) => s.id === Number(streetId));
|
||
if (!selectedStreet) return;
|
||
|
||
setCoords({
|
||
latitude: selectedStreet.latitude,
|
||
longitude: selectedStreet.longitude,
|
||
});
|
||
form.setValue("latitude", selectedStreet.latitude);
|
||
form.setValue("longitude", selectedStreet.longitude);
|
||
|
||
setCircleCoords([selectedStreet.latitude, selectedStreet.longitude]);
|
||
|
||
if (mapRef.current) {
|
||
mapRef.current.setCenter(
|
||
[selectedStreet.latitude, selectedStreet.longitude],
|
||
16,
|
||
);
|
||
}
|
||
};
|
||
|
||
const handleShowMyLocation = () => {
|
||
if (!navigator.geolocation) {
|
||
alert("Sizning brauzeringiz geolokatsiyani qo‘llab-quvvatlamaydi");
|
||
return;
|
||
}
|
||
navigator.geolocation.getCurrentPosition(
|
||
(position) => {
|
||
const lat = position.coords.latitude;
|
||
const lon = position.coords.longitude;
|
||
setCoords({ latitude: lat, longitude: lon });
|
||
form.setValue("latitude", lat);
|
||
form.setValue("longitude", lon);
|
||
if (mapRef.current) {
|
||
mapRef.current.setCenter([lat, lon], 20);
|
||
}
|
||
},
|
||
(error) => {
|
||
alert("Joylashuv aniqlanmadi: " + error.message);
|
||
},
|
||
);
|
||
};
|
||
|
||
const handleMapClick = (
|
||
e: ymaps.IEvent<MouseEvent, { coords: [number, number] }>,
|
||
) => {
|
||
const [lat, lon] = e.get("coords");
|
||
setCoords({ latitude: lat, longitude: lon });
|
||
form.setValue("latitude", lat);
|
||
form.setValue("longitude", lon);
|
||
};
|
||
|
||
const onSubmit = (values: z.infer<typeof objectForm>) => {
|
||
mutate({
|
||
district_id: Number(values.districts),
|
||
extra_location: {
|
||
latitude: values.latitude,
|
||
longitude: values.longitude,
|
||
},
|
||
inn: values.inn,
|
||
latitude: values.latitude,
|
||
longitude: values.longitude,
|
||
name: values.name,
|
||
owner_phone: onlyNumber(values.phoneDirector),
|
||
place_id: Number(values.streets),
|
||
responsible_phone: onlyNumber(values.phonePharmacy),
|
||
});
|
||
};
|
||
|
||
return (
|
||
<DashboardLayout>
|
||
<div className="max-w-3xl mx-auto space-y-6">
|
||
<h1 className="text-3xl font-bold">{"Qo'shish"}</h1>
|
||
<Form {...form}>
|
||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
|
||
<FormField
|
||
control={form.control}
|
||
name="name"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Nomi</FormLabel>
|
||
<FormControl>
|
||
<Input {...field} placeholder="Nomi" className="h-12" />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="inn"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>INN</FormLabel>
|
||
<FormControl>
|
||
<Input {...field} placeholder="INN" className="h-12" />
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="phoneDirector"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Dorixona egasining nomeri</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
placeholder="+998 90 123-45-67"
|
||
className="h-12"
|
||
onChange={(e) => {
|
||
const formatted = formatPhone(e.target.value);
|
||
field.onChange(formatted);
|
||
}}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="phonePharmacy"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Mas’ul shaxsning nomeri</FormLabel>
|
||
<FormControl>
|
||
<Input
|
||
{...field}
|
||
placeholder="+998 90 123-45-67"
|
||
className="h-12"
|
||
onChange={(e) => {
|
||
const formatted = formatPhone(e.target.value);
|
||
field.onChange(formatted);
|
||
}}
|
||
/>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="districts"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>Tuman</FormLabel>
|
||
<FormControl>
|
||
<Select
|
||
key={field.value}
|
||
onValueChange={async (id) => {
|
||
field.onChange(id);
|
||
const selectedDistrict = districts?.find(
|
||
(d) => d.id === Number(id),
|
||
);
|
||
if (!selectedDistrict) return;
|
||
|
||
const coordsData = await getCoords(
|
||
selectedDistrict.name,
|
||
);
|
||
if (!coordsData) return;
|
||
|
||
setCoords({
|
||
latitude: coordsData.lat,
|
||
longitude: coordsData.lon,
|
||
});
|
||
setPolygonCoords(coordsData.polygon);
|
||
}}
|
||
value={field.value}
|
||
>
|
||
<SelectTrigger className="w-full h-12">
|
||
<SelectValue placeholder="Tumanlar" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{districts?.map((d) => (
|
||
<SelectItem key={d.id} value={String(d.id)}>
|
||
{d.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="streets"
|
||
render={({ field }) => (
|
||
<FormItem>
|
||
<FormLabel>{"Ko'cha"}</FormLabel>
|
||
<FormControl>
|
||
<Select
|
||
key={field.value}
|
||
value={field.value}
|
||
onValueChange={(value) => {
|
||
field.onChange(value);
|
||
handleStreetChange(value);
|
||
}}
|
||
>
|
||
<SelectTrigger className="w-full h-12">
|
||
<SelectValue placeholder="Ko'chalar" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{streets?.map((s) => (
|
||
<SelectItem key={s.id} value={String(s.id)}>
|
||
{s.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<FormField
|
||
control={form.control}
|
||
name="latitude"
|
||
render={() => (
|
||
<FormItem>
|
||
<FormLabel>Xarita</FormLabel>
|
||
<FormControl>
|
||
<div className="relative h-80 border rounded-lg overflow-hidden">
|
||
<YMaps query={{ lang: "en_RU" }}>
|
||
<Map
|
||
defaultState={{
|
||
center: [coords.latitude, coords.longitude],
|
||
zoom: 12,
|
||
}}
|
||
width="100%"
|
||
height="100%"
|
||
onClick={handleMapClick}
|
||
>
|
||
<ZoomControl
|
||
options={{
|
||
position: { right: "10px", bottom: "70px" },
|
||
}}
|
||
/>
|
||
<Placemark
|
||
geometry={[coords.latitude, coords.longitude]}
|
||
/>
|
||
|
||
{polygonCoords && (
|
||
<Polygon
|
||
geometry={polygonCoords}
|
||
options={{
|
||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||
strokeWidth: 2,
|
||
interactivityModel: "default#transparent",
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{circleCoords && (
|
||
<Circle
|
||
geometry={[circleCoords, 500]}
|
||
options={{
|
||
fillColor: "rgba(255, 100, 0, 0.3)",
|
||
strokeColor: "rgba(255, 100, 0, 0.8)",
|
||
strokeWidth: 2,
|
||
interactivityModel: "default#transparent",
|
||
}}
|
||
/>
|
||
)}
|
||
</Map>
|
||
</YMaps>
|
||
|
||
<Button
|
||
type="button"
|
||
size="sm"
|
||
onClick={handleShowMyLocation}
|
||
className="absolute bottom-3 right-2.5 shadow-md bg-white text-black hover:bg-gray-100"
|
||
>
|
||
<LocateFixed className="w-4 h-4 mr-1" />
|
||
</Button>
|
||
</div>
|
||
</FormControl>
|
||
<FormMessage />
|
||
</FormItem>
|
||
)}
|
||
/>
|
||
|
||
<div className="flex gap-2 justify-end">
|
||
<Button
|
||
variant="outline"
|
||
type="button"
|
||
className="h-12 p-3"
|
||
onClick={() => router("/pharmacy")}
|
||
>
|
||
Bekor qilish
|
||
</Button>
|
||
<Button type="submit" className="h-12 p-5">
|
||
{isPending ? <Loader2 className="animate-spin" /> : "Qo'shish"}
|
||
</Button>
|
||
</div>
|
||
</form>
|
||
</Form>
|
||
</div>
|
||
</DashboardLayout>
|
||
);
|
||
};
|
||
|
||
export default CreatePharmacy;
|