update api request and response

This commit is contained in:
Samandar Turgunboyev
2025-12-05 17:49:55 +05:00
parent f7dbb665a0
commit 17b833dd88
23 changed files with 1155 additions and 36 deletions

View File

@@ -0,0 +1,17 @@
import type { DistributedList } from "@/features/distributed/lib/data";
import httpClient from "@/shared/config/api/httpClient";
import { API_URLS } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
export const distributed_api = {
async list(params: {
limit?: number;
offset?: number;
product?: string;
user?: string;
date?: string;
}): Promise<AxiosResponse<DistributedList>> {
const res = await httpClient.get(API_URLS.DISTRIBUTED, { params });
return res;
},
};

View File

@@ -0,0 +1,29 @@
export interface DistributedList {
status_code: number;
status: string;
message: string;
data: {
count: number;
next: null | string;
previous: null | string;
results: DistributedListData[];
};
}
export interface DistributedListData {
id: number;
product: {
id: number;
name: string;
price: number;
};
quantity: number;
employee_name: string;
user: {
id: number;
first_name: string;
last_name: string;
};
created_at: string;
date: string;
}

View File

@@ -0,0 +1,199 @@
import { distributed_api } from "@/features/distributed/lib/api";
import type { DistributedListData } from "@/features/distributed/lib/data";
import { DistributedDetail } from "@/features/distributed/ui/SpecificationDetail ";
import formatDate from "@/shared/lib/formatDate";
import { Button } from "@/shared/ui/button";
import { Calendar } from "@/shared/ui/calendar";
import { Input } from "@/shared/ui/input";
import Pagination from "@/shared/ui/pagination";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { useQuery } from "@tanstack/react-query";
import { ChevronDownIcon, Eye, Loader2 } from "lucide-react";
import { useState } from "react";
const DistributedList = () => {
const [currentPage, setCurrentPage] = useState(1);
const [nameFilter, setNameFilter] = useState<string>("");
const limit = 20;
const [disctritFilter, setDisctritFilter] = useState<string>("");
const [openDate, setOpenDate] = useState<boolean>(false);
const [dateFilter, setDateFilter] = useState<Date | undefined>(undefined);
const [open, setOpen] = useState<boolean>(false);
const [supportDetail, setSupportDetail] =
useState<DistributedListData | null>(null);
const { data, isLoading, isError } = useQuery({
queryKey: [
"distributed_list",
currentPage,
nameFilter,
disctritFilter,
dateFilter,
],
queryFn: () =>
distributed_api.list({
limit,
offset: (currentPage - 1) * limit,
product: nameFilter,
user: disctritFilter,
date: dateFilter && formatDate.format(dateFilter, "YYYY-MM-DD"),
}),
select(data) {
return data.data.data;
},
});
const totalPages = data ? Math.ceil(data?.count / limit) : 1;
return (
<div className="flex flex-col h-full p-10 w-full">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<h1 className="text-2xl font-bold">Yordam so'rovlari ro'yxati</h1>
<div className="flex gap-2 mb-4">
<Input
type="text"
placeholder="Foydalanuvchi nomi"
className="h-12"
value={disctritFilter}
onChange={(e) => setDisctritFilter(e.target.value)}
/>
<Input
type="text"
placeholder="Mahsulot nomi"
className="h-12"
value={nameFilter}
onChange={(e) => setNameFilter(e.target.value)}
/>
<Popover open={openDate} onOpenChange={setOpenDate}>
<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);
setOpenDate(false);
}}
/>
<div className="p-2 border-t bg-white">
<Button
variant="outline"
className="w-full"
onClick={() => {
setDateFilter(undefined);
setOpenDate(false);
}}
>
Tozalash
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
<div className="flex-1 overflow-auto">
{isLoading && (
<div className="h-full flex items-center justify-center bg-white/70 z-10">
<span className="text-lg font-medium">
<Loader2 className="animate-spin" />
</span>
</div>
)}
{isError && (
<div className="h-full flex items-center justify-center z-10">
<span className="text-lg font-medium text-red-600">
Ma'lumotlarni olishda xatolik yuz berdi.
</span>
</div>
)}
{!isLoading && !isError && (
<Table>
<TableHeader>
<TableRow className="text-center">
<TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Kim qo'shgan</TableHead>
<TableHead className="text-start">Xaridorning ismi</TableHead>
<TableHead className="text-start">Mahsulot nomi</TableHead>
<TableHead className="text-start">Nechta berilgan</TableHead>
<TableHead className="text-start">Topshirilgan sana</TableHead>
<TableHead className="text-start">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data && data.results.length > 0 ? (
data?.results.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.employee_name}</TableCell>
<TableCell>{plan.product.name}</TableCell>
<TableCell>{plan.quantity}</TableCell>
<TableCell>
{formatDate.format(plan.date, "YYYY-MM-DD")}
</TableCell>
<TableCell>
<Button
className="bg-blue-500 hover:bg-blue-500 cursor-pointer"
onClick={() => {
setOpen(true);
setSupportDetail(plan);
}}
>
<Eye />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} className="text-center py-4 text-lg">
Farmasevtika topilmadi.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
/>
<DistributedDetail
open={open}
setOpen={setOpen}
specification={supportDetail}
/>
</div>
);
};
export default DistributedList;

View File

@@ -0,0 +1,92 @@
"use client";
import type { DistributedListData } from "@/features/distributed/lib/data";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import type { Dispatch, SetStateAction } from "react";
interface Props {
specification: DistributedListData | null;
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
}
export const DistributedDetail = ({ specification, open, setOpen }: Props) => {
if (!specification) return null;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader className="border-b pb-4">
<DialogTitle className="text-2xl font-bold text-gray-800">
Tafsilot
</DialogTitle>
</DialogHeader>
<div className="space-y-6 mt-6">
{/* Asosiy ma'lumotlar - Grid */}
<div className="grid grid-cols-1">
{/* Xaridor */}
<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">
Xaridorning ismi
</p>
<p className="text-lg font-semibold text-gray-800">
{specification.employee_name}
</p>
</div>
{/* Foydalanuvchi */}
<div className="bg-gradient-to-br mt-5 from-purple-50 to-purple-100 rounded-lg p-4 border border-purple-200 md:col-span-2">
<p className="text-sm text-purple-600 font-medium mb-1">
Mas'ul xodim
</p>
<p className="text-lg font-semibold text-gray-800">
{specification.user.first_name} {specification.user.last_name}
</p>
</div>
<div className="bg-gradient-to-br mt-5 from-green-50 to-green-100 rounded-lg p-4 border border-green-200 md:col-span-2">
<p className="text-sm text-green-600 font-medium mb-1">
Topshirilgan sanasi
</p>
<p className="text-lg font-semibold text-gray-800">
{specification.date}
</p>
</div>
</div>
{/* Dorilar ro'yxati */}
<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">
Topshirilgan dori
</h3>
<div className="space-y-3">
<div className="bg-white rounded-lg p-4 border border-gray-200 hover:border-indigo-300 transition-colors">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<p className="font-semibold text-gray-800">
{specification.product.name}
</p>
</div>
<div className="flex items-center gap-4 text-sm text-gray-600">
<span>
Miqdor: <strong>{specification.quantity} ta</strong>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -17,6 +17,7 @@ export const doctor_api = {
work_place?: string;
sphere?: string;
user?: string;
user_id?: number;
}): Promise<AxiosResponse<DoctorListRes>> {
const res = await httpClient.get(`${API_URLS.DOCTOR}list/`, { params });
return res;

View File

@@ -106,16 +106,19 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
return data.data.data;
},
});
const user_id = form.watch("user");
const { data: object, isLoading: isObjectLoading } = useQuery({
queryKey: ["object_list", searchUser, selectDiscrit],
queryKey: ["object_list", searchUser, selectDiscrit, user_id],
queryFn: () => {
const params: {
name?: string;
district?: string;
user_id?: number;
} = {
name: searchUser,
district: selectDiscrit,
user_id: Number(user_id),
};
return object_api.list(params);
@@ -125,8 +128,6 @@ const AddedDoctor = ({ initialValues, setDialogOpen }: Props) => {
},
});
const user_id = form.watch("user");
const { data: discrit, isLoading: discritLoading } = useQuery({
queryKey: ["discrit_list", searchDiscrit, user_id],
queryFn: () => {

View File

@@ -14,6 +14,7 @@ export const object_api = {
name?: string;
district?: string;
user?: string;
user_id?: number;
}): Promise<AxiosResponse<ObjectListRes>> {
const res = await httpClient.get(`${API_URLS.OBJECT}list/`, { params });
return res;

View File

@@ -15,6 +15,7 @@ export const pharmacies_api = {
place?: string;
district?: string;
user?: string;
user_id?: number;
}): Promise<AxiosResponse<PharmaciesListRes>> {
const res = await httpClient.get(`${API_URLS.PHARMACIES}list/`, { params });
return res;

View File

@@ -102,16 +102,19 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
return data.data.data;
},
});
const user_id = form.watch("user");
const { data: object, isLoading: isObjectLoading } = useQuery({
queryKey: ["object_list", searchUser, selectDiscrit],
queryKey: ["object_list", searchUser, selectDiscrit, user_id],
queryFn: () => {
const params: {
name?: string;
district?: string;
user_id?: number;
} = {
name: searchUser,
district: selectDiscrit,
user_id: Number(user_id),
};
return object_api.list(params);
@@ -121,8 +124,6 @@ const AddedPharmacies = ({ initialValues, setDialogOpen }: Props) => {
},
});
const user_id = form.watch("user");
const { data: discrit, isLoading: discritLoading } = useQuery({
queryKey: ["discrit_list", searchDiscrit, user_id],
queryFn: () => {

View File

@@ -26,12 +26,27 @@ export interface PlanListData {
title: string;
description: string;
date: string;
comment: null | string;
doctor: {
id: number;
first_name: string;
last_name: string;
} | null;
pharmacy: {
id: number;
name: string;
} | null;
user: {
id: number;
first_name: string;
last_name: string;
};
is_done: true;
longitude: number;
latitude: number;
extra_location: {
latitude: number;
longitude: number;
};
created_at: string;
}
@@ -40,10 +55,18 @@ export interface PlanCreateReq {
description: string;
date: string;
user_id: number;
doctor_id: number | null;
pharmacy_id: number | null;
longitude: number;
latitude: number;
extra_location: { longitude: number; latitude: number };
}
export interface PlanUpdateReq {
title: string;
description: string;
date: string;
longitude: number;
latitude: number;
extra_location: { longitude: number; latitude: number };
}

View File

@@ -5,4 +5,10 @@ export const createPlanFormData = z.object({
description: z.string().min(1, { message: "Majburiy maydon" }),
user: z.string().min(1, { message: "Majburiy maydon" }),
date: z.string().min(1, { message: "Majburiy maydon" }),
doctor_id: z.string().optional(),
pharmacy_id: z.string().optional(),
});
// longitude: number;
// latitude: number;
// extra_location: { longitude: number; latitude: number };

View File

@@ -1,3 +1,5 @@
import { doctor_api } from "@/features/doctors/lib/api";
import { pharmacies_api } from "@/features/pharmacies/lib/api";
import { plans_api } from "@/features/plans/lib/api";
import type {
PlanCreateReq,
@@ -53,8 +55,16 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
date: initialValues ? initialValues?.date : "",
},
});
const [type, setType] = useState<"DOCTOR" | "PHARM">("DOCTOR");
const [long, setLong] = useState<number>(41.233);
const [lat, setLat] = useState<number>(63.233);
const [searchUser, setSearchUser] = useState<string>("");
const [openUser, setOpenUser] = useState<boolean>(false);
const [searchDoctor, setSearchDoctor] = useState<string>("");
const [openDoctor, setOpenDoctor] = useState<boolean>(false);
const [searchPharm, setSearchPharm] = useState<string>("");
const [openPharm, setOpenPharm] = useState<boolean>(false);
const queryClient = useQueryClient();
const [open, setOpen] = useState(false);
@@ -79,6 +89,50 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
},
});
const user_id = form.watch("user");
const { data: doctor, isLoading: isDoctorLoading } = useQuery({
queryKey: ["doctor_list", searchDoctor, user_id],
queryFn: () => {
const params: {
limit?: number;
offset?: number;
user_id?: number;
full_name?: string;
} = {
limit: 8,
full_name: searchDoctor,
user_id: Number(user_id),
};
return doctor_api.list(params);
},
select(data) {
return data.data.data;
},
});
const { data: pharm, isLoading: isPharmLoading } = useQuery({
queryKey: ["pharm_list", searchPharm, user_id],
queryFn: () => {
const params: {
limit?: number;
offset?: number;
user_id?: number;
name?: string;
} = {
limit: 8,
name: searchPharm,
user_id: Number(user_id),
};
return pharmacies_api.list(params);
},
select(data) {
return data.data.data;
},
});
const { mutate, isPending } = useMutation({
mutationFn: (body: PlanCreateReq) => plans_api.create(body),
onSuccess: () => {
@@ -117,18 +171,32 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
function onSubmit(data: z.infer<typeof createPlanFormData>) {
if (initialValues) {
edit({
id: initialValues.id,
body: {
date: formatDate.format(data.date, "YYYY-MM-DD"),
description: data.description,
extra_location: {
latitude: initialValues.latitude,
longitude: initialValues.longitude,
},
latitude: initialValues.latitude,
longitude: initialValues.longitude,
title: data.name,
},
id: initialValues.id,
});
} else {
mutate({
date: formatDate.format(data.date, "YYYY-MM-DD"),
description: data.description,
extra_location: {
latitude: lat,
longitude: long,
},
latitude: lat,
longitude: long,
title: data.name,
doctor_id: data.doctor_id ? Number(data.doctor_id) : null,
pharmacy_id: data.pharmacy_id ? Number(data.pharmacy_id) : null,
user_id: Number(data.user),
});
}
@@ -223,6 +291,213 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
);
}}
/>
<div className="flex gap-2">
<Button
type="button"
onClick={() => setType("DOCTOR")}
className={cn(
"cursor-pointer h-10 text-sm",
type === "DOCTOR"
? "bg-blue-700 hover:bg-blue-700 text-white"
: "bg-gray-300 hover:bg-gray-300 text-black/80",
)}
>
Shifokorga birlashtirish
</Button>
<Button
onClick={() => setType("PHARM")}
type="button"
className={cn(
"cursor-pointer h-10 text-sm",
type === "PHARM"
? "bg-blue-700 hover:bg-blue-700 text-white"
: "bg-gray-300 hover:bg-gray-300 text-black/80",
)}
>
Dorixonaga birlashtirish
</Button>
</div>
{type === "DOCTOR" && (
<FormField
name="doctor_id"
control={form.control}
render={({ field }) => {
const selectedUser = doctor?.results.find(
(u) => String(u.id) === field.value,
);
return (
<FormItem className="flex flex-col">
<Label className="text-md">Shifokorlar</Label>
<Popover open={openDoctor} onOpenChange={setOpenDoctor}>
<PopoverTrigger asChild disabled={initialValues !== null}>
<FormControl>
<Button
type="button"
variant="outline"
role="combobox"
aria-expanded={openDoctor}
className={cn(
"w-full h-12 justify-between",
!field.value && "text-muted-foreground",
)}
>
{selectedUser
? `${selectedUser.first_name} ${selectedUser.last_name}`
: "Shifokorni 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={searchDoctor}
onValueChange={setSearchDoctor}
/>
<CommandList>
{isDoctorLoading ? (
<div className="py-6 text-center text-sm">
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
</div>
) : doctor && doctor.results.length > 0 ? (
<CommandGroup>
{doctor.results.map((u) => (
<CommandItem
key={u.id}
value={`${u.id}`}
onSelect={() => {
field.onChange(String(u.id));
setOpenDoctor(false);
setLat(u.latitude);
setLong(u.longitude);
form.setValue("pharmacy_id", undefined);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
field.value === String(u.id)
? "opacity-100"
: "opacity-0",
)}
/>
{u.first_name} {u.last_name}
</CommandItem>
))}
</CommandGroup>
) : (
<CommandEmpty>Shifokor topilmadi</CommandEmpty>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
);
}}
/>
)}
{type === "PHARM" && (
<FormField
name="pharmacy_id"
control={form.control}
render={({ field }) => {
const selectedUser = pharm?.results.find(
(u) => String(u.id) === field.value,
);
return (
<FormItem className="flex flex-col">
<Label className="text-md">Dorixonalar</Label>
<Popover open={openPharm} onOpenChange={setOpenPharm}>
<PopoverTrigger asChild disabled={initialValues !== null}>
<FormControl>
<Button
type="button"
variant="outline"
role="combobox"
aria-expanded={openPharm}
className={cn(
"w-full h-12 justify-between",
!field.value && "text-muted-foreground",
)}
>
{selectedUser
? `${selectedUser.name}`
: "Dorixonani 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={searchPharm}
onValueChange={setSearchPharm}
/>
<CommandList>
{isPharmLoading ? (
<div className="py-6 text-center text-sm">
<Loader2 className="mx-auto h-4 w-4 animate-spin" />
</div>
) : pharm && pharm.results.length > 0 ? (
<CommandGroup>
{pharm.results.map((u) => (
<CommandItem
key={u.id}
value={`${u.id}`}
onSelect={() => {
field.onChange(String(u.id));
setOpenPharm(false);
setLat(u.latitude);
setLong(u.longitude);
form.setValue("doctor_id", undefined);
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
field.value === String(u.id)
? "opacity-100"
: "opacity-0",
)}
/>
{u.name}
</CommandItem>
))}
</CommandGroup>
) : (
<CommandEmpty>Dorixona topilmadi</CommandEmpty>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
);
}}
/>
)}
<FormField
control={form.control}
@@ -307,7 +582,9 @@ const AddedPlan = ({ initialValues, setDialogOpen }: Props) => {
<Button
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
disabled={isPending || editPending || initialValues?.is_done}
disabled={
isPending || editPending || initialValues?.comment ? true : false
}
>
{isPending || editPending ? (
<Loader2 className="animate-spin" />

View File

@@ -59,6 +59,10 @@ const PalanTable = ({
<TableHead className="text-start">Reja nomi</TableHead>
<TableHead className="text-start">Tavsifi</TableHead>
<TableHead className="text-start">Kimga tegishli</TableHead>
<TableHead className="text-start">Shifokor biriktirgan</TableHead>
<TableHead className="text-start">
Dorixonaga biriktirgan
</TableHead>
<TableHead className="text-start">Status</TableHead>
<TableHead className="text-right">Harakatlar</TableHead>
</TableRow>
@@ -72,12 +76,20 @@ const PalanTable = ({
<TableCell>
{plan.user.first_name + " " + plan.user.last_name}
</TableCell>
<TableCell>
{plan.doctor
? plan.doctor.first_name + " " + plan.doctor.last_name
: "-"}
</TableCell>
<TableCell>
{plan.pharmacy ? plan.pharmacy.name : "-"}
</TableCell>
<TableCell
className={clsx(
plan.is_done ? "text-green-500" : "text-red-500",
plan.comment ? "text-green-500" : "text-red-500",
)}
>
{plan.is_done ? "Bajarilgan" : "Bajarilmagan"}
{plan.comment ? "Bajarilgan" : "Bajarilmagan"}
</TableCell>
<TableCell className="flex gap-2 justify-end">
<Button
@@ -94,7 +106,7 @@ const PalanTable = ({
<Button
variant="outline"
size="sm"
disabled={plan.is_done}
disabled={plan.comment ? true : false}
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
onClick={() => {
setEditingPlan(plan);
@@ -107,7 +119,7 @@ const PalanTable = ({
variant="destructive"
size="sm"
className="cursor-pointer"
disabled={plan.is_done}
disabled={plan.comment ? true : false}
onClick={() => handleDelete(plan)}
>
<Trash className="h-4 w-4" />

View File

@@ -8,6 +8,13 @@ import {
} from "@/shared/ui/dialog";
import { Badge } from "@/shared/ui/badge";
import {
Circle,
Map,
Placemark,
YMaps,
ZoomControl,
} from "@pbe/react-yandex-maps";
import clsx from "clsx";
import { type Dispatch, type SetStateAction } from "react";
@@ -22,7 +29,7 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
return (
<Dialog open={detail} onOpenChange={setDetail}>
<DialogContent className="sm:max-w-lg">
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-xl font-semibold">
Reja haqida batafsil
@@ -30,19 +37,47 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
</DialogHeader>
<DialogDescription className="space-y-4 mt-2 text-md">
{/* Reja nomi */}
<div>
<p className="font-semibold text-gray-900">Reja nomi:</p>
<p>{plan.title}</p>
</div>
{/* Reja tavsifi */}
<div>
<p className="font-semibold text-gray-900">Tavsifi:</p>
<p>{plan.description}</p>
</div>
{/* Kimga tegishli */}
{plan.comment && (
<div>
<p className="font-semibold text-gray-900">Qanday bajarildi:</p>
<p>{plan.comment}</p>
</div>
)}
{plan.doctor && (
<div>
<p className="font-semibold text-gray-900">
Shikorga biriktirgan
</p>
<p>
<span>Shifokot ismi: </span>
{plan.doctor.first_name} {plan.doctor.last_name}
</p>
</div>
)}
{plan.pharmacy && (
<div>
<p className="font-semibold text-gray-900">
Dorixonaga biriktirgan
</p>
<p>
<span>Dorixona nomi: </span>
{plan.pharmacy.name}
</p>
</div>
)}
<div>
<p className="font-semibold text-gray-900">Kimga tegishli:</p>
<p>
@@ -50,21 +85,56 @@ const PlanDetail = ({ detail, setDetail, plan }: Props) => {
</p>
</div>
{/* Reja statusi */}
<div>
<p className="font-semibold text-gray-900">Reja statusi:</p>
<Badge
className={clsx(
plan.is_done
plan.comment
? "bg-green-100 text-green-700"
: "bg-yellow-100 text-yellow-700",
"text-sm px-4 py-2 mt-2",
)}
>
{plan.is_done ? "Bajarilgan" : "Bajarilmagan"}
{plan.comment ? "Bajarilgan" : "Bajarilmagan"}
</Badge>
</div>
<div>
{plan.doctor
? "Shifokor manzili:"
: plan.pharmacy && "Dorixona manzili:"}
</div>
<YMaps query={{ lang: "en_RU" }}>
<div className="h-[300px] w-full rounded-md overflow-hidden">
<Map
state={{
center: [plan.latitude, plan.longitude],
zoom: 12,
}}
width="100%"
height="100%"
>
<ZoomControl
options={{
position: { right: "10px", bottom: "70px" },
}}
/>
<Placemark geometry={[plan.latitude, plan.longitude]} />
<Circle
geometry={[[plan.latitude, plan.longitude], 300]}
options={{
fillColor: "rgba(255, 100, 0, 0.3)",
strokeColor: "rgba(255, 100, 0, 0.8)",
strokeWidth: 2,
}}
/>
</Map>
</div>
</YMaps>
</DialogDescription>
</DialogContent>
</Dialog>

View File

@@ -0,0 +1,18 @@
import type { SupportListRes } from "@/features/support/lib/data";
import httpClient from "@/shared/config/api/httpClient";
import { API_URLS } from "@/shared/config/api/URLs";
import type { AxiosResponse } from "axios";
export const support_api = {
async list(params: {
limit?: number;
offset?: number;
problem?: string;
district?: string;
user?: string;
date?: string;
}): Promise<AxiosResponse<SupportListRes>> {
const res = await httpClient.get(API_URLS.SUPPORT, { params });
return res;
},
};

View File

@@ -0,0 +1,27 @@
export interface SupportListRes {
status_code: number;
status: string;
message: string;
data: {
count: number;
next: null | string;
previous: null | string;
results: SupportListData[];
};
}
export interface SupportListData {
id: number;
problem: string;
date: string;
type: "PROBLEM" | "HELP";
district: {
id: number;
name: string;
} | null;
user: {
id: number;
first_name: string;
last_name: string;
};
created_at: string;
}

View File

@@ -0,0 +1,70 @@
import type { SupportListData } from "@/features/support/lib/data";
import { Button } from "@/shared/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/shared/ui/dialog";
import type { Dispatch, SetStateAction } from "react";
const SupportDetail = ({
open,
setOpen,
supportDetail,
}: {
open: boolean;
setOpen: Dispatch<SetStateAction<boolean>>;
supportDetail: SupportListData | null;
}) => {
if (supportDetail === null) return null;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Batafsil</DialogTitle>
<DialogDescription className="mt-5 flex flex-col gap-2">
<p className="text-black text-lg font-medium">
<span className="text-foreground">Kim jo'natgan:</span>{" "}
{supportDetail?.user.first_name} {supportDetail?.user.last_name}
</p>
{supportDetail?.district && (
<p className="text-black text-lg font-medium">
<span className="text-foreground">Tuman nomi:</span>{" "}
{supportDetail?.district?.name}
</p>
)}
<p className="text-black text-lg font-medium">
<span className="text-foreground">Jo'natilgan vaqti:</span>{" "}
{supportDetail?.date}
</p>
<p className="text-black text-lg font-medium">
<span className="text-foreground">Xabar turi:</span>{" "}
{supportDetail?.type === "PROBLEM"
? "Muommo hal qilish"
: "Yordam so'rash"}
</p>
<p className="text-black text-lg font-medium">
<span className="text-foreground">Xabar tavsifi:</span>{" "}
{supportDetail?.problem}
</p>
</DialogDescription>
</DialogHeader>
<DialogFooter className="mt-5">
<Button
className="bg-blue-500 hover:bg-blue-500 cursor-pointer w-32 h-12 text-md"
onClick={() => {
setOpen(false);
}}
>
Yopish
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
export default SupportDetail;

View File

@@ -0,0 +1,225 @@
import { support_api } from "@/features/support/lib/api";
import type { SupportListData } from "@/features/support/lib/data";
import SupportDetail from "@/features/support/ui/SupportDetail";
import formatDate from "@/shared/lib/formatDate";
import { Button } from "@/shared/ui/button";
import { Calendar } from "@/shared/ui/calendar";
import { Input } from "@/shared/ui/input";
import Pagination from "@/shared/ui/pagination";
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/shared/ui/table";
import { useQuery } from "@tanstack/react-query";
import { ChevronDownIcon, Eye, Loader2 } from "lucide-react";
import { useEffect, useState } from "react";
const SupportList = () => {
const [currentPage, setCurrentPage] = useState(1);
const [nameFilter, setNameFilter] = useState<string>("");
const [disctritFilter, setDisctritFilter] = useState<string | null>(null);
const [open, setOpen] = useState<boolean>(false);
const [openDate, setOpenDate] = useState<boolean>(false);
const [supportDetail, setSupportDetail] = useState<SupportListData | null>(
null,
);
const [dateFilter, setDateFilter] = useState<Date | undefined>(undefined);
useEffect(() => {
if (disctritFilter?.length === 0) {
setDisctritFilter(null);
}
}, [disctritFilter]);
const limit = 20;
const { data, isLoading, isError } = useQuery({
queryKey: [
"factory_list",
currentPage,
nameFilter,
disctritFilter,
dateFilter,
],
queryFn: () => {
const params: {
limit?: number;
offset?: number;
problem?: string;
district?: string;
user?: string;
date?: string;
} = {
limit: limit,
offset: (currentPage - 1) * limit,
user: nameFilter,
};
if (disctritFilter !== null) {
params.district = disctritFilter;
}
if (dateFilter) {
params.date = formatDate.format(dateFilter, "YYYY-MM-DD");
}
return support_api.list(params);
},
select(data) {
return data.data.data;
},
});
const totalPages = data ? Math.ceil(data?.count / limit) : 1;
return (
<div className="flex flex-col h-full p-10 w-full">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
<h1 className="text-2xl font-bold">Yordam so'rovlari ro'yxati</h1>
<div className="flex gap-2 mb-4">
<Input
type="text"
placeholder="Foydalanuvchi nomi"
className="h-12"
value={nameFilter}
onChange={(e) => setNameFilter(e.target.value)}
/>
<Input
type="text"
placeholder="Tuman nomi"
className="h-12"
value={disctritFilter ?? ""}
onChange={(e) => setDisctritFilter(e.target.value)}
/>
<Popover open={openDate} onOpenChange={setOpenDate}>
<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);
setOpenDate(false);
}}
/>
<div className="p-2 border-t bg-white">
<Button
variant="outline"
className="w-full"
onClick={() => {
setDateFilter(undefined);
setOpenDate(false);
}}
>
Tozalash
</Button>
</div>
</PopoverContent>
</Popover>
</div>
</div>
<div className="flex-1 overflow-auto">
{isLoading && (
<div className="h-full flex items-center justify-center bg-white/70 z-10">
<span className="text-lg font-medium">
<Loader2 className="animate-spin" />
</span>
</div>
)}
{isError && (
<div className="h-full flex items-center justify-center z-10">
<span className="text-lg font-medium text-red-600">
Ma'lumotlarni olishda xatolik yuz berdi.
</span>
</div>
)}
{!isLoading && !isError && (
<Table>
<TableHeader>
<TableRow className="text-center">
<TableHead className="text-start">ID</TableHead>
<TableHead className="text-start">Kim jo'natgan</TableHead>
<TableHead className="text-start">Habar haqida</TableHead>
<TableHead className="text-start">Habar turi</TableHead>
<TableHead className="text-start">Tuman</TableHead>
<TableHead className="text-start">Jo'natilgan sanasi</TableHead>
<TableHead className="text-start">Amallar</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data && data.results.length > 0 ? (
data?.results.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.problem.slice(0, 50)}...</TableCell>
<TableCell>
{plan.type === "PROBLEM"
? "Muommo hal qilish"
: "Yordam so'rash"}
</TableCell>
<TableCell>
{plan.district ? plan.district.name : "-"}
</TableCell>
<TableCell>{plan.date}</TableCell>
<TableCell>
<Button
className="bg-blue-500 hover:bg-blue-500 cursor-pointer"
onClick={() => {
setOpen(true);
setSupportDetail(plan);
}}
>
<Eye />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={7} className="text-center py-4 text-lg">
Farmasevtika topilmadi.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)}
</div>
<Pagination
currentPage={currentPage}
setCurrentPage={setCurrentPage}
totalPages={totalPages}
/>
<SupportDetail
open={open}
setOpen={setOpen}
supportDetail={supportDetail}
/>
</div>
);
};
export default SupportList;

12
src/pages/Distributed.tsx Normal file
View File

@@ -0,0 +1,12 @@
import DistributedList from "@/features/distributed/ui/DistributedList";
import SidebarLayout from "@/SidebarLayout";
const Distributed = () => {
return (
<SidebarLayout>
<DistributedList />
</SidebarLayout>
);
};
export default Distributed;

12
src/pages/Support.tsx Normal file
View File

@@ -0,0 +1,12 @@
import SupportList from "@/features/support/ui/SupportList";
import SidebarLayout from "@/SidebarLayout";
const Support = () => {
return (
<SidebarLayout>
<SupportList />
</SidebarLayout>
);
};
export default Support;

View File

@@ -1,4 +1,5 @@
import LoginLayout from "@/LoginLayout";
import Distributed from "@/pages/Distributed";
import Districts from "@/pages/Districts";
import Doctors from "@/pages/Doctors";
import UsersPage from "@/pages/Home";
@@ -11,6 +12,7 @@ import Region from "@/pages/Region";
import Reports from "@/pages/Reports";
import SentLocations from "@/pages/SentLocations";
import Specifications from "@/pages/Specifications";
import Support from "@/pages/Support";
import TourPlan from "@/pages/TourPlan";
import routesConfig from "@/providers/routing/config";
import { Navigate, useRoutes } from "react-router-dom";
@@ -78,6 +80,14 @@ const AppRouter = () => {
path: "/dashboard/region",
element: <Region />,
},
{
path: "/dashboard/support",
element: <Support />,
},
{
path: "/dashboard/distributed-product/",
element: <Distributed />,
},
]);
return routes;

View File

@@ -1,19 +1,23 @@
const API_V = "/api/v1/";
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/",
LOGIN: `${API_V}authentication/admin_login/`,
USER: `${API_V}admin/user/`,
REGION: `${API_V}admin/district/`,
REGIONS: `${API_V}admin/region/`,
DISTRICT: `${API_V}admin/district/`,
DOCTOR: `${API_V}admin/doctor/`,
OBJECT: `${API_V}admin/place/`,
PHARMACIES: `${API_V}admin/pharmacy/`,
PLANS: `${API_V}admin/plan/`,
PILL: `${API_V}admin/product/`,
LOCATION: `${API_V}admin/location/`,
USER_LOCATION: `${API_V}admin/user_location/`,
ORDER: `${API_V}admin/order/`,
FACTORY: `${API_V}admin/factory/`,
REPORT: `${API_V}admin/payment/`,
TOUR_PLAN: `${API_V}admin/tour_plan/`,
SUPPORT: `${API_V}admin/support/list/`,
DISTRIBUTED: `${API_V}admin/distributed_product/list/`,
};

View File

@@ -1,4 +1,5 @@
import {
AlertCircle,
BriefcaseMedical,
Building2,
Calendar,
@@ -96,6 +97,16 @@ const items = [
url: "/dashboard/pharmaceuticals",
icon: Microscope,
},
{
title: "Yordam so'rovlari",
url: "/dashboard/support",
icon: AlertCircle,
},
{
title: "Tarqatilgan dorilar",
url: "/dashboard/distributed-product",
icon: Pill,
},
];
export function AppSidebar() {