apilar ulandi
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { LOGIN } from "@/shared/config/api/URLs";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
interface LoginRes {
|
||||
@@ -16,7 +16,7 @@ export const auth_pai = {
|
||||
username: string;
|
||||
password: string;
|
||||
}): Promise<AxiosResponse<LoginRes>> {
|
||||
const res = await httpClient.post(LOGIN, body);
|
||||
const res = await httpClient.post(API_URLS.LOGIN, body);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DistrictListRes } from "@/features/districts/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { DISTRICT } from "@/shared/config/api/URLs";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const discrit_api = {
|
||||
@@ -10,7 +10,7 @@ export const discrit_api = {
|
||||
name?: string;
|
||||
user?: number;
|
||||
}): Promise<AxiosResponse<DistrictListRes>> {
|
||||
const res = await httpClient.get(`${DISTRICT}list/`, { params });
|
||||
const res = await httpClient.get(`${API_URLS.DISTRICT}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
@@ -18,7 +18,7 @@ export const discrit_api = {
|
||||
name: string;
|
||||
user_id: number;
|
||||
}): Promise<AxiosResponse<DistrictListRes>> {
|
||||
const res = await httpClient.post(`${DISTRICT}create/`, body);
|
||||
const res = await httpClient.post(`${API_URLS.DISTRICT}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
@@ -32,12 +32,15 @@ export const discrit_api = {
|
||||
user: number;
|
||||
};
|
||||
}): Promise<AxiosResponse<DistrictListRes>> {
|
||||
const res = await httpClient.patch(`${DISTRICT}${id}/update/`, body);
|
||||
const res = await httpClient.patch(
|
||||
`${API_URLS.DISTRICT}${id}/update/`,
|
||||
body,
|
||||
);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number): Promise<AxiosResponse<DistrictListRes>> {
|
||||
const res = await httpClient.delete(`${DISTRICT}${id}/delete/`);
|
||||
const res = await httpClient.delete(`${API_URLS.DISTRICT}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
UpdateDoctorReq,
|
||||
} from "@/features/doctors/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { DOCTOR } from "@/shared/config/api/URLs";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const doctor_api = {
|
||||
@@ -18,22 +18,22 @@ export const doctor_api = {
|
||||
sphere?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<DoctorListRes>> {
|
||||
const res = await httpClient.get(`${DOCTOR}list/`, { params });
|
||||
const res = await httpClient.get(`${API_URLS.DOCTOR}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: CreateDoctorReq) {
|
||||
const res = await httpClient.post(`${DOCTOR}create/`, body);
|
||||
const res = await httpClient.post(`${API_URLS.DOCTOR}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: UpdateDoctorReq }) {
|
||||
const res = await httpClient.patch(`${DOCTOR}${id}/update/`, body);
|
||||
const res = await httpClient.patch(`${API_URLS.DOCTOR}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${DOCTOR}${id}/delete/`);
|
||||
const res = await httpClient.delete(`${API_URLS.DOCTOR}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
40
src/features/location/lib/api.ts
Normal file
40
src/features/location/lib/api.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { LocationListRes } from "@/features/location/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const location_api = {
|
||||
async list(params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
date?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<LocationListRes>> {
|
||||
const res = await httpClient.get(`${API_URLS.LOCATION}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${API_URLS.LOCATION}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
|
||||
async list_user_location(params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
date?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<LocationListRes>> {
|
||||
const res = await httpClient.get(`${API_URLS.USER_LOCATION}list/`, {
|
||||
params,
|
||||
});
|
||||
return res;
|
||||
},
|
||||
|
||||
async list_user_location_delete(id: number) {
|
||||
const res = await httpClient.delete(
|
||||
`${API_URLS.USER_LOCATION}${id}/delete/`,
|
||||
);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -75,3 +75,51 @@ export const LocationFakeData: LocationListType[] = [
|
||||
createdAt: new Date("2025-02-01T10:15:00"),
|
||||
},
|
||||
];
|
||||
|
||||
export interface LocationListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: LocationListDataRes[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface LocationListDataRes {
|
||||
id: number;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
created_at: string;
|
||||
district: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
place: {
|
||||
id: number;
|
||||
name: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
};
|
||||
doctor: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
};
|
||||
pharmacy: {
|
||||
id: number;
|
||||
name: string;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
};
|
||||
user: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
114
src/features/location/ui/DeleteLocation.tsx
Normal file
114
src/features/location/ui/DeleteLocation.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import { location_api } from "@/features/location/lib/api";
|
||||
import type { LocationListDataRes } from "@/features/location/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, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setLocationDelete: Dispatch<SetStateAction<LocationListDataRes | null>>;
|
||||
locationDelete: LocationListDataRes | null;
|
||||
viewLocation: "user_send" | "user_send_object";
|
||||
}
|
||||
const DeleteLocation = ({
|
||||
opneDelete,
|
||||
viewLocation,
|
||||
locationDelete,
|
||||
setOpenDelete,
|
||||
setLocationDelete,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteUser, isPending } = useMutation({
|
||||
mutationFn: (id: number) => location_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["location_list"] });
|
||||
toast.success(`Jo'natilgan lokatsiya o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setLocationDelete(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",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteUserLocation, isPending: deleteUserLocationPen } =
|
||||
useMutation({
|
||||
mutationFn: (id: number) => location_api.list_user_location_delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["user_location_list"] });
|
||||
toast.success(`Jo'natilgan lokatsiya o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setLocationDelete(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>Dorini o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham jo'natilgan lokatsiyani 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={() =>
|
||||
locationDelete && viewLocation === "user_send_object"
|
||||
? deleteUser(locationDelete.id)
|
||||
: locationDelete &&
|
||||
viewLocation === "user_send" &&
|
||||
deleteUserLocation(locationDelete.id)
|
||||
}
|
||||
>
|
||||
{isPending || deleteUserLocationPen ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteLocation;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LocationListType } from "@/features/location/lib/data";
|
||||
import type { LocationListDataRes } from "@/features/location/lib/data";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -7,26 +7,100 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { Circle, Map, Placemark, Polygon, YMaps } from "@pbe/react-yandex-maps";
|
||||
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
detail: boolean;
|
||||
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||
object: LocationListType | null;
|
||||
object: LocationListDataRes | null;
|
||||
}
|
||||
|
||||
interface CoordsData {
|
||||
lat: number;
|
||||
lon: number;
|
||||
polygon: [number, number][][];
|
||||
}
|
||||
|
||||
const LocationDetailDialog = ({ detail, object, setDetail }: Props) => {
|
||||
const [circle, setCircle] = useState<string[] | undefined>([""]);
|
||||
const [coords, setCoords] = useState<[number, number]>([
|
||||
41.311081, 69.240562,
|
||||
]);
|
||||
|
||||
const [polygonCoords, setPolygonCoords] = useState<[number, number][][]>([]);
|
||||
|
||||
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 && object.object) {
|
||||
setCircle([object.object.lat, object.object.long]);
|
||||
} else if (object && object.pharmcies) {
|
||||
setCircle([object.pharmcies.lat, object.pharmcies.long]);
|
||||
if (!object) return;
|
||||
|
||||
const load = async () => {
|
||||
if (object.district) {
|
||||
const district = await getCoords(object.district.name);
|
||||
|
||||
if (district) {
|
||||
setPolygonCoords(district.polygon);
|
||||
}
|
||||
} else {
|
||||
setPolygonCoords([]);
|
||||
}
|
||||
|
||||
setCoords([Number(object.latitude), Number(object.longitude)]);
|
||||
};
|
||||
|
||||
load();
|
||||
}, [object]);
|
||||
|
||||
useEffect(() => {
|
||||
if (object && object.place) {
|
||||
setCircle([
|
||||
object.place.latitude.toString(),
|
||||
object.place.longitude.toString(),
|
||||
]);
|
||||
} else if (object && object.pharmacy) {
|
||||
setCircle([
|
||||
object.pharmacy.latitude.toString(),
|
||||
object.pharmacy.longitude.toString(),
|
||||
]);
|
||||
} else if (object && object.doctor) {
|
||||
setCircle([object.doctor.lat, object.doctor.long]);
|
||||
setCircle([
|
||||
object.doctor.latitude.toString(),
|
||||
object.doctor.longitude.toString(),
|
||||
]);
|
||||
} else {
|
||||
setCircle(undefined);
|
||||
}
|
||||
@@ -49,13 +123,13 @@ const LocationDetailDialog = ({ detail, object, setDetail }: Props) => {
|
||||
Jo'natgan foydalanvchi:
|
||||
</p>
|
||||
<p className="text-black">
|
||||
{object.user.firstName} {object.user.lastName}
|
||||
{object.user.first_name} {object.user.last_name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<p className="font-semibold text-gray-900">Jo'natgan vaqti:</p>
|
||||
<p className="text-black">
|
||||
{formatDate.format(object.createdAt, "DD-MM-YYYY")}
|
||||
{formatDate.format(object.created_at, "DD-MM-YYYY")}
|
||||
</p>
|
||||
</div>
|
||||
{object.district && (
|
||||
@@ -69,15 +143,16 @@ const LocationDetailDialog = ({ detail, object, setDetail }: Props) => {
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(object.lat), Number(object.long)],
|
||||
zoom: 16,
|
||||
center: coords,
|
||||
zoom: 12,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
>
|
||||
<Placemark
|
||||
geometry={[Number(object.lat), Number(object.long)]}
|
||||
/>
|
||||
{/* Marking user location */}
|
||||
<Placemark geometry={coords} />
|
||||
|
||||
{/* Circle around user */}
|
||||
{circle && (
|
||||
<Circle
|
||||
geometry={[[Number(circle[0]), Number(circle[1])], 100]}
|
||||
@@ -88,6 +163,18 @@ const LocationDetailDialog = ({ detail, object, setDetail }: Props) => {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* District polygon */}
|
||||
{polygonCoords.length > 0 && (
|
||||
<Polygon
|
||||
geometry={polygonCoords}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.15)",
|
||||
strokeColor: "rgba(0, 150, 255, 1)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,13 @@ 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 {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
@@ -12,6 +19,8 @@ interface Props {
|
||||
setDateFilter: Dispatch<SetStateAction<Date | undefined>>;
|
||||
searchUser: string;
|
||||
setSearchUser: Dispatch<SetStateAction<string>>;
|
||||
viewLocation: "user_send" | "user_send_object";
|
||||
setViewLocation: Dispatch<SetStateAction<"user_send" | "user_send_object">>;
|
||||
}
|
||||
|
||||
const LocationFilter = ({
|
||||
@@ -21,9 +30,28 @@ const LocationFilter = ({
|
||||
setDateFilter,
|
||||
searchUser,
|
||||
setSearchUser,
|
||||
viewLocation,
|
||||
setViewLocation,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Select
|
||||
value={viewLocation}
|
||||
onValueChange={(v) =>
|
||||
setViewLocation(v as "user_send" | "user_send_object")
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[240px] !h-12">
|
||||
<SelectValue placeholder="Tanlang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="user_send_object">
|
||||
Obyektdan jo'natilgan
|
||||
</SelectItem>
|
||||
<SelectItem value="user_send">Turgan joyidan jo'natilgan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
|
||||
@@ -1,44 +1,66 @@
|
||||
import {
|
||||
LocationFakeData,
|
||||
type LocationListType,
|
||||
} from "@/features/location/lib/data";
|
||||
import { location_api } from "@/features/location/lib/api";
|
||||
import { type LocationListDataRes } from "@/features/location/lib/data";
|
||||
import DeleteLocation from "@/features/location/ui/DeleteLocation";
|
||||
import LocationDetailDialog from "@/features/location/ui/LocationDetailDialog";
|
||||
import LocationFilter from "@/features/location/ui/LocationFilter";
|
||||
import LocationTable from "@/features/location/ui/LocationTable";
|
||||
import UserLocationTable from "@/features/location/ui/UserLocationTable";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const LocationList = () => {
|
||||
const [data, setData] = useState<LocationListType[]>(LocationFakeData);
|
||||
const [detail, setDetail] = useState<LocationListType | null>(null);
|
||||
const [detail, setDetail] = useState<LocationListDataRes | null>(null);
|
||||
const [detailDialog, setDetailDialog] = useState<boolean>(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
|
||||
// Filter state
|
||||
const limit = 20;
|
||||
const [dateFilter, setDateFilter] = useState<Date | undefined>(undefined);
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
const [viewLocation, setViewLocation] = useState<
|
||||
"user_send" | "user_send_object"
|
||||
>("user_send_object");
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setData((prev) => prev.filter((e) => e.id !== id));
|
||||
const { data: location } = useQuery({
|
||||
queryKey: ["location_list", currentPage, searchUser, dateFilter],
|
||||
queryFn: () =>
|
||||
location_api.list({
|
||||
limit,
|
||||
offset: (currentPage - 1) * limit,
|
||||
user: searchUser,
|
||||
date: dateFilter && formatDate.format(dateFilter, "YYYY-MM-DD"),
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { data: user_location } = useQuery({
|
||||
queryKey: ["user_location_list", currentPage, searchUser, dateFilter],
|
||||
queryFn: () =>
|
||||
location_api.list_user_location({
|
||||
limit,
|
||||
offset: (currentPage - 1) * limit,
|
||||
user: searchUser,
|
||||
date: dateFilter && formatDate.format(dateFilter, "YYYY-MM-DD"),
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const totalPages = location ? Math.ceil(location.count / limit) : 1;
|
||||
|
||||
const [openDelete, setOpenDelete] = useState<boolean>(false);
|
||||
const [locationDelete, setLocationDelete] =
|
||||
useState<LocationListDataRes | null>(null);
|
||||
|
||||
const handleDelete = (id: LocationListDataRes) => {
|
||||
setOpenDelete(true);
|
||||
setLocationDelete(id);
|
||||
};
|
||||
|
||||
// Filtered data
|
||||
const filtered = useMemo(() => {
|
||||
return data.filter((item) => {
|
||||
const dateMatch = dateFilter
|
||||
? item.createdAt.toDateString() === dateFilter.toDateString()
|
||||
: true;
|
||||
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return dateMatch && userMatch;
|
||||
});
|
||||
}, [data, dateFilter, 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">
|
||||
@@ -51,6 +73,8 @@ const LocationList = () => {
|
||||
setDateFilter={setDateFilter}
|
||||
setOpen={setOpen}
|
||||
setSearchUser={setSearchUser}
|
||||
setViewLocation={setViewLocation}
|
||||
viewLocation={viewLocation}
|
||||
/>
|
||||
|
||||
<LocationDetailDialog
|
||||
@@ -59,18 +83,46 @@ const LocationList = () => {
|
||||
object={detail}
|
||||
/>
|
||||
</div>
|
||||
{viewLocation === "user_send_object" && (
|
||||
<>
|
||||
<LocationTable
|
||||
filtered={location ? location.results : []}
|
||||
handleDelete={handleDelete}
|
||||
setDetail={setDetail}
|
||||
setDetailDialog={setDetailDialog}
|
||||
/>
|
||||
|
||||
<LocationTable
|
||||
filtered={filtered}
|
||||
handleDelete={handleDelete}
|
||||
setDetail={setDetail}
|
||||
setDetailDialog={setDetailDialog}
|
||||
/>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
{viewLocation === "user_send" && (
|
||||
<>
|
||||
<UserLocationTable
|
||||
filtered={user_location ? user_location.results : []}
|
||||
handleDelete={handleDelete}
|
||||
setDetail={setDetail}
|
||||
setDetailDialog={setDetailDialog}
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<DeleteLocation
|
||||
locationDelete={locationDelete}
|
||||
opneDelete={openDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
setLocationDelete={setLocationDelete}
|
||||
viewLocation={viewLocation}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { LocationListType } from "@/features/location/lib/data";
|
||||
import type { LocationListDataRes } from "@/features/location/lib/data";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -13,10 +13,10 @@ import { Eye, Trash2 } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
filtered: LocationListType[];
|
||||
setDetail: Dispatch<SetStateAction<LocationListType | null>>;
|
||||
filtered: LocationListDataRes[] | [];
|
||||
setDetail: Dispatch<SetStateAction<LocationListDataRes | null>>;
|
||||
setDetailDialog: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (id: number) => void;
|
||||
handleDelete: (id: LocationListDataRes) => void;
|
||||
}
|
||||
|
||||
const LocationTable = ({
|
||||
@@ -42,20 +42,20 @@ const LocationTable = ({
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
{item.user.first_name} {item.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(item.createdAt, "DD-MM-YYYY")}
|
||||
{formatDate.format(item.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{item.district
|
||||
? "Tuman"
|
||||
: item.object
|
||||
: item.place
|
||||
? "Obyekt"
|
||||
: item.doctor
|
||||
? "Shifokor"
|
||||
: item.pharmcies
|
||||
: item.pharmacy
|
||||
? "Dorixona"
|
||||
: "Turgan joyidan"}
|
||||
</TableCell>
|
||||
@@ -76,7 +76,7 @@ const LocationTable = ({
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
|
||||
92
src/features/location/ui/UserLocationTable.tsx
Normal file
92
src/features/location/ui/UserLocationTable.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { LocationListDataRes } 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: LocationListDataRes[] | [];
|
||||
setDetail: Dispatch<SetStateAction<LocationListDataRes | null>>;
|
||||
setDetailDialog: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (id: LocationListDataRes) => void;
|
||||
}
|
||||
|
||||
const UserLocationTable = ({
|
||||
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.first_name} {item.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(item.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{item.district
|
||||
? "Tuman"
|
||||
: item.place
|
||||
? "Obyekt"
|
||||
: item.doctor
|
||||
? "Shifokor"
|
||||
: item.pharmacy
|
||||
? "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)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserLocationTable;
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
ObjectUpdate,
|
||||
} from "@/features/objects/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { OBJECT } from "@/shared/config/api/URLs";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const object_api = {
|
||||
@@ -15,22 +15,22 @@ export const object_api = {
|
||||
district?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<ObjectListRes>> {
|
||||
const res = await httpClient.get(`${OBJECT}list/`, { params });
|
||||
const res = await httpClient.get(`${API_URLS.OBJECT}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: ObjectCreate) {
|
||||
const res = await httpClient.post(`${OBJECT}create/`, body);
|
||||
const res = await httpClient.post(`${API_URLS.OBJECT}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: ObjectUpdate }) {
|
||||
const res = await httpClient.patch(`${OBJECT}${id}/update/`, body);
|
||||
const res = await httpClient.patch(`${API_URLS.OBJECT}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${OBJECT}${id}/delete/`);
|
||||
const res = await httpClient.delete(`${API_URLS.OBJECT}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
33
src/features/pharm/lib/api.ts
Normal file
33
src/features/pharm/lib/api.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { FactoryCreate, FactoryListRes } from "@/features/pharm/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const factory_api = {
|
||||
async list(params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
name?: string;
|
||||
}): Promise<AxiosResponse<FactoryListRes>> {
|
||||
const res = await httpClient.get(`${API_URLS.FACTORY}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: FactoryCreate) {
|
||||
const res = await httpClient.post(`${API_URLS.FACTORY}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ id, body }: { id: number; body: FactoryCreate }) {
|
||||
const res = await httpClient.patch(
|
||||
`${API_URLS.FACTORY}${id}/update/`,
|
||||
body,
|
||||
);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${API_URLS.FACTORY}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -9,3 +9,25 @@ export const pharmData: PharmType[] = [
|
||||
name: "Meridyn",
|
||||
},
|
||||
];
|
||||
|
||||
export interface FactoryListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: FactoryListDataRes[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface FactoryListDataRes {
|
||||
id: number;
|
||||
name: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface FactoryCreate {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PharmType } from "@/features/pharm/lib/data";
|
||||
import { factory_api } from "@/features/pharm/lib/api";
|
||||
import type { FactoryCreate, PharmType } from "@/features/pharm/lib/data";
|
||||
import { pharmForm } from "@/features/pharm/lib/form";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -11,19 +12,21 @@ import {
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: PharmType | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setPlans: Dispatch<SetStateAction<PharmType[]>>;
|
||||
}
|
||||
|
||||
const AddedPharm = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
const [load, setLoad] = useState(false);
|
||||
const AddedPharm = ({ initialValues, setDialogOpen }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const form = useForm<z.infer<typeof pharmForm>>({
|
||||
resolver: zodResolver(pharmForm),
|
||||
defaultValues: {
|
||||
@@ -31,35 +34,51 @@ const AddedPharm = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: FactoryCreate) => factory_api.create(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["factory_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: update, isPending: updatePending } = useMutation({
|
||||
mutationFn: ({ id, body }: { id: number; body: FactoryCreate }) =>
|
||||
factory_api.update({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["factory_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(data: z.infer<typeof pharmForm>) {
|
||||
setLoad(true);
|
||||
if (initialValues) {
|
||||
setTimeout(() => {
|
||||
setPlans((prev) =>
|
||||
prev.map((plan) =>
|
||||
plan.id === initialValues.id
|
||||
? {
|
||||
...plan,
|
||||
...data,
|
||||
}
|
||||
: plan,
|
||||
),
|
||||
);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
if (!initialValues) {
|
||||
mutate({
|
||||
name: data.name,
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setPlans((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length ? prev[prev.length - 1].id + 1 : 1,
|
||||
name: data.name,
|
||||
},
|
||||
]);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
update({
|
||||
body: {
|
||||
name: data.name,
|
||||
},
|
||||
id: initialValues.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +106,9 @@ const AddedPharm = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
|
||||
<Button
|
||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||
disabled={load}
|
||||
disabled={isPending || updatePending}
|
||||
>
|
||||
{load ? (
|
||||
{isPending || updatePending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
|
||||
89
src/features/pharm/ui/DeletePharm.tsx
Normal file
89
src/features/pharm/ui/DeletePharm.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { factory_api } from "@/features/pharm/lib/api";
|
||||
import type { FactoryListDataRes } from "@/features/pharm/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, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setPillDelete: Dispatch<SetStateAction<FactoryListDataRes | null>>;
|
||||
pillDelete: FactoryListDataRes | null;
|
||||
}
|
||||
|
||||
const DeletePharm = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
pillDelete,
|
||||
setPillDelete,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteUser, isPending } = useMutation({
|
||||
mutationFn: (id: number) => factory_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["factory_list"] });
|
||||
toast.success(`Farmasevtika o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setPillDelete(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>Farmasevtikani o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham {pillDelete?.name} nomli farmasevtikani
|
||||
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={() => pillDelete && deleteUser(pillDelete.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeletePharm;
|
||||
@@ -1,5 +1,10 @@
|
||||
import { pharmData, type PharmType } from "@/features/pharm/lib/data";
|
||||
import { factory_api } from "@/features/pharm/lib/api";
|
||||
import {
|
||||
type FactoryListDataRes,
|
||||
type PharmType,
|
||||
} from "@/features/pharm/lib/data";
|
||||
import AddedPharm from "@/features/pharm/ui/AddedPharm";
|
||||
import DeletePharm from "@/features/pharm/ui/DeletePharm";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -9,6 +14,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -17,34 +23,39 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Edit, Loader2, Plus, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const PharmList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<PharmType[]>(pharmData);
|
||||
const [nameFilter, setNameFilter] = useState<string>("");
|
||||
const limit = 20;
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["factory_list", currentPage, nameFilter],
|
||||
queryFn: () =>
|
||||
factory_api.list({
|
||||
limit,
|
||||
offset: (currentPage - 1) * limit,
|
||||
name: nameFilter,
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
const totalPages = data ? Math.ceil(data?.count / limit) : 1;
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<PharmType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const [nameFilter, setNameFilter] = useState<string>("");
|
||||
const [openDelete, setOpenDelete] = useState<boolean>(false);
|
||||
const [pillDelete, setPillDelete] = useState<FactoryListDataRes | null>(null);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setPlans(plans.filter((p) => p.id !== id));
|
||||
const handleDelete = (id: FactoryListDataRes) => {
|
||||
setOpenDelete(true);
|
||||
setPillDelete(id);
|
||||
};
|
||||
|
||||
const filteredPlans = useMemo(() => {
|
||||
return plans.filter((item) => {
|
||||
const statusMatch = item.name
|
||||
.toLowerCase()
|
||||
.includes(nameFilter.toLowerCase());
|
||||
|
||||
return statusMatch;
|
||||
});
|
||||
}, [plans, nameFilter]);
|
||||
|
||||
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">
|
||||
@@ -80,7 +91,6 @@ const PharmList = () => {
|
||||
<AddedPharm
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -88,84 +98,83 @@ const PharmList = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Nomi</TableHead>
|
||||
<TableHead className="text-end">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPlans.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.name}</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>
|
||||
{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>
|
||||
)}
|
||||
{!isLoading && !isError && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Nomi</TableHead>
|
||||
<TableHead className="text-end">Amallar</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data && data.results.length > 0 ? (
|
||||
data?.results.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.name}</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)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={3} className="text-center py-4 text-lg">
|
||||
Farmasevtika topilmadi.
|
||||
</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}
|
||||
/>
|
||||
|
||||
<DeletePharm
|
||||
opneDelete={openDelete}
|
||||
pillDelete={pillDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
setPillDelete={setPillDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
UpdatePharmaciesReq,
|
||||
} from "@/features/pharmacies/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { PHARMACIES } from "@/shared/config/api/URLs";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const pharmacies_api = {
|
||||
@@ -16,22 +16,25 @@ export const pharmacies_api = {
|
||||
district?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<PharmaciesListRes>> {
|
||||
const res = await httpClient.get(`${PHARMACIES}list/`, { params });
|
||||
const res = await httpClient.get(`${API_URLS.PHARMACIES}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: CreatePharmaciesReq) {
|
||||
const res = await httpClient.post(`${PHARMACIES}create/`, body);
|
||||
const res = await httpClient.post(`${API_URLS.PHARMACIES}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: UpdatePharmaciesReq }) {
|
||||
const res = await httpClient.patch(`${PHARMACIES}${id}/update/`, body);
|
||||
const res = await httpClient.patch(
|
||||
`${API_URLS.PHARMACIES}${id}/update/`,
|
||||
body,
|
||||
);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${PHARMACIES}${id}/delete/`);
|
||||
const res = await httpClient.delete(`${API_URLS.PHARMACIES}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
30
src/features/pill/lib/api.ts
Normal file
30
src/features/pill/lib/api.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { PillCreateReq, PillListRes } from "@/features/pill/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const pill_api = {
|
||||
async list(params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
name?: string;
|
||||
}): Promise<AxiosResponse<PillListRes>> {
|
||||
const res = await httpClient.get(`${API_URLS.PILL}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async added(body: PillCreateReq) {
|
||||
const res = httpClient.post(`${API_URLS.PILL}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: PillCreateReq }) {
|
||||
const res = httpClient.patch(`${API_URLS.PILL}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = httpClient.delete(`${API_URLS.PILL}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -16,3 +16,27 @@ export const FakePills: PillType[] = [
|
||||
{ id: 9, name: "Amoxicillin 500mg", price: "28000" },
|
||||
{ id: 10, name: "Immuno Plus", price: "30000" },
|
||||
];
|
||||
|
||||
export interface PillListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: PillListData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PillListData {
|
||||
id: number;
|
||||
name: string;
|
||||
price: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PillCreateReq {
|
||||
name: string;
|
||||
price: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { PillType } from "@/features/pill/lib/data";
|
||||
import { pill_api } from "@/features/pill/lib/api";
|
||||
import type { PillCreateReq, PillType } from "@/features/pill/lib/data";
|
||||
import { createPillFormData } from "@/features/pill/lib/form";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
@@ -12,20 +13,22 @@ import {
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { 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: PillType | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setPlans: Dispatch<SetStateAction<PillType[]>>;
|
||||
}
|
||||
|
||||
const AddedPill = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
const [load, setLoad] = useState(false);
|
||||
const AddedPill = ({ initialValues, setDialogOpen }: Props) => {
|
||||
const [displayPrice, setDisplayPrice] = useState<string>("");
|
||||
const queryClient = useQueryClient();
|
||||
const form = useForm<z.infer<typeof createPillFormData>>({
|
||||
resolver: zodResolver(createPillFormData),
|
||||
defaultValues: {
|
||||
@@ -34,6 +37,44 @@ const AddedPill = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: added, isPending: addedPending } = useMutation({
|
||||
mutationFn: (body: PillCreateReq) => {
|
||||
return pill_api.added(body);
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Dori qo'shildi");
|
||||
setDialogOpen(false);
|
||||
queryClient.resetQueries({ queryKey: ["pill_list"] });
|
||||
},
|
||||
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: PillCreateReq }) => {
|
||||
return pill_api.update({ body, id });
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Dori yangilandi");
|
||||
queryClient.resetQueries({ queryKey: ["pill_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",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
setDisplayPrice(formatPrice(initialValues.price));
|
||||
@@ -41,35 +82,19 @@ const AddedPill = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
}, [initialValues]);
|
||||
|
||||
function onSubmit(data: z.infer<typeof createPillFormData>) {
|
||||
setLoad(true);
|
||||
if (initialValues) {
|
||||
setTimeout(() => {
|
||||
setPlans((prev) =>
|
||||
prev.map((plan) =>
|
||||
plan.id === initialValues.id
|
||||
? {
|
||||
...plan,
|
||||
...data,
|
||||
}
|
||||
: plan,
|
||||
),
|
||||
);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
edit({
|
||||
id: initialValues.id,
|
||||
body: {
|
||||
name: data.name,
|
||||
price: data.price,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setPlans((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length ? prev[prev.length - 1].id + 1 : 1,
|
||||
name: data.name,
|
||||
price: data.price,
|
||||
},
|
||||
]);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
added({
|
||||
name: data.name,
|
||||
price: data.price,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,9 +150,9 @@ const AddedPill = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
|
||||
<Button
|
||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||
disabled={load}
|
||||
disabled={addedPending || editPending}
|
||||
>
|
||||
{load ? (
|
||||
{addedPending || editPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
|
||||
88
src/features/pill/ui/DeletePill.tsx
Normal file
88
src/features/pill/ui/DeletePill.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { pill_api } from "@/features/pill/lib/api";
|
||||
import type { PillListData } from "@/features/pill/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, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setPillDelete: Dispatch<SetStateAction<PillListData | null>>;
|
||||
pillDelete: PillListData | null;
|
||||
}
|
||||
|
||||
const DeletePill = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
pillDelete,
|
||||
setPillDelete,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteUser, isPending } = useMutation({
|
||||
mutationFn: (id: number) => pill_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["pill_list"] });
|
||||
toast.success(`Foydalanuvchi o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setPillDelete(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>Dorini o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham {pillDelete?.name} nomli dorini 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={() => pillDelete && deleteUser(pillDelete.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeletePill;
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FakePills, type PillType } from "@/features/pill/lib/data";
|
||||
import { pill_api } from "@/features/pill/lib/api";
|
||||
import { type PillListData, type PillType } from "@/features/pill/lib/data";
|
||||
import AddedPill from "@/features/pill/ui/AddedPill";
|
||||
import DeletePill from "@/features/pill/ui/DeletePill";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -18,34 +21,41 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Edit, Plus, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const PillList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<PillType[]>(FakePills);
|
||||
const limit = 20;
|
||||
const [nameFilter, setNameFilter] = useState<string>("");
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ["pill_list", nameFilter, currentPage],
|
||||
queryFn: () =>
|
||||
pill_api.list({
|
||||
limit,
|
||||
offset: (currentPage - 1) * limit,
|
||||
name: nameFilter,
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const totalPages = data ? Math.ceil(data.count / limit) : 1;
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<PillType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const [nameFilter, setNameFilter] = useState<string>("");
|
||||
const [openDelete, setOpenDelete] = useState<boolean>(false);
|
||||
const [pillDelete, setPillDelete] = useState<PillListData | null>(null);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setPlans(plans.filter((p) => p.id !== id));
|
||||
const handleDelete = (id: PillListData) => {
|
||||
setOpenDelete(true);
|
||||
setPillDelete(id);
|
||||
};
|
||||
|
||||
const filteredPlans = useMemo(() => {
|
||||
return plans.filter((item) => {
|
||||
const statusMatch = item.name
|
||||
.toLowerCase()
|
||||
.includes(nameFilter.toLowerCase());
|
||||
|
||||
return statusMatch;
|
||||
});
|
||||
}, [plans, nameFilter]);
|
||||
|
||||
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">
|
||||
@@ -79,7 +89,6 @@ const PillList = () => {
|
||||
<AddedPill
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -97,7 +106,7 @@ const PillList = () => {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPlans.map((plan) => (
|
||||
{data?.results.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.name}</TableCell>
|
||||
@@ -118,7 +127,7 @@ const PillList = () => {
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -129,44 +138,18 @@ const PillList = () => {
|
||||
</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}
|
||||
/>
|
||||
|
||||
<DeletePill
|
||||
opneDelete={openDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
pillDelete={pillDelete}
|
||||
setPillDelete={setPillDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
36
src/features/plans/lib/api.ts
Normal file
36
src/features/plans/lib/api.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
PlanCreateReq,
|
||||
PlanListRes,
|
||||
PlanUpdateReq,
|
||||
} from "@/features/plans/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const plans_api = {
|
||||
async list(params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: boolean;
|
||||
date?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<PlanListRes>> {
|
||||
const res = await httpClient.get(`${API_URLS.PLANS}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: PlanCreateReq) {
|
||||
const res = await httpClient.post(`${API_URLS.PLANS}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: PlanUpdateReq }) {
|
||||
const res = await httpClient.patch(`${API_URLS.PLANS}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${API_URLS.PLANS}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -8,3 +8,42 @@ export interface Plan {
|
||||
status: "Bajarildi" | "Bajarilmagan";
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface PlanListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: PlanListData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlanListData {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
user: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
is_done: true;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PlanCreateReq {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
user_id: number;
|
||||
}
|
||||
|
||||
export interface PlanUpdateReq {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
@@ -4,4 +4,5 @@ export const createPlanFormData = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
description: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
date: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
|
||||
@@ -1,7 +1,23 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import { plans_api } from "@/features/plans/lib/api";
|
||||
import type {
|
||||
PlanCreateReq,
|
||||
PlanListData,
|
||||
PlanUpdateReq,
|
||||
} from "@/features/plans/lib/data";
|
||||
import { createPlanFormData } from "@/features/plans/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/shared/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -11,71 +27,110 @@ 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 { Loader2 } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Check, ChevronDownIcon, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues?: Plan | null;
|
||||
initialValues?: PlanListData | null;
|
||||
setDialogOpen: (open: boolean) => void;
|
||||
setPlans: React.Dispatch<React.SetStateAction<Plan[]>>;
|
||||
}
|
||||
|
||||
const AddedPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
const [load, setLoad] = useState(false);
|
||||
const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
const form = useForm<z.infer<typeof createPlanFormData>>({
|
||||
resolver: zodResolver(createPlanFormData),
|
||||
defaultValues: {
|
||||
name: initialValues?.name || "",
|
||||
name: initialValues?.title || "",
|
||||
description: initialValues?.description || "",
|
||||
user: initialValues ? String(initialValues.user.id) : "",
|
||||
date: initialValues ? initialValues?.date : "",
|
||||
},
|
||||
});
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||
const queryClient = useQueryClient();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { data: user, isLoading: isUserLoading } = useQuery({
|
||||
queryKey: ["user_list", searchUser],
|
||||
queryFn: () => {
|
||||
const params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
is_active?: boolean | string;
|
||||
region_id?: number;
|
||||
} = {
|
||||
limit: 8,
|
||||
search: searchUser,
|
||||
};
|
||||
|
||||
return user_api.list(params);
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: PlanCreateReq) => plans_api.create(body),
|
||||
onSuccess: () => {
|
||||
setDialogOpen(false);
|
||||
toast.success("Reja qo'shildi");
|
||||
queryClient.refetchQueries({ queryKey: ["plan_list"] });
|
||||
},
|
||||
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: PlanUpdateReq; id: number }) =>
|
||||
plans_api.update({ body, id }),
|
||||
onSuccess: () => {
|
||||
setDialogOpen(false);
|
||||
toast.success("Reja tahrirlandi");
|
||||
queryClient.refetchQueries({ queryKey: ["plan_list"] });
|
||||
},
|
||||
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(data: z.infer<typeof createPlanFormData>) {
|
||||
setLoad(true);
|
||||
if (initialValues) {
|
||||
setTimeout(() => {
|
||||
setPlans((prev) =>
|
||||
prev.map((plan) =>
|
||||
plan.id === initialValues.id
|
||||
? {
|
||||
...plan,
|
||||
...data,
|
||||
user: FakeUserList.find((u) => u.id === Number(data.user))!, // user obyekt
|
||||
}
|
||||
: plan,
|
||||
),
|
||||
);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
edit({
|
||||
id: initialValues.id,
|
||||
body: {
|
||||
date: formatDate.format(data.date, "YYYY-MM-DD"),
|
||||
description: data.description,
|
||||
title: data.name,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setPlans((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length ? prev[prev.length - 1].id + 1 : 1,
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
user: FakeUserList.find((u) => u.id === Number(data.user))!, // user obyekt
|
||||
status: "Bajarilmagan",
|
||||
createdAt: new Date(),
|
||||
},
|
||||
]);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
mutate({
|
||||
date: formatDate.format(data.date, "YYYY-MM-DD"),
|
||||
description: data.description,
|
||||
title: data.name,
|
||||
user_id: Number(data.user),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,26 +141,87 @@ const AddedPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
<FormField
|
||||
name="user"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Kimga tegishli</Label>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="foydalanuvchi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FakeUserList.map((e) => (
|
||||
<SelectItem value={String(e.id)}>
|
||||
{e.firstName} {e.lastName}
|
||||
</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
|
||||
@@ -144,11 +260,56 @@ const AddedPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Rejani bajarish kuni</Label>
|
||||
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full h-12 justify-between font-normal"
|
||||
>
|
||||
{field.value
|
||||
? new Date(field.value).toLocaleDateString()
|
||||
: "Sanani tanlang"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
toYear={new Date().getFullYear() + 50}
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(value) => {
|
||||
if (value) {
|
||||
field.onChange(value.toISOString()); // ⬅️ forma ichiga yozamiz
|
||||
}
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||
disabled={load}
|
||||
disabled={isPending || editPending || initialValues?.is_done}
|
||||
>
|
||||
{load ? (
|
||||
{isPending || editPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
|
||||
89
src/features/plans/ui/DeletePlan.tsx
Normal file
89
src/features/plans/ui/DeletePlan.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { plans_api } from "@/features/plans/lib/api";
|
||||
import type { PlanListData } from "@/features/plans/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, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setPlanDelete: Dispatch<SetStateAction<PlanListData | null>>;
|
||||
planDelete: PlanListData | null;
|
||||
}
|
||||
|
||||
const DeletePlan = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
planDelete,
|
||||
setPlanDelete,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteUser, isPending } = useMutation({
|
||||
mutationFn: (id: number) => plans_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["plan_list"] });
|
||||
toast.success(`Foydalanuvchi o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setPlanDelete(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>Rejani o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham {planDelete?.user.first_name}{" "}
|
||||
{planDelete?.user.last_name} ha tegishli rejani 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={() => planDelete && deleteUser(planDelete.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeletePlan;
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import type { PlanListData } from "@/features/plans/lib/data";
|
||||
import AddedPlan from "@/features/plans/ui/AddedPlan";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
@@ -32,9 +32,8 @@ interface Props {
|
||||
setSearchUser: Dispatch<SetStateAction<string>>;
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
editingPlan: Plan | null;
|
||||
setEditingPlan: Dispatch<SetStateAction<Plan | null>>;
|
||||
setPlans: Dispatch<SetStateAction<Plan[]>>;
|
||||
editingPlan: PlanListData | null;
|
||||
setEditingPlan: Dispatch<SetStateAction<PlanListData | null>>;
|
||||
}
|
||||
|
||||
const FilterPlans = ({
|
||||
@@ -50,7 +49,6 @@ const FilterPlans = ({
|
||||
setDialogOpen,
|
||||
setEditingPlan,
|
||||
editingPlan,
|
||||
setPlans,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex gap-2 mb-4">
|
||||
@@ -60,8 +58,8 @@ const FilterPlans = ({
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Barchasi</SelectItem>
|
||||
<SelectItem value="Bajarildi">Bajarildi</SelectItem>
|
||||
<SelectItem value="Bajarilmagan">Bajarilmagan</SelectItem>
|
||||
<SelectItem value="true">Bajarildi</SelectItem>
|
||||
<SelectItem value="false">Bajarilmagan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
@@ -128,7 +126,6 @@ const FilterPlans = ({
|
||||
<AddedPlan
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import type { PlanListData } from "@/features/plans/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Table,
|
||||
@@ -9,91 +9,115 @@ import {
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { Edit, Eye, Trash } from "lucide-react";
|
||||
import { Edit, Eye, Loader2, Trash } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
filteredPlans: Plan[];
|
||||
setEditingPlan: Dispatch<SetStateAction<Plan | null>>;
|
||||
filteredPlans: PlanListData[] | [];
|
||||
setEditingPlan: Dispatch<SetStateAction<PlanListData | null>>;
|
||||
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (id: number) => void;
|
||||
handleDelete: (id: PlanListData) => void;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
isFetching: boolean;
|
||||
}
|
||||
|
||||
const PalanTable = ({
|
||||
filteredPlans,
|
||||
setEditingPlan,
|
||||
setDetail,
|
||||
isError,
|
||||
isFetching,
|
||||
isLoading,
|
||||
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>
|
||||
{(isLoading || isFetching) && (
|
||||
<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>
|
||||
)}
|
||||
|
||||
{!isLoading && !isError && (
|
||||
<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>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPlans.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.title}</TableCell>
|
||||
<TableCell>{plan.description}</TableCell>
|
||||
<TableCell>
|
||||
{plan.user.first_name + " " + plan.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={clsx(
|
||||
plan.is_done ? "text-green-500" : "text-red-500",
|
||||
)}
|
||||
>
|
||||
{plan.is_done ? "Bajarilgan" : "Bajarilmagan"}
|
||||
</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"
|
||||
disabled={plan.is_done}
|
||||
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"
|
||||
disabled={plan.is_done}
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import type { PlanListData } from "@/features/plans/lib/data";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -14,7 +14,7 @@ import { type Dispatch, type SetStateAction } from "react";
|
||||
interface Props {
|
||||
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||
detail: boolean;
|
||||
plan: Plan | null;
|
||||
plan: PlanListData | null;
|
||||
}
|
||||
|
||||
const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
||||
@@ -33,7 +33,7 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
||||
{/* Reja nomi */}
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Reja nomi:</p>
|
||||
<p>{plan.name}</p>
|
||||
<p>{plan.title}</p>
|
||||
</div>
|
||||
|
||||
{/* Reja tavsifi */}
|
||||
@@ -46,7 +46,7 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Kimga tegishli:</p>
|
||||
<p>
|
||||
{plan.user.firstName} {plan.user.lastName}
|
||||
{plan.user.first_name} {plan.user.last_name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -56,13 +56,13 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
||||
|
||||
<Badge
|
||||
className={clsx(
|
||||
plan.status === "Bajarildi"
|
||||
plan.is_done
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-yellow-100 text-yellow-700",
|
||||
"text-sm px-4 py-2 mt-2",
|
||||
)}
|
||||
>
|
||||
{plan.status}
|
||||
{plan.is_done ? "Bajarilgan" : "Bajarilmagan"}
|
||||
</Badge>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
|
||||
@@ -1,63 +1,61 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import { plans_api } from "@/features/plans/lib/api";
|
||||
import type { PlanListData } from "@/features/plans/lib/data";
|
||||
import DeletePlan from "@/features/plans/ui/DeletePlan";
|
||||
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 formatDate from "@/shared/lib/formatDate";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const PlansList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<Plan[]>([
|
||||
{
|
||||
id: 1,
|
||||
name: "Tumanga borish",
|
||||
description: "Tumanga borish rejasi",
|
||||
user: FakeUserList[0],
|
||||
status: "Bajarildi",
|
||||
createdAt: new Date("2025-02-03"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Yangi reja",
|
||||
description: "Yangi reja tavsifi",
|
||||
user: FakeUserList[1],
|
||||
status: "Bajarilmagan",
|
||||
createdAt: new Date("2025-01-12"),
|
||||
},
|
||||
]);
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<Plan | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<boolean>(false);
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState<string>("all");
|
||||
const [dateFilter, setDateFilter] = useState<Date | undefined>(undefined);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
const limit = 20;
|
||||
const { data, isLoading, isError, isFetching } = useQuery({
|
||||
queryKey: ["plan_list", dateFilter, searchUser, statusFilter, currentPage],
|
||||
queryFn: () => {
|
||||
const params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
status?: boolean;
|
||||
date?: string;
|
||||
user?: string;
|
||||
} = {
|
||||
date: dateFilter && formatDate.format(dateFilter, "YYYY-MM-DD"),
|
||||
user: searchUser,
|
||||
limit,
|
||||
offset: (currentPage - 1) * limit,
|
||||
};
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setPlans(plans.filter((p) => p.id !== id));
|
||||
if (statusFilter !== "all") {
|
||||
params.status = statusFilter === "true" ? true : false;
|
||||
}
|
||||
|
||||
return plans_api.list(params);
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const totalPages = data ? Math.ceil(data.count / limit) : 1;
|
||||
const [editingPlan, setEditingPlan] = useState<PlanListData | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<boolean>(false);
|
||||
|
||||
const [openDelete, setOpenDelete] = useState<boolean>(false);
|
||||
const [planDelete, setPlanDelete] = useState<PlanListData | null>(null);
|
||||
|
||||
const handleDelete = (id: PlanListData) => {
|
||||
setOpenDelete(true);
|
||||
setPlanDelete(id);
|
||||
};
|
||||
|
||||
const filteredPlans = useMemo(() => {
|
||||
return plans.filter((item) => {
|
||||
const statusMatch =
|
||||
statusFilter === "all" || item.status === statusFilter;
|
||||
|
||||
const dateMatch = dateFilter
|
||||
? item.createdAt.toDateString() === dateFilter.toDateString()
|
||||
: true;
|
||||
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return statusMatch && dateMatch && userMatch;
|
||||
});
|
||||
}, [plans, statusFilter, dateFilter, 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">
|
||||
@@ -72,7 +70,6 @@ const PlansList = () => {
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
setOpen={setOpen}
|
||||
setPlans={setPlans}
|
||||
setSearchUser={setSearchUser}
|
||||
setStatusFilter={setStatusFilter}
|
||||
statusFilter={statusFilter}
|
||||
@@ -82,11 +79,14 @@ const PlansList = () => {
|
||||
</div>
|
||||
|
||||
<PalanTable
|
||||
filteredPlans={filteredPlans}
|
||||
filteredPlans={data ? data.results : []}
|
||||
handleDelete={handleDelete}
|
||||
setDetail={setDetail}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
isError={isError}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
@@ -94,6 +94,13 @@ const PlansList = () => {
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
|
||||
<DeletePlan
|
||||
opneDelete={openDelete}
|
||||
planDelete={planDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
setPlanDelete={setPlanDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RegionListRes } from "@/features/region/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { REGIONS } from "@/shared/config/api/URLs";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const region_api = {
|
||||
@@ -9,21 +9,24 @@ export const region_api = {
|
||||
offset?: number;
|
||||
name?: string;
|
||||
}): Promise<AxiosResponse<RegionListRes>> {
|
||||
return await httpClient.get(`${REGIONS}list/`, { params });
|
||||
return await httpClient.get(`${API_URLS.REGIONS}list/`, { params });
|
||||
},
|
||||
|
||||
async create(body: { name: string }) {
|
||||
const res = await httpClient.post(`${REGIONS}create/`, body);
|
||||
const res = await httpClient.post(`${API_URLS.REGIONS}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: { name: string } }) {
|
||||
const res = await httpClient.patch(`${REGIONS}${id}/update/`, body);
|
||||
const res = await httpClient.patch(
|
||||
`${API_URLS.REGIONS}${id}/update/`,
|
||||
body,
|
||||
);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${REGIONS}${id}/delete/`);
|
||||
const res = await httpClient.delete(`${API_URLS.REGIONS}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
|
||||
14
src/features/reports/lib/api.ts
Normal file
14
src/features/reports/lib/api.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { ResportListRes } from "@/features/reports/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const report_api = {
|
||||
async list(params: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<AxiosResponse<ResportListRes>> {
|
||||
const res = await httpClient.get(`${API_URLS.REPORT}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -67,3 +67,26 @@ export const ReportsData: ReportsTypeList[] = [
|
||||
month: new Date(2025, 4, 1),
|
||||
},
|
||||
];
|
||||
|
||||
export interface ResportListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: ResportListResData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ResportListResData {
|
||||
id: number;
|
||||
employee_name: string;
|
||||
factory: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
price: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
import { ReportsData, type ReportsTypeList } from "@/features/reports/lib/data";
|
||||
import AddedReport from "@/features/reports/ui/AddedReport";
|
||||
import { report_api } from "@/features/reports/lib/api";
|
||||
import ReportsTable from "@/features/reports/ui/ReportsTable";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const ReportsList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<ReportsTypeList[]>(ReportsData);
|
||||
const limit = 20;
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["report_list", currentPage],
|
||||
queryFn: () =>
|
||||
report_api.list({ limit, offset: (currentPage - 1) * limit }),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
const totalPages = data ? Math.ceil(data.count / limit) : 1;
|
||||
// const [plans, setPlans] = useState<ReportsTypeList[]>(ReportsData);
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<ReportsTypeList | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
// const [editingPlan, setEditingPlan] = useState<ReportsTypeList | null>(null);
|
||||
// const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setPlans(plans.filter((p) => p.id !== id));
|
||||
};
|
||||
// const handleDelete = (id: number) => {
|
||||
// setPlans(plans.filter((p) => p.id !== id));
|
||||
// };
|
||||
|
||||
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">To'lovlar</h1>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
{/* <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -53,14 +53,16 @@ const ReportsList = () => {
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</Dialog> */}
|
||||
</div>
|
||||
|
||||
<ReportsTable
|
||||
handleDelete={handleDelete}
|
||||
plans={plans}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
// handleDelete={handleDelete}
|
||||
plans={data ? data.results : []}
|
||||
// setDialogOpen={setDialogOpen}
|
||||
// setEditingPlan={setEditingPlan}
|
||||
isLoading={isLoading}
|
||||
isError={isError}
|
||||
/>
|
||||
|
||||
<Pagination
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ReportsTypeList } from "@/features/reports/lib/data";
|
||||
import type { ResportListResData } 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,
|
||||
@@ -10,43 +9,62 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import { Edit, Trash } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
|
||||
const ReportsTable = ({
|
||||
plans,
|
||||
setEditingPlan,
|
||||
setDialogOpen,
|
||||
handleDelete,
|
||||
// setEditingPlan,
|
||||
// setDialogOpen,
|
||||
// handleDelete,
|
||||
isError,
|
||||
isLoading,
|
||||
}: {
|
||||
plans: ReportsTypeList[];
|
||||
setEditingPlan: Dispatch<SetStateAction<ReportsTypeList | null>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (id: number) => void;
|
||||
plans: ResportListResData[];
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
// setEditingPlan: Dispatch<SetStateAction<ResportListResData | 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>
|
||||
{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>
|
||||
)}
|
||||
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
{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 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.employee_name}</TableCell>
|
||||
<TableCell>{formatPrice(plan.price, true)}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.created_at, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
|
||||
{/* <TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
@@ -66,11 +84,12 @@ const ReportsTable = ({
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableCell> */}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
33
src/features/specifications/lib/api.ts
Normal file
33
src/features/specifications/lib/api.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type {
|
||||
OrderCreateReq,
|
||||
OrderListRes,
|
||||
OrderUpdateReq,
|
||||
} from "@/features/specifications/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const order_api = {
|
||||
async list(params: {
|
||||
limit: number;
|
||||
offset: number;
|
||||
}): Promise<AxiosResponse<OrderListRes>> {
|
||||
const res = await httpClient.get(`${API_URLS.ORDER}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: OrderCreateReq) {
|
||||
const res = await httpClient.post(`${API_URLS.ORDER}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: OrderUpdateReq }) {
|
||||
const res = await httpClient.patch(`${API_URLS.ORDER}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${API_URLS.ORDER}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -79,3 +79,71 @@ export const FakeSpecifications: SpecificationsType[] = [
|
||||
paidPrice: 22400,
|
||||
},
|
||||
];
|
||||
|
||||
export interface OrderListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: OrderListDataRes[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrderListDataRes {
|
||||
id: number;
|
||||
factory: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
total_price: string;
|
||||
paid_price: string;
|
||||
advance: number;
|
||||
employee_name: string;
|
||||
overdue_price: string;
|
||||
order_items: [
|
||||
{
|
||||
id: number;
|
||||
product: number;
|
||||
quantity: number;
|
||||
total_price: string;
|
||||
},
|
||||
];
|
||||
file: string;
|
||||
user: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface OrderCreateReq {
|
||||
factory_id: number;
|
||||
paid_price: string;
|
||||
total_price: string;
|
||||
advance: number;
|
||||
employee_name: string;
|
||||
user_id: number;
|
||||
items: {
|
||||
product: number;
|
||||
quantity: number;
|
||||
total_price: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface OrderUpdateReq {
|
||||
total_price: string;
|
||||
user_id: number;
|
||||
factory_id: number;
|
||||
paid_price: string;
|
||||
advance: number;
|
||||
employee_name: string;
|
||||
overdue_price?: string;
|
||||
items: {
|
||||
product_id: number;
|
||||
quantity: number;
|
||||
total_price: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { pharmData } from "@/features/pharm/lib/data";
|
||||
import { factory_api } from "@/features/pharm/lib/api";
|
||||
import { pill_api } from "@/features/pill/lib/api";
|
||||
import { order_api } from "@/features/specifications/lib/api";
|
||||
import {
|
||||
SpecificationsFakePills,
|
||||
type SpecificationsType,
|
||||
type OrderCreateReq,
|
||||
type OrderListDataRes,
|
||||
type OrderUpdateReq,
|
||||
} from "@/features/specifications/lib/data";
|
||||
import {
|
||||
SpecificationsForm,
|
||||
type SpecificationsFormType,
|
||||
} from "@/features/specifications/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
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,
|
||||
@@ -21,65 +33,161 @@ 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 { useEffect } from "react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
initialValues: SpecificationsType | null;
|
||||
initialValues: OrderListDataRes | null;
|
||||
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setData: React.Dispatch<React.SetStateAction<SpecificationsType[]>>;
|
||||
}
|
||||
|
||||
export const AddedSpecification = ({
|
||||
setData,
|
||||
initialValues,
|
||||
setDialogOpen,
|
||||
}: Props) => {
|
||||
export const AddedSpecification = ({ initialValues, setDialogOpen }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const { data: pill } = useQuery({
|
||||
queryKey: ["pill_list", initialValues],
|
||||
queryFn: () =>
|
||||
pill_api.list({
|
||||
limit: 999,
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const [userSearch, setUserSearch] = useState<string>("");
|
||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||
|
||||
const { data: user, isLoading: isUserLoading } = useQuery({
|
||||
queryKey: ["user_list", userSearch, initialValues],
|
||||
queryFn: () => user_api.list({ search: userSearch }),
|
||||
select(data) {
|
||||
return data.data.data.results;
|
||||
},
|
||||
});
|
||||
|
||||
const [factorySearch, setFactorySearch] = useState<string>("");
|
||||
const [openFactory, setOpenFactory] = useState<boolean>(false);
|
||||
|
||||
const { data: pharm, isLoading: isPharmLoading } = useQuery({
|
||||
queryKey: ["factory_list", userSearch, initialValues],
|
||||
queryFn: () => factory_api.list({ name: factorySearch }),
|
||||
select(data) {
|
||||
return data.data.data.results;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: create, isPending: createPending } = useMutation({
|
||||
mutationFn: (body: OrderCreateReq) => order_api.create(body),
|
||||
onSuccess: () => {
|
||||
setDialogOpen(false);
|
||||
queryClient.resetQueries({ queryKey: ["order_list"] });
|
||||
},
|
||||
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: update, isPending: updatePending } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: number; body: OrderUpdateReq }) =>
|
||||
order_api.update({ body, id }),
|
||||
onSuccess: () => {
|
||||
setDialogOpen(false);
|
||||
queryClient.resetQueries({ queryKey: ["order_list"] });
|
||||
},
|
||||
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 form = useForm<SpecificationsFormType>({
|
||||
resolver: zodResolver(SpecificationsForm),
|
||||
defaultValues: initialValues
|
||||
? {
|
||||
client: initialValues.client,
|
||||
pharm: String(initialValues.pharm.id),
|
||||
percentage: initialValues.percentage,
|
||||
totalPrice: initialValues.totalPrice,
|
||||
paidPrice: initialValues.paidPrice,
|
||||
user: String(initialValues.user.id),
|
||||
medicines: [
|
||||
...initialValues.medicines,
|
||||
...SpecificationsFakePills.filter(
|
||||
(p) => !initialValues.medicines.some((m) => m.id === p.id),
|
||||
).map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
count: 0,
|
||||
price: p.price,
|
||||
})),
|
||||
],
|
||||
}
|
||||
: {
|
||||
client: "",
|
||||
pharm: "",
|
||||
user: "",
|
||||
percentage: 0,
|
||||
totalPrice: 0,
|
||||
paidPrice: 0,
|
||||
medicines: SpecificationsFakePills.map((p) => ({
|
||||
defaultValues: {
|
||||
client: "",
|
||||
pharm: "",
|
||||
user: "",
|
||||
percentage: 0,
|
||||
totalPrice: 0,
|
||||
paidPrice: 0,
|
||||
medicines: [],
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!pill) return;
|
||||
|
||||
if (initialValues) {
|
||||
const mergedMedicines = [
|
||||
...initialValues.order_items.map((item) => {
|
||||
const pillItem = pill.results.find((p) => p.id === item.product);
|
||||
|
||||
return {
|
||||
id: item.product,
|
||||
name: pillItem ? pillItem.name : "Unknown",
|
||||
price: pillItem ? Number(pillItem.price) : 0,
|
||||
count: Number(item.quantity),
|
||||
};
|
||||
}),
|
||||
|
||||
...pill.results
|
||||
.filter(
|
||||
(p) => !initialValues.order_items.some((m) => m.product === p.id),
|
||||
)
|
||||
.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
price: Number(p.price),
|
||||
count: 0,
|
||||
price: p.price,
|
||||
})),
|
||||
},
|
||||
});
|
||||
];
|
||||
|
||||
form.reset({
|
||||
client: initialValues.employee_name,
|
||||
pharm: String(initialValues.factory.id),
|
||||
percentage: initialValues.advance,
|
||||
totalPrice: Number(initialValues.total_price),
|
||||
paidPrice: Number(initialValues.paid_price),
|
||||
user: String(initialValues.user.id),
|
||||
medicines: mergedMedicines,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const fakeMedicines = pill.results.map((p) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
price: Number(p.price),
|
||||
count: 0,
|
||||
}));
|
||||
|
||||
form.reset({
|
||||
client: "",
|
||||
pharm: "",
|
||||
user: "",
|
||||
percentage: 0,
|
||||
totalPrice: 0,
|
||||
paidPrice: 0,
|
||||
medicines: fakeMedicines,
|
||||
});
|
||||
}, [pill, initialValues, form]);
|
||||
|
||||
const medicines = form.watch("medicines");
|
||||
|
||||
@@ -105,84 +213,229 @@ export const AddedSpecification = ({
|
||||
}, [form]);
|
||||
|
||||
const onSubmit = (values: SpecificationsFormType) => {
|
||||
if (initialValues) {
|
||||
setData((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === initialValues.id
|
||||
? {
|
||||
...item,
|
||||
...values,
|
||||
pharm: pharmData.find((e) => e.id === Number(values.pharm))!,
|
||||
user: FakeUserList.find((e) => e.id === Number(values.user))!,
|
||||
}
|
||||
: item,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
setData((prev) => [
|
||||
...prev,
|
||||
{
|
||||
...values,
|
||||
id: Date.now(),
|
||||
pharm: pharmData.find((e) => e.id === Number(values.pharm))!,
|
||||
user: FakeUserList[1],
|
||||
if (!initialValues) {
|
||||
const items = medicines
|
||||
.filter((med) => med.count > 0)
|
||||
.map((med) => ({
|
||||
product: med.id,
|
||||
quantity: med.count,
|
||||
total_price: (med.price * med.count).toFixed(2),
|
||||
}));
|
||||
|
||||
const total_price = items
|
||||
.reduce((sum, item) => sum + parseFloat(item.total_price), 0)
|
||||
.toFixed(2);
|
||||
|
||||
create({
|
||||
advance: values.percentage,
|
||||
employee_name: values.client,
|
||||
factory_id: Number(values.pharm),
|
||||
paid_price: String(values.paidPrice),
|
||||
total_price,
|
||||
items,
|
||||
user_id: Number(values.user),
|
||||
});
|
||||
} else if (initialValues) {
|
||||
const items = medicines
|
||||
.filter((med) => med.count > 0)
|
||||
.map((med) => ({
|
||||
product_id: med.id,
|
||||
quantity: med.count,
|
||||
total_price: (med.price * med.count).toFixed(2),
|
||||
}));
|
||||
|
||||
const total_price = items
|
||||
.reduce((sum, item) => sum + parseFloat(item.total_price), 0)
|
||||
.toFixed(2);
|
||||
|
||||
update({
|
||||
body: {
|
||||
advance: values.percentage,
|
||||
employee_name: values.client,
|
||||
paid_price: String(values.paidPrice),
|
||||
total_price,
|
||||
items: items,
|
||||
factory_id: Number(values.pharm),
|
||||
user_id: Number(values.user),
|
||||
},
|
||||
]);
|
||||
id: initialValues.id,
|
||||
});
|
||||
}
|
||||
setDialogOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pharm"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Farmasevtika</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Farmasevtikalar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{pharmData.map((e) => (
|
||||
<SelectItem value={String(e.id)} key={e.id}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const selectedUser = pharm?.find(
|
||||
(u) => String(u.id) === field.value,
|
||||
);
|
||||
return (
|
||||
<FormItem className="flex flex-col">
|
||||
<Label className="text-md">Farmasevtika</Label>
|
||||
|
||||
<Popover open={openFactory} onOpenChange={setOpenFactory}>
|
||||
<PopoverTrigger asChild>
|
||||
<FormControl>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openFactory}
|
||||
className={cn(
|
||||
"w-full h-12 justify-between",
|
||||
!field.value && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{selectedUser
|
||||
? `${selectedUser.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={factorySearch}
|
||||
onValueChange={setFactorySearch}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{isPharmLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : pharm && pharm.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{pharm.map((u) => (
|
||||
<CommandItem
|
||||
key={u.id}
|
||||
value={`${u.id}`}
|
||||
onSelect={() => {
|
||||
field.onChange(String(u.id));
|
||||
setOpenFactory(false);
|
||||
}}
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
field.value === String(u.id)
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
{u.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Farmasevtika topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<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)} key={e.id}>
|
||||
{e.firstName} {e.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
control={form.control}
|
||||
render={({ field }) => {
|
||||
const selectedUser = user?.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>
|
||||
<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={userSearch}
|
||||
onValueChange={setUserSearch}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{isUserLoading ? (
|
||||
<div className="py-6 text-center text-sm">
|
||||
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
|
||||
</div>
|
||||
) : user && user.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{user.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
|
||||
@@ -206,7 +459,10 @@ export const AddedSpecification = ({
|
||||
key={med.id}
|
||||
className="flex justify-between items-center space-x-2"
|
||||
>
|
||||
<p className="w-40">{med.name}</p>
|
||||
<div className="flex flex-col">
|
||||
<p className="w-40">{med.name}</p>
|
||||
<p>Narxi:{formatPrice(med.price)}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<FormControl>
|
||||
@@ -293,9 +549,10 @@ export const AddedSpecification = ({
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={createPending || updatePending}
|
||||
className="w-full bg-blue-500 hover:bg-blue-500 cursor-pointer h-12"
|
||||
>
|
||||
Saqlash
|
||||
{createPending || updatePending ? <Loader2 /> : "Saqlash"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
88
src/features/specifications/ui/DeleteOrder.tsx
Normal file
88
src/features/specifications/ui/DeleteOrder.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { order_api } from "@/features/specifications/lib/api";
|
||||
import type { OrderListDataRes } from "@/features/specifications/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, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setPillDelete: Dispatch<SetStateAction<OrderListDataRes | null>>;
|
||||
pillDelete: OrderListDataRes | null;
|
||||
}
|
||||
|
||||
const DeleteOrder = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
pillDelete,
|
||||
setPillDelete,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteUser, isPending } = useMutation({
|
||||
mutationFn: (id: number) => order_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["order_list"] });
|
||||
toast.success(`Foydalanuvchi o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setPillDelete(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>Dorini o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham bu zakazni 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={() => pillDelete && deleteUser(pillDelete.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteOrder;
|
||||
@@ -1,17 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import type { SpecificationsType } from "@/features/specifications/lib/data";
|
||||
import type { OrderListDataRes } from "@/features/specifications/lib/data";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { HardDriveDownloadIcon } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
specification: SpecificationsType | null;
|
||||
specification: OrderListDataRes | null;
|
||||
open: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
@@ -21,6 +23,37 @@ export const SpecificationDetail = ({
|
||||
open,
|
||||
setOpen,
|
||||
}: Props) => {
|
||||
const downloadFile = async (fileUrl: string, fileName: string) => {
|
||||
try {
|
||||
const response = await fetch(fileUrl, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
// Agar token kerak bo'lsa qo'shing
|
||||
// Authorization: `Bearer ${yourToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error("Fayl yuklab olishda xatolik yuz berdi");
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = fileName || "file.pdf"; // fayl nomi
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
|
||||
// Tozalash
|
||||
a.remove();
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (!specification) return null;
|
||||
|
||||
return (
|
||||
@@ -39,7 +72,7 @@ export const SpecificationDetail = ({
|
||||
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
|
||||
<p className="text-sm text-blue-600 font-medium mb-1">Xaridor</p>
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
{specification.client}
|
||||
{specification.employee_name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -49,7 +82,7 @@ export const SpecificationDetail = ({
|
||||
Farmasevtika
|
||||
</p>
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
{specification.pharm.name}
|
||||
{specification.factory.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +92,7 @@ export const SpecificationDetail = ({
|
||||
Mas'ul xodim
|
||||
</p>
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
{specification.user.firstName} {specification.user.lastName}
|
||||
{specification.user.first_name} {specification.user.last_name}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -68,13 +101,13 @@ export const SpecificationDetail = ({
|
||||
<div className="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-4 flex items-center">
|
||||
<span className="bg-indigo-600 text-white rounded-full w-8 h-8 flex items-center justify-center text-sm mr-3">
|
||||
{specification.medicines.length}
|
||||
{specification.order_items.length}
|
||||
</span>
|
||||
Dorilar ro'yxati
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{specification.medicines.map((med, index) => (
|
||||
{specification.order_items.map((med, index) => (
|
||||
<div
|
||||
key={med.id}
|
||||
className="bg-white rounded-lg p-4 border border-gray-200 hover:border-indigo-300 transition-colors"
|
||||
@@ -86,23 +119,28 @@ export const SpecificationDetail = ({
|
||||
#{index + 1}
|
||||
</span>
|
||||
<p className="font-semibold text-gray-800">
|
||||
{med.name}
|
||||
{med.product}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||
<span>
|
||||
Miqdor: <strong>{med.count} ta</strong>
|
||||
Miqdor: <strong>{med.quantity} ta</strong>
|
||||
</span>
|
||||
<span>×</span>
|
||||
<span>
|
||||
Narx: <strong>{formatPrice(med.price)}</strong>
|
||||
Narx:{" "}
|
||||
<strong>
|
||||
{formatPrice(
|
||||
Number(med.total_price) / med.quantity,
|
||||
)}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right ml-4">
|
||||
<p className="text-xs text-gray-500 mb-1">Jami</p>
|
||||
<p className="text-lg font-bold text-indigo-600">
|
||||
{formatPrice(med.count * med.price)}
|
||||
{formatPrice(med.total_price)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -121,29 +159,38 @@ export const SpecificationDetail = ({
|
||||
<div className="flex justify-between items-center pb-3 border-b border-slate-300">
|
||||
<span className="text-gray-600 font-medium">Jami narx:</span>
|
||||
<span className="text-xl font-bold text-gray-800">
|
||||
{formatPrice(specification.totalPrice)}
|
||||
{formatPrice(specification.total_price)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pb-3 border-b border-slate-300">
|
||||
<span className="text-gray-600 font-medium">
|
||||
Chegirma foizi:
|
||||
To'langan foizi:
|
||||
</span>
|
||||
<span className="text-lg font-semibold text-orange-600">
|
||||
{specification.percentage}%
|
||||
{specification.advance}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<span className="text-gray-700 font-bold text-lg">
|
||||
To'lanadi:
|
||||
To'langan:
|
||||
</span>
|
||||
<span className="text-2xl font-bold text-green-600">
|
||||
{formatPrice(specification.paidPrice)}
|
||||
{formatPrice(specification.paid_price)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full h-12 bg-blue-600 text-md hover:bg-blue-700 cursor-pointer"
|
||||
onClick={() =>
|
||||
downloadFile(specification.file, `order-${specification.id}`)
|
||||
}
|
||||
>
|
||||
<HardDriveDownloadIcon className="size-5" />
|
||||
PDF faylda yuklab olish
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
FakeSpecifications,
|
||||
type SpecificationsType,
|
||||
} from "@/features/specifications/lib/data";
|
||||
import { order_api } from "@/features/specifications/lib/api";
|
||||
import { type OrderListDataRes } from "@/features/specifications/lib/data";
|
||||
import { AddedSpecification } from "@/features/specifications/ui/AddedSpecification";
|
||||
import DeleteOrder from "@/features/specifications/ui/DeleteOrder";
|
||||
import { SpecificationDetail } from "@/features/specifications/ui/SpecificationDetail ";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@@ -23,30 +23,38 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Eye, Loader2, Pencil, Plus, Trash2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const SpecificationsList = () => {
|
||||
const [data, setData] = useState<SpecificationsType[]>(FakeSpecifications);
|
||||
const [editingPlan, setEditingPlan] = useState<SpecificationsType | null>(
|
||||
null,
|
||||
);
|
||||
const [detail, setDetail] = useState<SpecificationsType | null>(null);
|
||||
const [editingPlan, setEditingPlan] = useState<OrderListDataRes | null>(null);
|
||||
const [detail, setDetail] = useState<OrderListDataRes | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const limit = 20;
|
||||
|
||||
const handleDelete = (id: number) =>
|
||||
setData((prev) => prev.filter((e) => e.id !== id));
|
||||
const {
|
||||
data: order,
|
||||
isLoading,
|
||||
isError,
|
||||
} = useQuery({
|
||||
queryKey: ["order_list", currentPage],
|
||||
queryFn: () => order_api.list({ limit, offset: (currentPage - 1) * limit }),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
const totalPages = order ? Math.ceil(order.count / limit) : 1;
|
||||
|
||||
const [openDelete, setOpenDelete] = useState<boolean>(false);
|
||||
const [pillDelete, setPillDelete] = useState<OrderListDataRes | null>(null);
|
||||
|
||||
const handleDelete = (id: OrderListDataRes) => {
|
||||
setOpenDelete(true);
|
||||
setPillDelete(id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
@@ -70,7 +78,6 @@ const SpecificationsList = () => {
|
||||
<AddedSpecification
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setData={setData}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -83,103 +90,98 @@ const SpecificationsList = () => {
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Foydalanuvchi</TableHead>
|
||||
<TableHead>Farmasevtika</TableHead>
|
||||
<TableHead>Zakaz qilgan</TableHead>
|
||||
<TableHead>Jami</TableHead>
|
||||
<TableHead>% To‘langan</TableHead>
|
||||
<TableHead>To‘langan summa</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item, idx) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{idx + 1}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell>{item.pharm.name}</TableCell>
|
||||
<TableCell>{item.client}</TableCell>
|
||||
<TableCell>{formatPrice(item.totalPrice)}</TableCell>
|
||||
<TableCell>{item.percentage}%</TableCell>
|
||||
<TableCell>{formatPrice(item.paidPrice)}</TableCell>
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailOpen(true);
|
||||
}}
|
||||
className="bg-green-500 hover:bg-green-500 hover:text-white text-white cursor-pointer"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="bg-blue-600 text-white hover:bg-blue-600 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
{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>
|
||||
)}
|
||||
{!isLoading && !isError && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Foydalanuvchi</TableHead>
|
||||
<TableHead>Farmasevtika</TableHead>
|
||||
<TableHead>Zakaz qilgan</TableHead>
|
||||
<TableHead>Jami</TableHead>
|
||||
<TableHead>% To‘langan</TableHead>
|
||||
<TableHead>To‘langan summa</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{order?.results.map((item, idx) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{idx + 1}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.first_name} {item.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell>{item.factory.name}</TableCell>
|
||||
<TableCell>{item.employee_name}</TableCell>
|
||||
<TableCell>{formatPrice(item.total_price)}</TableCell>
|
||||
<TableCell>{item.advance}%</TableCell>
|
||||
<TableCell>{formatPrice(item.paid_price)}</TableCell>
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailOpen(true);
|
||||
}}
|
||||
className="bg-green-500 hover:bg-green-500 hover:text-white text-white cursor-pointer"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="bg-blue-600 text-white hover:bg-blue-600 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
size="icon"
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
className={clsx(
|
||||
currentPage === i + 1 ? "bg-blue-500 hover:bg-blue-600" : "",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={currentPage}
|
||||
setCurrentPage={setCurrentPage}
|
||||
totalPages={totalPages}
|
||||
/>
|
||||
|
||||
<DeleteOrder
|
||||
opneDelete={openDelete}
|
||||
pillDelete={pillDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
setPillDelete={setPillDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
39
src/features/tour-plan/lib/api.ts
Normal file
39
src/features/tour-plan/lib/api.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type {
|
||||
PlanTourCreate,
|
||||
PlanTourListRes,
|
||||
PlanTourUpdate,
|
||||
} from "@/features/tour-plan/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const tour_plan_api = {
|
||||
async list(params: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
name?: string;
|
||||
date?: string;
|
||||
user?: string;
|
||||
}): Promise<AxiosResponse<PlanTourListRes>> {
|
||||
const res = await httpClient.get(`${API_URLS.TOUR_PLAN}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: PlanTourCreate) {
|
||||
const res = await httpClient.post(`${API_URLS.TOUR_PLAN}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: PlanTourUpdate }) {
|
||||
const res = await httpClient.patch(
|
||||
`${API_URLS.TOUR_PLAN}${id}/update/`,
|
||||
body,
|
||||
);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete(id: number) {
|
||||
const res = await httpClient.delete(`${API_URLS.TOUR_PLAN}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -58,3 +58,46 @@ export const fakeTourPlan: TourPlanType[] = [
|
||||
status: "planned",
|
||||
},
|
||||
];
|
||||
|
||||
export interface PlanTourListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: PlanTourListDataRes[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface PlanTourListDataRes {
|
||||
id: number;
|
||||
place_name: string;
|
||||
user: {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
};
|
||||
latitude: null | string;
|
||||
longitude: null | string;
|
||||
location_send: boolean;
|
||||
date: null | string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface PlanTourCreate {
|
||||
place_name: string;
|
||||
// latitude: number;
|
||||
// longitude: number;
|
||||
user_id: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
export interface PlanTourUpdate {
|
||||
place_name: string;
|
||||
user: number;
|
||||
// latitude: number;
|
||||
// longitude: number;
|
||||
date: string;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,4 @@ export const tourPlanForm = z.object({
|
||||
district: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
date: z.date().min(1, { message: "Majburiy maydon" }),
|
||||
long: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
lat: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
|
||||
@@ -1,8 +1,23 @@
|
||||
import type { TourPlanType } from "@/features/tour-plan/lib/data";
|
||||
import { tour_plan_api } from "@/features/tour-plan/lib/api";
|
||||
import type {
|
||||
PlanTourCreate,
|
||||
PlanTourListDataRes,
|
||||
PlanTourUpdate,
|
||||
} from "@/features/tour-plan/lib/data";
|
||||
import { tourPlanForm } from "@/features/tour-plan/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { user_api } from "@/features/users/lib/api";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/shared/ui/command";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -13,103 +28,196 @@ import {
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { ChevronDownIcon, Loader2 } from "lucide-react";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Check, ChevronDownIcon, ChevronsUpDown, Loader2 } from "lucide-react";
|
||||
import { 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: TourPlanType | null;
|
||||
initialValues: PlanTourListDataRes | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setPlans: Dispatch<SetStateAction<TourPlanType[]>>;
|
||||
}
|
||||
|
||||
const AddedTourPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
const AddedTourPlan = ({ initialValues, setDialogOpen }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const form = useForm<z.infer<typeof tourPlanForm>>({
|
||||
resolver: zodResolver(tourPlanForm),
|
||||
defaultValues: {
|
||||
date: initialValues?.date || undefined,
|
||||
district: initialValues?.district || "",
|
||||
lat: initialValues?.lat || "41.2949",
|
||||
long: initialValues?.long || "69.2361",
|
||||
date: initialValues?.date ? new Date(initialValues?.date) : undefined,
|
||||
district: initialValues?.place_name || "",
|
||||
|
||||
user: initialValues?.user.id.toString() || "",
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: PlanTourCreate) => tour_plan_api.create(body),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["tour_plan_list"] });
|
||||
setDialogOpen(false);
|
||||
},
|
||||
onError: (err: AxiosError) => {
|
||||
const errMessage = err.response?.data as { message: string };
|
||||
const messageText = errMessage.message;
|
||||
toast.error(messageText || "Xatolik yuz berdi", {
|
||||
richColors: true,
|
||||
position: "top-center",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: edit, isPending: editPending } = useMutation({
|
||||
mutationFn: ({ body, id }: { id: number; body: PlanTourUpdate }) =>
|
||||
tour_plan_api.update({ body, id }),
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["tour_plan_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 [open, setOpen] = useState(false);
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
|
||||
const lat = form.watch("lat");
|
||||
const long = form.watch("long");
|
||||
const [openUser, setOpenUser] = useState<boolean>(false);
|
||||
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;
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof tourPlanForm>) {
|
||||
setLoad(true);
|
||||
const newObject: TourPlanType = {
|
||||
id: initialValues ? initialValues.id : Date.now(),
|
||||
user: FakeUserList.find((u) => u.id === Number(values.user))!,
|
||||
date: values.date,
|
||||
district: values.district,
|
||||
lat: values.lat,
|
||||
long: values.long,
|
||||
status: "planned",
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setPlans((prev) => {
|
||||
if (initialValues) {
|
||||
return prev.map((item) =>
|
||||
item.id === initialValues.id ? newObject : item,
|
||||
);
|
||||
} else {
|
||||
return [...prev, newObject];
|
||||
}
|
||||
if (!initialValues) {
|
||||
mutate({
|
||||
date: formatDate.format(values.date, "YYYY-MM-DD"),
|
||||
place_name: values.district,
|
||||
user_id: Number(values.user),
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
} else if (initialValues) {
|
||||
edit({
|
||||
body: {
|
||||
user: Number(values.user),
|
||||
date: formatDate.format(values.date, "YYYY-MM-DD"),
|
||||
place_name: values.district,
|
||||
},
|
||||
id: initialValues.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Kim uchun</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full">
|
||||
<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>
|
||||
<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
|
||||
@@ -168,7 +276,7 @@ const AddedTourPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
{/* <div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{ center: [Number(lat), Number(long)], zoom: 16 }}
|
||||
@@ -188,13 +296,13 @@ const AddedTourPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||
>
|
||||
{load ? (
|
||||
{isPending || editPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
|
||||
89
src/features/tour-plan/ui/DeleteTourPlab.tsx
Normal file
89
src/features/tour-plan/ui/DeleteTourPlab.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { tour_plan_api } from "@/features/tour-plan/lib/api";
|
||||
import type { PlanTourListDataRes } from "@/features/tour-plan/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, SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
opneDelete: boolean;
|
||||
setOpenDelete: Dispatch<SetStateAction<boolean>>;
|
||||
setPlanDelete: Dispatch<SetStateAction<PlanTourListDataRes | null>>;
|
||||
planDelete: PlanTourListDataRes | null;
|
||||
}
|
||||
|
||||
const DeleteTourPlan = ({
|
||||
opneDelete,
|
||||
setOpenDelete,
|
||||
planDelete,
|
||||
setPlanDelete,
|
||||
}: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: deleteUser, isPending } = useMutation({
|
||||
mutationFn: (id: number) => tour_plan_api.delete(id),
|
||||
|
||||
onSuccess: () => {
|
||||
queryClient.refetchQueries({ queryKey: ["tour_plan_list"] });
|
||||
toast.success(`Foydalanuvchi o'chirildi`);
|
||||
setOpenDelete(false);
|
||||
setPlanDelete(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>Tur Planni o'chirish</DialogTitle>
|
||||
<DialogDescription className="text-md font-semibold">
|
||||
Siz rostan ham {planDelete?.user.first_name}{" "}
|
||||
{planDelete?.user.last_name} ha tegishli rejani 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={() => planDelete && deleteUser(planDelete.id)}
|
||||
>
|
||||
{isPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Trash />
|
||||
O'chirish
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteTourPlan;
|
||||
113
src/features/tour-plan/ui/FilterTourPlan.tsx
Normal file
113
src/features/tour-plan/ui/FilterTourPlan.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import type { PlanTourListDataRes } from "@/features/tour-plan/lib/data";
|
||||
import AddedTourPlan from "@/features/tour-plan/ui/AddedTourPlan";
|
||||
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 { ChevronDownIcon, Plus } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
dateFilter: Date | undefined;
|
||||
setDateFilter: Dispatch<SetStateAction<Date | undefined>>;
|
||||
searchUser: string;
|
||||
setSearchUser: Dispatch<SetStateAction<string>>;
|
||||
dialogOpen: boolean;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setEditingPlan: Dispatch<SetStateAction<PlanTourListDataRes | null>>;
|
||||
editingPlan: PlanTourListDataRes | null;
|
||||
}
|
||||
|
||||
const FilterTourPlan = ({
|
||||
dateFilter,
|
||||
setDateFilter,
|
||||
searchUser,
|
||||
setSearchUser,
|
||||
setDialogOpen,
|
||||
dialogOpen,
|
||||
setEditingPlan,
|
||||
editingPlan,
|
||||
}: Props) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className="flex gap-2 mb-4">
|
||||
<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);
|
||||
}}
|
||||
toYear={new Date().getFullYear() + 50}
|
||||
/>
|
||||
<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>
|
||||
|
||||
<AddedTourPlan
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterTourPlan;
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import type { TourPlanType } from "@/features/tour-plan/lib/data";
|
||||
import type { PlanTourListDataRes } from "@/features/tour-plan/lib/data";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -13,7 +14,7 @@ import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
plan: TourPlanType | null;
|
||||
plan: PlanTourListDataRes | null;
|
||||
}
|
||||
|
||||
const TourPlanDetailDialog = ({ open, setOpen, plan }: Props) => {
|
||||
@@ -33,55 +34,50 @@ const TourPlanDetailDialog = ({ open, setOpen, plan }: Props) => {
|
||||
<div>
|
||||
<p className="font-semibold">Foydalanuvchi:</p>
|
||||
<p>
|
||||
{plan.user.firstName} {plan.user.lastName}
|
||||
{plan.user.first_name} {plan.user.last_name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* District */}
|
||||
<div>
|
||||
<p className="font-semibold">Hudud:</p>
|
||||
<p>{plan.district}</p>
|
||||
<p>{plan.place_name}</p>
|
||||
</div>
|
||||
|
||||
{/* Sana */}
|
||||
<div>
|
||||
<p className="font-semibold">Sana:</p>
|
||||
<p>{plan.date.toLocaleString()}</p>
|
||||
<p>{plan.date && formatDate.format(plan.date, "YYYY-MM-DD")}</p>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<p className="font-semibold">Status:</p>
|
||||
<Badge
|
||||
className={
|
||||
plan.status === "completed" ? "bg-green-600" : "bg-yellow-500"
|
||||
}
|
||||
className={plan.location_send ? "bg-green-600" : "bg-yellow-500"}
|
||||
>
|
||||
{plan.status === "completed" ? "Bajarilgan" : "Rejalashtirilgan"}
|
||||
{plan.location_send ? "Bajarilgan" : "Rejalashtirilgan"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{plan.userLocation && (
|
||||
{plan.location_send && (
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [
|
||||
Number(plan.userLocation.lat),
|
||||
Number(plan.userLocation.long),
|
||||
],
|
||||
center: [Number(plan.latitude), Number(plan.longitude)],
|
||||
zoom: 16,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
>
|
||||
<Placemark
|
||||
geometry={[
|
||||
Number(plan.userLocation.lat),
|
||||
Number(plan.userLocation.long),
|
||||
]}
|
||||
geometry={[Number(plan.latitude), Number(plan.longitude)]}
|
||||
/>
|
||||
<Circle
|
||||
geometry={[[Number(plan.lat), Number(plan.long)], 100]}
|
||||
geometry={[
|
||||
[Number(plan.latitude), Number(plan.longitude)],
|
||||
100,
|
||||
]}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
|
||||
@@ -1,151 +1,67 @@
|
||||
import { fakeTourPlan, type TourPlanType } from "@/features/tour-plan/lib/data";
|
||||
import AddedTourPlan from "@/features/tour-plan/ui/AddedTourPlan";
|
||||
import { tour_plan_api } from "@/features/tour-plan/lib/api";
|
||||
import { type PlanTourListDataRes } from "@/features/tour-plan/lib/data";
|
||||
import DeleteTourPlan from "@/features/tour-plan/ui/DeleteTourPlab";
|
||||
import FilterTourPlan from "@/features/tour-plan/ui/FilterTourPlan";
|
||||
import TourPlanDetailDialog from "@/features/tour-plan/ui/TourPlanDetailDialog";
|
||||
import TourPlanTable from "@/features/tour-plan/ui/TourPlanTable";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
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 {
|
||||
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 { useMemo, useState } from "react";
|
||||
import Pagination from "@/shared/ui/pagination";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const TourPlanList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<TourPlanType[]>(fakeTourPlan);
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<TourPlanType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<TourPlanType | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState<boolean>(false);
|
||||
|
||||
const limit = 20;
|
||||
const [dateFilter, setDateFilter] = useState<Date | undefined>(undefined);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setPlans(plans.filter((p) => p.id !== id));
|
||||
const { data, isError, isLoading, isFetching } = useQuery({
|
||||
queryKey: ["tour_plan_list", currentPage, dateFilter, searchUser],
|
||||
queryFn: () =>
|
||||
tour_plan_api.list({
|
||||
limit,
|
||||
offset: (currentPage - 1) * limit,
|
||||
date: dateFilter && formatDate.format(dateFilter, "YYYY-MM-DD"),
|
||||
user: searchUser,
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const totalPages = data ? Math.ceil(data.count / limit) : 1;
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<PlanTourListDataRes | null>(
|
||||
null,
|
||||
);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [detail, setDetail] = useState<PlanTourListDataRes | null>(null);
|
||||
const [detailOpen, setDetailOpen] = useState<boolean>(false);
|
||||
const [openDelete, setOpenDelete] = useState<boolean>(false);
|
||||
const [planDelete, setPlanDelete] = useState<PlanTourListDataRes | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
const handleDelete = (id: PlanTourListDataRes) => {
|
||||
setOpenDelete(true);
|
||||
setPlanDelete(id);
|
||||
};
|
||||
|
||||
const filteredPlans = useMemo(() => {
|
||||
return plans.filter((item) => {
|
||||
// 2) Sana filtri: createdAt === tanlangan sana
|
||||
const dateMatch = dateFilter
|
||||
? item.date.toDateString() === dateFilter.toDateString()
|
||||
: true;
|
||||
|
||||
// 3) User ism familiya bo'yicha qidiruv
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return dateMatch && userMatch;
|
||||
});
|
||||
}, [plans, dateFilter, searchUser]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||
<h1 className="text-2xl font-bold">Rejalarni boshqarish</h1>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
{/* 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>
|
||||
|
||||
<AddedTourPlan
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
<FilterTourPlan
|
||||
dateFilter={dateFilter}
|
||||
dialogOpen={dialogOpen}
|
||||
editingPlan={editingPlan}
|
||||
searchUser={searchUser}
|
||||
setDateFilter={setDateFilter}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
setSearchUser={setSearchUser}
|
||||
/>
|
||||
|
||||
<TourPlanDetailDialog
|
||||
plan={detail}
|
||||
@@ -154,116 +70,30 @@ const TourPlanList = () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Kimga tegishli</TableHead>
|
||||
<TableHead className="text-start">Boriladigan joyi</TableHead>
|
||||
<TableHead className="text-start">Sanasi</TableHead>
|
||||
<TableHead className="text-start">Statusi</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.user.firstName + " " + plan.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell>{plan.district}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.date, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={clsx(
|
||||
plan.status === "completed"
|
||||
? "text-green-500"
|
||||
: "text-red-500",
|
||||
)}
|
||||
>
|
||||
{plan.status === "completed" ? "Borildi" : "Borilmagan"}
|
||||
</TableCell>
|
||||
<TourPlanTable
|
||||
data={data ? data.results : []}
|
||||
handleDelete={handleDelete}
|
||||
isError={isError}
|
||||
isFetching={isFetching}
|
||||
isLoading={isLoading}
|
||||
setDetail={setDetail}
|
||||
setDetailOpen={setDetailOpen}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setEditingPlan={setEditingPlan}
|
||||
/>
|
||||
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(plan);
|
||||
setDetailOpen(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</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>
|
||||
<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>
|
||||
<DeleteTourPlan
|
||||
opneDelete={openDelete}
|
||||
planDelete={planDelete}
|
||||
setOpenDelete={setOpenDelete}
|
||||
setPlanDelete={setPlanDelete}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
135
src/features/tour-plan/ui/TourPlanTable.tsx
Normal file
135
src/features/tour-plan/ui/TourPlanTable.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
import type { PlanTourListDataRes } from "@/features/tour-plan/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 clsx from "clsx";
|
||||
import { Edit, Eye, Loader2, Trash } from "lucide-react";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
data: PlanTourListDataRes[];
|
||||
isLoading: boolean;
|
||||
isFetching: boolean;
|
||||
isError: boolean;
|
||||
setDetail: Dispatch<SetStateAction<PlanTourListDataRes | null>>;
|
||||
setDetailOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setEditingPlan: Dispatch<SetStateAction<PlanTourListDataRes | null>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
handleDelete: (paln: PlanTourListDataRes) => void;
|
||||
}
|
||||
|
||||
const TourPlanTable = ({
|
||||
data,
|
||||
isFetching,
|
||||
isError,
|
||||
isLoading,
|
||||
setDetail,
|
||||
setEditingPlan,
|
||||
handleDelete,
|
||||
setDialogOpen,
|
||||
setDetailOpen,
|
||||
}: Props) => {
|
||||
return (
|
||||
<div className="flex-1 overflow-auto">
|
||||
{(isLoading || isFetching) && (
|
||||
<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>
|
||||
)}{" "}
|
||||
{!isLoading && !isError && (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Kimga tegishli</TableHead>
|
||||
<TableHead className="text-start">Boriladigan joyi</TableHead>
|
||||
<TableHead className="text-start">Sanasi</TableHead>
|
||||
<TableHead className="text-start">Statusi</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.length > 0 ? (
|
||||
data.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>
|
||||
{plan.user.first_name + " " + plan.user.last_name}
|
||||
</TableCell>
|
||||
<TableCell>{plan.place_name}</TableCell>
|
||||
<TableCell>
|
||||
{plan.date && formatDate.format(plan.date, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={clsx(
|
||||
plan.location_send ? "text-green-500" : "text-red-500",
|
||||
)}
|
||||
>
|
||||
{plan.location_send ? "Borildi" : "Borilmagan"}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="flex gap-1 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(plan);
|
||||
setDetailOpen(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-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="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-4 text-lg">
|
||||
Tur plan topilmadi.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TourPlanTable;
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
UserUpdateReq,
|
||||
} from "@/features/users/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { USER } from "@/shared/config/api/URLs";
|
||||
import { API_URLS } from "@/shared/config/api/URLs";
|
||||
import axios, { type AxiosResponse } from "axios";
|
||||
|
||||
export const user_api = {
|
||||
@@ -16,27 +16,27 @@ export const user_api = {
|
||||
is_active?: boolean | string;
|
||||
region_id?: number;
|
||||
}): Promise<AxiosResponse<UserListRes>> {
|
||||
const res = await httpClient.get(`${USER}list/`, { params });
|
||||
const res = await httpClient.get(`${API_URLS.USER}list/`, { params });
|
||||
return res;
|
||||
},
|
||||
|
||||
async update({ body, id }: { id: number; body: UserUpdateReq }) {
|
||||
const res = await httpClient.patch(`${USER}${id}/update/`, body);
|
||||
const res = await httpClient.patch(`${API_URLS.USER}${id}/update/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: UserCreateReq) {
|
||||
const res = await httpClient.post(`${USER}create/`, body);
|
||||
const res = await httpClient.post(`${API_URLS.USER}create/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
async active(id: number) {
|
||||
const res = await httpClient.post(`${USER}${id}/activate/`);
|
||||
const res = await httpClient.post(`${API_URLS.USER}${id}/activate/`);
|
||||
return res;
|
||||
},
|
||||
|
||||
async delete({ id }: { id: number }) {
|
||||
const res = await httpClient.delete(`${USER}${id}/delete/`);
|
||||
const res = await httpClient.delete(`${API_URLS.USER}${id}/delete/`);
|
||||
return res;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
const BASE_URL =
|
||||
import.meta.env.VITE_API_URL || "https://api.meridynpharma.com";
|
||||
|
||||
const LOGIN = "/api/v1/authentication/admin_login/";
|
||||
const USER = "/api/v1/admin/user/";
|
||||
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,
|
||||
OBJECT,
|
||||
PHARMACIES,
|
||||
REGION,
|
||||
REGIONS,
|
||||
USER,
|
||||
export const API_URLS = {
|
||||
BASE_URL: import.meta.env.VITE_API_URL || "https://api.meridynpharma.com",
|
||||
LOGIN: "/api/v1/authentication/admin_login/",
|
||||
USER: "/api/v1/admin/user/",
|
||||
REGION: "/api/v1/admin/district/",
|
||||
REGIONS: "/api/v1/admin/region/",
|
||||
DISTRICT: "/api/v1/admin/district/",
|
||||
DOCTOR: "/api/v1/admin/doctor/",
|
||||
OBJECT: "/api/v1/admin/place/",
|
||||
PHARMACIES: "/api/v1/admin/pharmacy/",
|
||||
PLANS: "/api/v1/admin/plan/",
|
||||
PILL: "/api/v1/admin/product/",
|
||||
LOCATION: "/api/v1/admin/location/",
|
||||
USER_LOCATION: "/api/v1/admin/user_location/",
|
||||
ORDER: "/api/v1/admin/order/",
|
||||
FACTORY: "/api/v1/admin/factory/",
|
||||
REPORT: "/api/v1/admin/payment/",
|
||||
TOUR_PLAN: "/api/v1/admin/tour_plan/",
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import i18n from "@/shared/config/i18n";
|
||||
import { getToken } from "@/shared/lib/cookie";
|
||||
import axios from "axios";
|
||||
import { BASE_URL } from "./URLs";
|
||||
import { API_URLS } from "./URLs";
|
||||
|
||||
const httpClient = axios.create({
|
||||
baseURL: BASE_URL,
|
||||
baseURL: API_URLS.BASE_URL,
|
||||
timeout: 10000,
|
||||
});
|
||||
|
||||
|
||||
@@ -61,11 +61,6 @@ const items = [
|
||||
url: "/dashboard/pharmacies",
|
||||
icon: Hospital,
|
||||
},
|
||||
{
|
||||
title: "Hisobotlar",
|
||||
url: "/dashboard/reports",
|
||||
icon: ClipboardList,
|
||||
},
|
||||
{
|
||||
title: "Jo'natilgan joylar",
|
||||
url: "/dashboard/sent-locations",
|
||||
@@ -76,6 +71,11 @@ const items = [
|
||||
url: "/dashboard/specifications",
|
||||
icon: FileText,
|
||||
},
|
||||
{
|
||||
title: "Hisobotlar",
|
||||
url: "/dashboard/reports",
|
||||
icon: ClipboardList,
|
||||
},
|
||||
{
|
||||
title: "Tur planlar",
|
||||
url: "/dashboard/tour-plan",
|
||||
|
||||
Reference in New Issue
Block a user