first commit
This commit is contained in:
25
src/features/districts/lib/data.ts
Normal file
25
src/features/districts/lib/data.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { FakeUserList, type User } from "@/features/users/lib/data";
|
||||
|
||||
export interface District {
|
||||
id: number;
|
||||
name: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export const fakeDistrict: District[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Chilonzor",
|
||||
user: FakeUserList[0],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Yunusobod",
|
||||
user: FakeUserList[1],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Urgut",
|
||||
user: FakeUserList[2],
|
||||
},
|
||||
];
|
||||
6
src/features/districts/lib/form.ts
Normal file
6
src/features/districts/lib/form.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import z from "zod";
|
||||
|
||||
export const addDistrict = z.object({
|
||||
name: z.string().min(2, "Tuman nomi kamida 2 ta harf bo‘lishi kerak"),
|
||||
userId: z.string().min(1, "Foydalanuvchini tanlang"),
|
||||
});
|
||||
147
src/features/districts/ui/AddDistrict.tsx
Normal file
147
src/features/districts/ui/AddDistrict.tsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import type { District } from "@/features/districts/lib/data";
|
||||
import { addDistrict } from "@/features/districts/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} 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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import z from "zod";
|
||||
|
||||
type FormValues = z.infer<typeof addDistrict>;
|
||||
|
||||
interface Props {
|
||||
initialValues: District | null;
|
||||
setDistricts: Dispatch<SetStateAction<District[]>>;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export default function AddDistrict({
|
||||
initialValues,
|
||||
setDistricts,
|
||||
setDialogOpen,
|
||||
}: Props) {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
const form = useForm<FormValues>({
|
||||
resolver: zodResolver(addDistrict),
|
||||
defaultValues: {
|
||||
name: initialValues?.name ?? "",
|
||||
userId: initialValues ? String(initialValues.user.id) : "",
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: FormValues) {
|
||||
const selectedUser = FakeUserList.find(
|
||||
(u) => u.id === Number(values.userId),
|
||||
);
|
||||
|
||||
if (!selectedUser) return;
|
||||
setLoad(true);
|
||||
if (initialValues) {
|
||||
setTimeout(() => {
|
||||
setDistricts((prev) =>
|
||||
prev.map((d) =>
|
||||
d.id === initialValues.id
|
||||
? {
|
||||
...d,
|
||||
name: values.name,
|
||||
user: selectedUser,
|
||||
}
|
||||
: d,
|
||||
),
|
||||
);
|
||||
setDialogOpen(false);
|
||||
setLoad(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setDistricts((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length ? prev[prev.length - 1].id + 1 : 1,
|
||||
name: values.name,
|
||||
user: selectedUser,
|
||||
},
|
||||
]);
|
||||
setDialogOpen(false);
|
||||
setLoad(false);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
name="name"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Tuman nomi</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="Chilonzor tumani"
|
||||
className="h-12 text-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
name="userId"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Kim qo‘shgan</Label>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchi tanlang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FakeUserList.map((u) => (
|
||||
<SelectItem key={u.id} value={String(u.id)}>
|
||||
{u.firstName} {u.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* SUBMIT */}
|
||||
<Button className="w-full h-12 bg-blue-700 hover:bg-blue-700 cursor-pointer">
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Qo‘shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
196
src/features/districts/ui/DistrictsList.tsx
Normal file
196
src/features/districts/ui/DistrictsList.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
import { fakeDistrict, type District } from "@/features/districts/lib/data";
|
||||
import AddDistrict from "@/features/districts/ui/AddDistrict";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const DistrictsList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
|
||||
const [districts, setDistricts] = useState<District[]>(fakeDistrict);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [userSearch, setUserSearch] = useState("");
|
||||
|
||||
const [editing, setEditing] = useState<District | null>(null);
|
||||
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const filtered = districts.filter((d) => {
|
||||
return (
|
||||
d.name.toLowerCase().includes(search.toLowerCase()) &&
|
||||
`${d.user.firstName} ${d.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(userSearch.toLowerCase())
|
||||
);
|
||||
});
|
||||
|
||||
function deleteDistrict(id: number) {
|
||||
setDistricts((prev) => prev.filter((d) => d.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">Tumanlar ro‘yxati</h1>
|
||||
|
||||
<div className="flex gap-4 justify-end">
|
||||
<Input
|
||||
placeholder="Tuman nomi bo‘yicha qidirish..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="max-w-sm h-12"
|
||||
/>
|
||||
|
||||
<Input
|
||||
placeholder="Foydalanuvchi bo‘yicha qidirish..."
|
||||
value={userSearch}
|
||||
onChange={(e) => setUserSearch(e.target.value)}
|
||||
className="max-w-sm h-12"
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
onClick={() => setEditing(null)}
|
||||
className="bg-blue-500 h-12 cursor-pointer hover:bg-blue-500"
|
||||
>
|
||||
<Plus className="w-5 h-5 mr-1" /> Tuman qo‘shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editing ? "Tumanni tahrirlash" : "Yangi tuman qo‘shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddDistrict
|
||||
initialValues={editing}
|
||||
setDistricts={setDistricts}
|
||||
setDialogOpen={setDialogOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>Tuman nomi</TableHead>
|
||||
<TableHead>Kim qo‘shgan</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{filtered.map((d) => (
|
||||
<TableRow key={d.id}>
|
||||
<TableCell>{d.id}</TableCell>
|
||||
<TableCell>{d.name}</TableCell>
|
||||
<TableCell>
|
||||
{d.user.firstName} {d.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditing(d);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
className="bg-blue-500 text-white hover:text-white hover:bg-blue-500 cursor-pointer"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => deleteDistrict(d.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Trash className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
{filtered.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center py-6">
|
||||
Hech qanday tuman 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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistrictsList;
|
||||
52
src/features/doctors/lib/data.ts
Normal file
52
src/features/doctors/lib/data.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { fakeDistrict, type District } from "@/features/districts/lib/data";
|
||||
import {
|
||||
ObjectListData,
|
||||
type ObjectListType,
|
||||
} from "@/features/objects/lib/data";
|
||||
import { FakeUserList, type User } from "@/features/users/lib/data";
|
||||
|
||||
export interface DoctorListType {
|
||||
id: number;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
phone_number: string;
|
||||
work: string;
|
||||
spec: string;
|
||||
desc: string;
|
||||
district: District;
|
||||
user: User;
|
||||
object: ObjectListType;
|
||||
long: string;
|
||||
lat: string;
|
||||
}
|
||||
|
||||
export const doctorListData: DoctorListType[] = [
|
||||
{
|
||||
id: 1,
|
||||
first_name: "Ali",
|
||||
last_name: "Valiyev",
|
||||
phone_number: "+998901234567",
|
||||
work: "Toshkent Shifoxonasi",
|
||||
spec: "Kardiolog",
|
||||
desc: "Malakali kardiolog, 10 yillik tajribaga ega",
|
||||
district: fakeDistrict[0],
|
||||
user: FakeUserList[0],
|
||||
object: ObjectListData[0],
|
||||
lat: ObjectListData[0].lat,
|
||||
long: ObjectListData[0].long,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
first_name: "Madina",
|
||||
last_name: "Karimova",
|
||||
phone_number: "+998901112233",
|
||||
work: "Yunusobod Poliklinikasi",
|
||||
spec: "Pediatr",
|
||||
desc: "Bolalar shifokori, 7 yillik ish tajribasi mavjud",
|
||||
district: fakeDistrict[1],
|
||||
user: FakeUserList[1],
|
||||
object: ObjectListData[1],
|
||||
lat: ObjectListData[1].lat,
|
||||
long: ObjectListData[1].long,
|
||||
},
|
||||
];
|
||||
15
src/features/doctors/lib/form.ts
Normal file
15
src/features/doctors/lib/form.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import z from "zod";
|
||||
|
||||
export const DoctorForm = z.object({
|
||||
first_name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
last_name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
phone_number: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
work: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
spec: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
desc: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
district: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
object: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
long: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
lat: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
306
src/features/doctors/ui/AddedDoctor.tsx
Normal file
306
src/features/doctors/ui/AddedDoctor.tsx
Normal file
@@ -0,0 +1,306 @@
|
||||
import { fakeDistrict } from "@/features/districts/lib/data";
|
||||
import type { DoctorListType } from "@/features/doctors/lib/data";
|
||||
import { DoctorForm } from "@/features/doctors/lib/form";
|
||||
import { ObjectListData } from "@/features/objects/lib/data";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} 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 { Textarea } from "@/shared/ui/textarea";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: DoctorListType | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setData: Dispatch<SetStateAction<DoctorListType[]>>;
|
||||
}
|
||||
|
||||
const AddedDoctor = ({ initialValues, setData, setDialogOpen }: Props) => {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof DoctorForm>>({
|
||||
resolver: zodResolver(DoctorForm),
|
||||
defaultValues: {
|
||||
desc: initialValues?.desc || "",
|
||||
district: initialValues?.district.id.toString() || "",
|
||||
first_name: initialValues?.first_name || "",
|
||||
last_name: initialValues?.last_name || "",
|
||||
lat: initialValues?.lat || "41.2949",
|
||||
long: initialValues?.long || "69.2361",
|
||||
object: initialValues?.object.id.toString() || "",
|
||||
phone_number: initialValues?.phone_number || "+998",
|
||||
spec: initialValues?.spec || "",
|
||||
work: initialValues?.work || "",
|
||||
user: initialValues?.user.id.toString() || "",
|
||||
},
|
||||
});
|
||||
|
||||
const lat = form.watch("lat");
|
||||
const long = form.watch("long");
|
||||
|
||||
const handleMapClick = (e: { get: (key: string) => number[] }) => {
|
||||
const coords = e.get("coords");
|
||||
form.setValue("lat", coords[0].toString());
|
||||
form.setValue("long", coords[1].toString());
|
||||
};
|
||||
|
||||
function onSubmit(values: z.infer<typeof DoctorForm>) {
|
||||
setLoad(true);
|
||||
const newObject: DoctorListType = {
|
||||
id: initialValues ? initialValues.id : Date.now(),
|
||||
user: FakeUserList.find((u) => u.id === Number(values.user))!,
|
||||
district: fakeDistrict.find((d) => d.id === Number(values.district))!,
|
||||
desc: values.desc,
|
||||
first_name: values.first_name,
|
||||
last_name: values.last_name,
|
||||
lat: values.lat,
|
||||
long: values.long,
|
||||
object: ObjectListData.find((d) => d.id === Number(values.object))!,
|
||||
phone_number: values.phone_number,
|
||||
spec: values.spec,
|
||||
work: values.work,
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setData((prev) => {
|
||||
if (initialValues) {
|
||||
return prev.map((item) =>
|
||||
item.id === initialValues.id ? newObject : item,
|
||||
);
|
||||
} else {
|
||||
return [...prev, newObject];
|
||||
}
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="first_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Ism</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Ismi" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="last_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Familiya</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Familiyasi" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone_number"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Telefon raqami</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="+998 90 123-45-67"
|
||||
{...field}
|
||||
value={formatPhone(field.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="work"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Ish joyi</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="114-poliklinika" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="spec"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Sohasi</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Kardiolog" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="desc"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Tavsif</Label>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Tavsif"
|
||||
{...field}
|
||||
className="min-h-32 max-h-52"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="district"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Tuman</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Tumanlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fakeDistrict.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="object"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Obyekt</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Obyekt" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ObjectListData.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Foydalanuvchi</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchilar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FakeUserList.map((e) => (
|
||||
<SelectItem value={String(e.id)}>
|
||||
{e.firstName} {e.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{ center: [Number(lat), Number(long)], zoom: 16 }}
|
||||
width="100%"
|
||||
height="300px"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[Number(lat), Number(long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(lat), Number(long)], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0,150,255,0.2)",
|
||||
strokeColor: "rgba(0,150,255,0.8)",
|
||||
strokeWidth: 2,
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||
type="submit"
|
||||
>
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddedDoctor;
|
||||
94
src/features/doctors/ui/DoctorDetailDialog.tsx
Normal file
94
src/features/doctors/ui/DoctorDetailDialog.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { DoctorListType } from "@/features/doctors/lib/data";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
|
||||
interface Props {
|
||||
detail: boolean;
|
||||
setDetail: (open: boolean) => void;
|
||||
object: DoctorListType | null;
|
||||
}
|
||||
|
||||
const DoctorDetailDialog = ({ detail, setDetail, object }: Props) => {
|
||||
if (!object) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={detail} onOpenChange={setDetail}>
|
||||
<DialogContent className="max-w-lg w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Shifokor tafsilotlari</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 mt-2">
|
||||
<p>
|
||||
<span className="font-semibold">Ism Familiya:</span>{" "}
|
||||
{object.first_name} {object.last_name}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Telefon:</span>{" "}
|
||||
{formatPhone(object.phone_number)}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Ish joyi:</span> {object.work}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Mutaxassislik:</span> {object.spec}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Tavsif:</span> {object.desc}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Tuman:</span> {object.district.name}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Foydalanuvchi:</span>{" "}
|
||||
{object.user.firstName} {object.user.lastName}
|
||||
</p>
|
||||
<p>
|
||||
<span className="font-semibold">Obyekt:</span> {object.object.name}
|
||||
</p>
|
||||
<span className="font-semibold">Manzili:</span>
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(object.lat), Number(object.long)],
|
||||
zoom: 16,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
>
|
||||
<Placemark
|
||||
geometry={[Number(object.lat), Number(object.long)]}
|
||||
/>
|
||||
<Circle
|
||||
geometry={[[Number(object.lat), Number(object.long)], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogClose asChild>
|
||||
<Button className="mt-4 w-full bg-blue-600 cursor-pointer hover:bg-blue-600">
|
||||
Yopish
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DoctorDetailDialog;
|
||||
292
src/features/doctors/ui/DoctorsList.tsx
Normal file
292
src/features/doctors/ui/DoctorsList.tsx
Normal file
@@ -0,0 +1,292 @@
|
||||
import {
|
||||
doctorListData,
|
||||
type DoctorListType,
|
||||
} from "@/features/doctors/lib/data";
|
||||
import AddedDoctor from "@/features/doctors/ui/AddedDoctor";
|
||||
import DoctorDetailDialog from "@/features/doctors/ui/DoctorDetailDialog";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const DoctorsList = () => {
|
||||
const [data, setData] = useState<DoctorListType[]>(doctorListData);
|
||||
const [detail, setDetail] = useState<DoctorListType | null>(null);
|
||||
const [detailDialog, setDetailDialog] = useState<boolean>(false);
|
||||
const [editingPlan, setEditingPlan] = useState<DoctorListType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
|
||||
// Filter states
|
||||
const [searchName, setSearchName] = useState("");
|
||||
const [searchDistrict, setSearchDistrict] = useState("");
|
||||
const [searchObject, setSearchObject] = useState("");
|
||||
const [searchWork, setSearchWork] = useState("");
|
||||
const [searchSpec, setSearchSpec] = useState("");
|
||||
const [searchUser, setSearchUser] = useState("");
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setData((prev) => prev.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
// Filtered data
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter((item) => {
|
||||
const nameMatch = `${item.first_name} ${item.last_name}`
|
||||
.toLowerCase()
|
||||
.includes(searchName.toLowerCase());
|
||||
const districtMatch = item.district.name
|
||||
.toLowerCase()
|
||||
.includes(searchDistrict.toLowerCase());
|
||||
const objectMatch = item.object.name
|
||||
.toLowerCase()
|
||||
.includes(searchObject.toLowerCase());
|
||||
const workMatch = item.work
|
||||
.toLowerCase()
|
||||
.includes(searchWork.toLowerCase());
|
||||
const specMatch = item.spec
|
||||
.toLowerCase()
|
||||
.includes(searchSpec.toLowerCase());
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return (
|
||||
nameMatch &&
|
||||
districtMatch &&
|
||||
objectMatch &&
|
||||
workMatch &&
|
||||
specMatch &&
|
||||
userMatch
|
||||
);
|
||||
});
|
||||
}, [
|
||||
data,
|
||||
searchName,
|
||||
searchDistrict,
|
||||
searchObject,
|
||||
searchWork,
|
||||
searchSpec,
|
||||
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">
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<h1 className="text-2xl font-bold">Shifokorlarni boshqarish</h1>
|
||||
|
||||
<div className="flex justify-end gap-2 w-full">
|
||||
<Input
|
||||
placeholder="Shifokor Ism Familiyasi"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Tuman"
|
||||
value={searchDistrict}
|
||||
onChange={(e) => setSearchDistrict(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Obyekt"
|
||||
value={searchObject}
|
||||
onChange={(e) => setSearchObject(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Ish joyi"
|
||||
value={searchWork}
|
||||
onChange={(e) => setSearchWork(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Sohasi"
|
||||
value={searchSpec}
|
||||
onChange={(e) => setSearchSpec(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Kim qo'shgan"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-x-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan
|
||||
? "Shifokor tahrirlash"
|
||||
: "Yangi shifokor qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AddedDoctor
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setData={setData}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DoctorDetailDialog
|
||||
detail={detailDialog}
|
||||
setDetail={setDetailDialog}
|
||||
object={detail}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Shifokor Ism Familiyasi</TableHead>
|
||||
<TableHead>Telefon raqami</TableHead>
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Obyekt</TableHead>
|
||||
<TableHead>Ish joyi</TableHead>
|
||||
<TableHead>Sohasi</TableHead>
|
||||
<TableHead>Kim qo'shgan</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{item.first_name} {item.last_name}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">
|
||||
{formatPhone(item.phone_number)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{item.district.name}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{item.object.name}</TableCell>
|
||||
<TableCell>{item.work}</TableCell>
|
||||
<TableCell>{item.spec}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DoctorsList;
|
||||
77
src/features/location/lib/data.ts
Normal file
77
src/features/location/lib/data.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { fakeDistrict, type District } from "@/features/districts/lib/data";
|
||||
import {
|
||||
doctorListData,
|
||||
type DoctorListType,
|
||||
} from "@/features/doctors/lib/data";
|
||||
import {
|
||||
ObjectListData,
|
||||
type ObjectListType,
|
||||
} from "@/features/objects/lib/data";
|
||||
import {
|
||||
PharmciesData,
|
||||
type PharmciesType,
|
||||
} from "@/features/pharmacies/lib/data";
|
||||
import { FakeUserList, type User } from "@/features/users/lib/data";
|
||||
|
||||
export interface LocationListType {
|
||||
id: number;
|
||||
user: User;
|
||||
object?: ObjectListType;
|
||||
district?: District;
|
||||
pharmcies?: PharmciesType;
|
||||
doctor?: DoctorListType;
|
||||
long: string;
|
||||
lat: string;
|
||||
createdAt: Date; // ⬅ qo‘shildi
|
||||
}
|
||||
|
||||
export const LocationFakeData: LocationListType[] = [
|
||||
{
|
||||
id: 1,
|
||||
user: FakeUserList[0],
|
||||
object: ObjectListData[0],
|
||||
long: "69.2401",
|
||||
lat: "41.2995",
|
||||
createdAt: new Date("2025-02-01T10:15:00"),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user: FakeUserList[1],
|
||||
district: fakeDistrict[1],
|
||||
long: "69.2305",
|
||||
lat: "41.3102",
|
||||
createdAt: new Date("2025-02-03T14:22:00"),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
user: FakeUserList[2],
|
||||
pharmcies: PharmciesData[0],
|
||||
long: "69.2450",
|
||||
lat: "41.3000",
|
||||
createdAt: new Date("2025-02-05T09:40:00"),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
user: FakeUserList[0],
|
||||
doctor: doctorListData[2],
|
||||
long: "69.2250",
|
||||
lat: "41.3122",
|
||||
createdAt: new Date("2025-02-10T18:10:00"),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
user: FakeUserList[3],
|
||||
object: ObjectListData[2],
|
||||
long: "69.2180",
|
||||
lat: "41.3055",
|
||||
createdAt: new Date("2025-02-12T11:55:00"),
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
user: FakeUserList[5],
|
||||
object: ObjectListData[1],
|
||||
long: "69.2043",
|
||||
lat: "41.2859",
|
||||
createdAt: new Date("2025-02-01T10:15:00"),
|
||||
},
|
||||
];
|
||||
100
src/features/location/ui/LocationDetailDialog.tsx
Normal file
100
src/features/location/ui/LocationDetailDialog.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { LocationListType } from "@/features/location/lib/data";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Circle, Map, Placemark, 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;
|
||||
}
|
||||
|
||||
const LocationDetailDialog = ({ detail, object, setDetail }: Props) => {
|
||||
const [circle, setCircle] = useState<string[] | undefined>([""]);
|
||||
|
||||
useEffect(() => {
|
||||
if (object && object.object) {
|
||||
setCircle([object.object.lat, object.object.long]);
|
||||
} else if (object && object.pharmcies) {
|
||||
setCircle([object.pharmcies.lat, object.pharmcies.long]);
|
||||
} else if (object && object.doctor) {
|
||||
setCircle([object.doctor.lat, object.doctor.long]);
|
||||
} else {
|
||||
setCircle(undefined);
|
||||
}
|
||||
}, [object]);
|
||||
|
||||
if (!object) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={detail} onOpenChange={setDetail}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
Reja haqida batafsil
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<DialogDescription className="space-y-4 mt-2 text-md">
|
||||
<div className="flex gap-2">
|
||||
<p className="font-semibold text-gray-900">
|
||||
Jo'natgan foydalanvchi:
|
||||
</p>
|
||||
<p className="text-black">
|
||||
{object.user.firstName} {object.user.lastName}
|
||||
</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")}
|
||||
</p>
|
||||
</div>
|
||||
{object.district && (
|
||||
<div className="flex gap-2">
|
||||
<p className="font-semibold text-gray-900">Tuman:</p>
|
||||
<p className="text-black">{object.district.name}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="font-semibold text-gray-900">Qayerdan jo'natdi:</p>
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(object.lat), Number(object.long)],
|
||||
zoom: 16,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
>
|
||||
<Placemark
|
||||
geometry={[Number(object.lat), Number(object.long)]}
|
||||
/>
|
||||
{circle && (
|
||||
<Circle
|
||||
geometry={[[Number(circle[0]), Number(circle[1])], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationDetailDialog;
|
||||
224
src/features/location/ui/LocationList.tsx
Normal file
224
src/features/location/ui/LocationList.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
import {
|
||||
LocationFakeData,
|
||||
type LocationListType,
|
||||
} from "@/features/location/lib/data";
|
||||
import LocationDetailDialog from "@/features/location/ui/LocationDetailDialog";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const LocationList = () => {
|
||||
const [data, setData] = useState<LocationListType[]>(LocationFakeData);
|
||||
const [detail, setDetail] = useState<LocationListType | null>(null);
|
||||
const [detailDialog, setDetailDialog] = useState<boolean>(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
|
||||
// Filter state
|
||||
const [dateFilter, setDateFilter] = useState<Date | undefined>(undefined);
|
||||
const [searchUser, setSearchUser] = useState<string>("");
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setData((prev) => prev.filter((e) => e.id !== 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">
|
||||
<h1 className="text-2xl font-bold">Jo'natilgan lokatsiyalar</h1>
|
||||
|
||||
<div className="flex gap-2 w-full md:w-auto">
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-48 justify-between font-normal h-12"
|
||||
>
|
||||
{dateFilter ? dateFilter.toDateString() : "Sana"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateFilter}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDateFilter(date);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="p-2 border-t bg-white">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setDateFilter(undefined);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Tozalash
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Foydalanuvchi ismi"
|
||||
className="h-12"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<LocationDetailDialog
|
||||
detail={detailDialog}
|
||||
setDetail={setDetailDialog}
|
||||
object={detail}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Jo'natgan foydalanuvchi</TableHead>
|
||||
<TableHead>Jo'natgan vaqti</TableHead>
|
||||
<TableHead>Qayerdan jo'natdi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(item.createdAt, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{item.district
|
||||
? "Tuman"
|
||||
: item.object
|
||||
? "Obyekt"
|
||||
: item.doctor
|
||||
? "Shifokor"
|
||||
: item.pharmcies
|
||||
? "Dorixona"
|
||||
: "Turgan joyidan"}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationList;
|
||||
33
src/features/objects/lib/data.ts
Normal file
33
src/features/objects/lib/data.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { fakeDistrict, type District } from "@/features/districts/lib/data";
|
||||
import { FakeUserList, type User } from "@/features/users/lib/data";
|
||||
|
||||
export interface ObjectListType {
|
||||
id: number;
|
||||
name: string;
|
||||
district: District;
|
||||
user: User;
|
||||
long: string;
|
||||
lat: string;
|
||||
moreLong: string[];
|
||||
}
|
||||
|
||||
export const ObjectListData: ObjectListType[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Sport Kompleksi A",
|
||||
district: fakeDistrict[0],
|
||||
user: FakeUserList[0],
|
||||
long: "69.2361",
|
||||
lat: "41.2949",
|
||||
moreLong: ["41.2949", "69.2361"],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Fitnes Markazi B",
|
||||
district: fakeDistrict[1],
|
||||
user: FakeUserList[1],
|
||||
lat: "41.2851",
|
||||
long: "69.2043",
|
||||
moreLong: ["41.2851", "69.2043"],
|
||||
},
|
||||
];
|
||||
9
src/features/objects/lib/form.ts
Normal file
9
src/features/objects/lib/form.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import z from "zod";
|
||||
|
||||
export const ObjectForm = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
district: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
long: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
lat: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
194
src/features/objects/ui/AddedObject.tsx
Normal file
194
src/features/objects/ui/AddedObject.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { fakeDistrict } from "@/features/districts/lib/data";
|
||||
import type { ObjectListType } from "@/features/objects/lib/data";
|
||||
import { ObjectForm } from "@/features/objects/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} 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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: ObjectListType | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setData: Dispatch<SetStateAction<ObjectListType[]>>;
|
||||
}
|
||||
|
||||
export default function AddedObject({
|
||||
initialValues,
|
||||
setDialogOpen,
|
||||
setData,
|
||||
}: Props) {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof ObjectForm>>({
|
||||
resolver: zodResolver(ObjectForm),
|
||||
defaultValues: {
|
||||
lat: initialValues?.lat || "41.2949",
|
||||
long: initialValues?.long || "69.2361",
|
||||
name: initialValues?.name || "",
|
||||
user: initialValues ? String(initialValues.user.id) : "",
|
||||
district: initialValues ? String(initialValues.district.id) : "",
|
||||
},
|
||||
});
|
||||
|
||||
const lat = form.watch("lat");
|
||||
const long = form.watch("long");
|
||||
|
||||
const handleMapClick = (e: { get: (key: string) => number[] }) => {
|
||||
const coords = e.get("coords");
|
||||
form.setValue("lat", coords[0].toString());
|
||||
form.setValue("long", coords[1].toString());
|
||||
};
|
||||
|
||||
function onSubmit(values: z.infer<typeof ObjectForm>) {
|
||||
setLoad(true);
|
||||
const newObject: ObjectListType = {
|
||||
id: initialValues ? initialValues.id : Date.now(),
|
||||
name: values.name,
|
||||
lat: values.lat,
|
||||
long: values.long,
|
||||
moreLong: [values.long, values.lat],
|
||||
user: FakeUserList.find((u) => u.id === Number(values.user))!,
|
||||
district: fakeDistrict.find((d) => d.id === Number(values.district))!,
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setData((prev) => {
|
||||
if (initialValues) {
|
||||
return prev.map((item) =>
|
||||
item.id === initialValues.id ? newObject : item,
|
||||
);
|
||||
} else {
|
||||
return [...prev, newObject];
|
||||
}
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Obyekt nomi</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Nomi" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="district"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Tuman</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Tumanlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fakeDistrict.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Foydalanuvchi</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchilar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FakeUserList.map((e) => (
|
||||
<SelectItem value={String(e.id)}>
|
||||
{e.firstName} {e.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(lat), Number(long)],
|
||||
zoom: 16,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[Number(lat), Number(long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(lat), Number(long)], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||
type="submit"
|
||||
>
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
76
src/features/objects/ui/ObjectDetail.tsx
Normal file
76
src/features/objects/ui/ObjectDetail.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { ObjectListType } from "@/features/objects/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Card, CardContent } from "@/shared/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
object: ObjectListType | null;
|
||||
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||
detail: boolean;
|
||||
}
|
||||
|
||||
const ObjectDetailDialog = ({ object, detail, setDetail }: Props) => {
|
||||
if (!object) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={detail} onOpenChange={setDetail}>
|
||||
<DialogContent className="max-w-lg w-full">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{object.name}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Card className="my-2 shadow-sm">
|
||||
<CardContent className="space-y-2">
|
||||
<div>
|
||||
<span className="font-semibold">Tuman:</span>{" "}
|
||||
{object.district.name}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-semibold">Foydalanuvchi:</span>{" "}
|
||||
{object.user.firstName} {object.user.lastName}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(object.lat), Number(object.long)],
|
||||
zoom: 16,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
>
|
||||
<Placemark geometry={[Number(object.lat), Number(object.long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(object.lat), Number(object.long)], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 text-right">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">Yopish</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default ObjectDetailDialog;
|
||||
231
src/features/objects/ui/ObjectList.tsx
Normal file
231
src/features/objects/ui/ObjectList.tsx
Normal file
@@ -0,0 +1,231 @@
|
||||
import {
|
||||
ObjectListData,
|
||||
type ObjectListType,
|
||||
} from "@/features/objects/lib/data";
|
||||
import AddedObject from "@/features/objects/ui/AddedObject";
|
||||
import ObjectDetailDialog from "@/features/objects/ui/ObjectDetail";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
export default function ObjectList() {
|
||||
const [data, setData] = useState<ObjectListType[]>(ObjectListData);
|
||||
const [detail, setDetail] = useState<ObjectListType | null>(null);
|
||||
const [detailDialog, setDetailDialog] = useState<boolean>(false);
|
||||
const [editingPlan, setEditingPlan] = useState<ObjectListType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
|
||||
// Filter state
|
||||
const [searchName, setSearchName] = useState("");
|
||||
const [searchDistrict, setSearchDistrict] = useState("");
|
||||
const [searchUser, setSearchUser] = useState("");
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setData((prev) => prev.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
// Filtered data
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter((item) => {
|
||||
const nameMatch = item.name
|
||||
.toLowerCase()
|
||||
.includes(searchName.toLowerCase());
|
||||
const districtMatch = item.district.name
|
||||
.toLowerCase()
|
||||
.includes(searchDistrict.toLowerCase());
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return nameMatch && districtMatch && userMatch;
|
||||
});
|
||||
}, [data, searchName, searchDistrict, 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">
|
||||
<h1 className="text-2xl font-bold">Obyektlarni boshqarish</h1>
|
||||
|
||||
<div className="flex gap-2 flex-wrap w-full md:w-auto">
|
||||
<Input
|
||||
placeholder="Obyekt nomi"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Tuman"
|
||||
value={searchDistrict}
|
||||
onChange={(e) => setSearchDistrict(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Foydalanuvchi"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan
|
||||
? "Obyektni tahrirlash"
|
||||
: "Yangi obyekt qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddedObject
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setData={setData}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<ObjectDetailDialog
|
||||
detail={detailDialog}
|
||||
setDetail={setDetailDialog}
|
||||
object={detail}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Obyekt nomi</TableHead>
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Foydalanuvchi</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell className="font-medium">{item.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{item.district.name}</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
11
src/features/pharm/lib/data.ts
Normal file
11
src/features/pharm/lib/data.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export interface PharmType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const pharmData: PharmType[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Meridyn",
|
||||
},
|
||||
];
|
||||
5
src/features/pharm/lib/form.ts
Normal file
5
src/features/pharm/lib/form.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import z from "zod";
|
||||
|
||||
export const pharmForm = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
106
src/features/pharm/ui/AddedPharm.tsx
Normal file
106
src/features/pharm/ui/AddedPharm.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import type { PharmType } from "@/features/pharm/lib/data";
|
||||
import { pharmForm } from "@/features/pharm/lib/form";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
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 form = useForm<z.infer<typeof pharmForm>>({
|
||||
resolver: zodResolver(pharmForm),
|
||||
defaultValues: {
|
||||
name: initialValues?.name || "",
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setPlans((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length ? prev[prev.length - 1].id + 1 : 1,
|
||||
name: data.name,
|
||||
},
|
||||
]);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Farmasevtika nomi</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Farmasevtika nomi"
|
||||
className="h-12 !text-md"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||
disabled={load}
|
||||
>
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Saqlash"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddedPharm;
|
||||
173
src/features/pharm/ui/PharmList.tsx
Normal file
173
src/features/pharm/ui/PharmList.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { pharmData, type PharmType } from "@/features/pharm/lib/data";
|
||||
import AddedPharm from "@/features/pharm/ui/AddedPharm";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const PharmList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<PharmType[]>(pharmData);
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<PharmType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const [nameFilter, setNameFilter] = useState<string>("");
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setPlans(plans.filter((p) => p.id !== 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">
|
||||
<h1 className="text-2xl font-bold">Farmasevtikalar ro'yxati</h1>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Farmasevtika nomi"
|
||||
className="h-12"
|
||||
value={nameFilter}
|
||||
onChange={(e) => setNameFilter(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
|
||||
? "Farmasevtikani tahrirlash"
|
||||
: "Yangi farmasevtika qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddedPharm
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PharmList;
|
||||
82
src/features/pharmacies/lib/data.ts
Normal file
82
src/features/pharmacies/lib/data.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import { fakeDistrict, type District } from "@/features/districts/lib/data";
|
||||
import {
|
||||
ObjectListData,
|
||||
type ObjectListType,
|
||||
} from "@/features/objects/lib/data";
|
||||
import { FakeUserList, type User } from "@/features/users/lib/data";
|
||||
|
||||
export interface PharmciesType {
|
||||
id: number;
|
||||
name: string;
|
||||
inn: string;
|
||||
phone_number: string;
|
||||
additional_phone: string;
|
||||
district: District;
|
||||
user: User;
|
||||
object: ObjectListType;
|
||||
long: string;
|
||||
lat: string;
|
||||
}
|
||||
|
||||
export const PharmciesData: PharmciesType[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "City Pharmacy",
|
||||
inn: "123456789",
|
||||
phone_number: "+998901234567",
|
||||
additional_phone: "+998901112233",
|
||||
district: fakeDistrict[0],
|
||||
user: FakeUserList[0],
|
||||
object: ObjectListData[0],
|
||||
long: "69.2401",
|
||||
lat: "41.2995",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Central Pharmacy",
|
||||
inn: "987654321",
|
||||
phone_number: "+998902345678",
|
||||
additional_phone: "+998903334455",
|
||||
district: fakeDistrict[1],
|
||||
user: FakeUserList[1],
|
||||
object: ObjectListData[1],
|
||||
long: "69.2305",
|
||||
lat: "41.3102",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: "Green Pharmacy",
|
||||
inn: "112233445",
|
||||
phone_number: "+998903456789",
|
||||
additional_phone: "+998904445566",
|
||||
district: fakeDistrict[2],
|
||||
user: FakeUserList[2],
|
||||
object: ObjectListData[1],
|
||||
long: "69.2208",
|
||||
lat: "41.305",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: "HealthPlus Pharmacy",
|
||||
inn: "556677889",
|
||||
phone_number: "+998905678901",
|
||||
additional_phone: "+998906667788",
|
||||
district: fakeDistrict[0],
|
||||
user: FakeUserList[1],
|
||||
object: ObjectListData[1],
|
||||
long: "69.235",
|
||||
lat: "41.312",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: "Optima Pharmacy",
|
||||
inn: "998877665",
|
||||
phone_number: "+998907890123",
|
||||
additional_phone: "+998908889900",
|
||||
district: fakeDistrict[1],
|
||||
user: FakeUserList[0],
|
||||
object: ObjectListData[0],
|
||||
long: "69.245",
|
||||
lat: "41.3",
|
||||
},
|
||||
];
|
||||
13
src/features/pharmacies/lib/form.ts
Normal file
13
src/features/pharmacies/lib/form.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import z from "zod";
|
||||
|
||||
export const PharmForm = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
inn: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
phone_number: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
additional_phone: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
district: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
object: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
long: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
lat: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
277
src/features/pharmacies/ui/AddedPharmacies.tsx
Normal file
277
src/features/pharmacies/ui/AddedPharmacies.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
import { fakeDistrict } from "@/features/districts/lib/data";
|
||||
import { ObjectListData } from "@/features/objects/lib/data";
|
||||
import type { PharmciesType } from "@/features/pharmacies/lib/data";
|
||||
import { PharmForm } from "@/features/pharmacies/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import onlyNumber from "@/shared/lib/onlyNumber";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} 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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: PharmciesType | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setData: Dispatch<SetStateAction<PharmciesType[]>>;
|
||||
}
|
||||
|
||||
const AddedPharmacies = ({ initialValues, setData, setDialogOpen }: Props) => {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof PharmForm>>({
|
||||
resolver: zodResolver(PharmForm),
|
||||
defaultValues: {
|
||||
additional_phone: initialValues?.additional_phone || "+998",
|
||||
district: initialValues?.district.id.toString() || "",
|
||||
inn: initialValues?.inn || "",
|
||||
lat: initialValues?.lat || "41.2949",
|
||||
long: initialValues?.long || "69.2361",
|
||||
name: initialValues?.name || "",
|
||||
object: initialValues?.object.id.toString() || "",
|
||||
phone_number: initialValues?.phone_number || "+998",
|
||||
user: initialValues?.user.id.toString() || "",
|
||||
},
|
||||
});
|
||||
|
||||
const lat = form.watch("lat");
|
||||
const long = form.watch("long");
|
||||
|
||||
const handleMapClick = (e: { get: (key: string) => number[] }) => {
|
||||
const coords = e.get("coords");
|
||||
form.setValue("lat", coords[0].toString());
|
||||
form.setValue("long", coords[1].toString());
|
||||
};
|
||||
|
||||
function onSubmit(values: z.infer<typeof PharmForm>) {
|
||||
setLoad(true);
|
||||
const newObject: PharmciesType = {
|
||||
id: initialValues ? initialValues.id : Date.now(),
|
||||
name: values.name,
|
||||
lat: values.lat,
|
||||
long: values.long,
|
||||
user: FakeUserList.find((u) => u.id === Number(values.user))!,
|
||||
district: fakeDistrict.find((d) => d.id === Number(values.district))!,
|
||||
additional_phone: onlyNumber(values.additional_phone),
|
||||
inn: values.inn,
|
||||
object: ObjectListData.find((o) => o.id === Number(values.object))!,
|
||||
phone_number: onlyNumber(values.phone_number),
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setData((prev) => {
|
||||
if (initialValues) {
|
||||
return prev.map((item) =>
|
||||
item.id === initialValues.id ? newObject : item,
|
||||
);
|
||||
} else {
|
||||
return [...prev, newObject];
|
||||
}
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form className="space-y-4" onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Dorixona nomi</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Nomi" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="inn"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Inn</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Inn" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone_number"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Dorixonaning boshlig'ini raqami</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="+998 90 123-45-67"
|
||||
{...field}
|
||||
value={formatPhone(field.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="additional_phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Ma'sul shaxsning raqami</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="+998 90 123-45-67"
|
||||
{...field}
|
||||
value={formatPhone(field.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="district"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Tuman</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Tumanlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{fakeDistrict.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="object"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Obyekt</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Obyektlar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{ObjectListData.map((e) => (
|
||||
<SelectItem key={e.id} value={String(e.id)}>
|
||||
{e.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="user"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Foydalanuvchi</Label>
|
||||
<FormControl>
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchilar" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FakeUserList.map((e) => (
|
||||
<SelectItem value={String(e.id)}>
|
||||
{e.firstName} {e.lastName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(lat), Number(long)],
|
||||
zoom: 16,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[Number(lat), Number(long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(lat), Number(long)], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
<Button
|
||||
className="w-full h-12 bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||
type="submit"
|
||||
>
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddedPharmacies;
|
||||
109
src/features/pharmacies/ui/PharmDetailDialog.tsx
Normal file
109
src/features/pharmacies/ui/PharmDetailDialog.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import type { PharmciesType } from "@/features/pharmacies/lib/data";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
detail: boolean;
|
||||
setDetail: (value: boolean) => void;
|
||||
object: PharmciesType | null;
|
||||
}
|
||||
|
||||
const PharmDetailDialog = ({ detail, setDetail, object }: Props) => {
|
||||
const [open, setOpen] = useState(detail);
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(detail);
|
||||
}, [detail]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onOpenChange={(val) => {
|
||||
setOpen(val);
|
||||
setDetail(val);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Farmatsiya tafsilotlari</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{object ? (
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<strong>Nomi:</strong> {object.name}
|
||||
</div>
|
||||
<div>
|
||||
<strong>INN:</strong> {object.inn}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Telefon:</strong> {formatPhone(object.phone_number)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Qo‘shimcha telefon:</strong>{" "}
|
||||
{formatPhone(object.additional_phone)}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Tuman:</strong> {object.district.name}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Obyekt:</strong> {object.object.name}
|
||||
</div>
|
||||
<div>
|
||||
<strong>Kimga tegishli:</strong> {object.user.firstName}{" "}
|
||||
{object.user.lastName}
|
||||
</div>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [Number(object.lat), Number(object.long)],
|
||||
zoom: 16,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
>
|
||||
<Placemark
|
||||
geometry={[Number(object.lat), Number(object.long)]}
|
||||
/>
|
||||
<Circle
|
||||
geometry={[[Number(object.lat), Number(object.long)], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>Ma'lumot topilmadi</div>
|
||||
)}
|
||||
<div className="text-right">
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full h-12 bg-blue-500 text-white cursor-pointer hover:bg-blue-500 hover:text-white"
|
||||
>
|
||||
Yopish
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PharmDetailDialog;
|
||||
248
src/features/pharmacies/ui/PharmaciesList.tsx
Normal file
248
src/features/pharmacies/ui/PharmaciesList.tsx
Normal file
@@ -0,0 +1,248 @@
|
||||
import {
|
||||
PharmciesData,
|
||||
type PharmciesType,
|
||||
} from "@/features/pharmacies/lib/data";
|
||||
import AddedPharmacies from "@/features/pharmacies/ui/AddedPharmacies";
|
||||
import PharmDetailDialog from "@/features/pharmacies/ui/PharmDetailDialog";
|
||||
import formatPhone from "@/shared/lib/formatPhone";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const PharmaciesList = () => {
|
||||
const [data, setData] = useState<PharmciesType[]>(PharmciesData);
|
||||
const [detail, setDetail] = useState<PharmciesType | null>(null);
|
||||
const [detailDialog, setDetailDialog] = useState<boolean>(false);
|
||||
const [editingPlan, setEditingPlan] = useState<PharmciesType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
|
||||
const [searchName, setSearchName] = useState("");
|
||||
const [searchDistrict, setSearchDistrict] = useState("");
|
||||
const [searchObject, setSearchObject] = useState("");
|
||||
const [searchUser, setSearchUser] = useState("");
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setData((prev) => prev.filter((e) => e.id !== id));
|
||||
};
|
||||
|
||||
const filteredData = useMemo(() => {
|
||||
return data.filter((item) => {
|
||||
const nameMatch = `${item.name}`
|
||||
.toLowerCase()
|
||||
.includes(searchName.toLowerCase());
|
||||
const districtMatch = item.district.name
|
||||
.toLowerCase()
|
||||
.includes(searchDistrict.toLowerCase());
|
||||
const objectMatch = item.object.name
|
||||
.toLowerCase()
|
||||
.includes(searchObject.toLowerCase());
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return nameMatch && districtMatch && objectMatch && userMatch;
|
||||
});
|
||||
}, [data, searchName, searchDistrict, searchObject, searchUser]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center mb-4 gap-4">
|
||||
<div className="flex flex-col gap-4 w-full">
|
||||
<h1 className="text-2xl font-bold">Dorixonalrni boshqarish</h1>
|
||||
|
||||
<div className="flex justify-end gap-2 w-full">
|
||||
<Input
|
||||
placeholder="Dorixona nomi"
|
||||
value={searchName}
|
||||
onChange={(e) => setSearchName(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Tuman"
|
||||
value={searchDistrict}
|
||||
onChange={(e) => setSearchDistrict(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Obyekt"
|
||||
value={searchObject}
|
||||
onChange={(e) => setSearchObject(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Input
|
||||
placeholder="Kim qo'shgan"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
className="w-full md:w-48"
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg max-h-[80vh] overflow-x-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan
|
||||
? "Dorixonani tahrirlash"
|
||||
: "Yangi dorixona qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AddedPharmacies
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setData={setData}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PharmDetailDialog
|
||||
detail={detailDialog}
|
||||
setDetail={setDetailDialog}
|
||||
object={detail}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>#</TableHead>
|
||||
<TableHead>Dorixona nomi</TableHead>
|
||||
<TableHead>Inn</TableHead>
|
||||
<TableHead>Egasining nomeri</TableHead>
|
||||
<TableHead>Ma'sul shaxsning nomeri</TableHead>
|
||||
<TableHead>Tuman</TableHead>
|
||||
<TableHead>Obyekt</TableHead>
|
||||
<TableHead>Kim qo'shgan</TableHead>
|
||||
<TableHead className="text-right">Amallar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredData.map((item, index) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{index + 1}</TableCell>
|
||||
<TableCell>{item.name}</TableCell>
|
||||
<TableCell>{item.inn}</TableCell>
|
||||
<TableCell>{formatPhone(item.phone_number)}</TableCell>
|
||||
<TableCell>{formatPhone(item.additional_phone)}</TableCell>
|
||||
<TableCell>{item.district.name}</TableCell>
|
||||
<TableCell>{item.object.name}</TableCell>
|
||||
<TableCell>
|
||||
{item.user.firstName} {item.user.lastName}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-right flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
setDetail(item);
|
||||
setDetailDialog(true);
|
||||
}}
|
||||
className="bg-green-600 text-white cursor-pointer hover:bg-green-600 hover:text-white"
|
||||
>
|
||||
<Eye size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="bg-blue-600 text-white cursor-pointer hover:bg-blue-600 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(item);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Pencil size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(item.id)}
|
||||
>
|
||||
<Trash2 size={18} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PharmaciesList;
|
||||
18
src/features/pill/lib/data.ts
Normal file
18
src/features/pill/lib/data.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export interface PillType {
|
||||
id: number;
|
||||
name: string;
|
||||
price: string;
|
||||
}
|
||||
|
||||
export const FakePills: PillType[] = [
|
||||
{ id: 1, name: "Paracetamol 500mg", price: "12000" },
|
||||
{ id: 2, name: "Ibuprofen 200mg", price: "18000" },
|
||||
{ id: 3, name: "Noshpa 40mg", price: "10000" },
|
||||
{ id: 4, name: "Aspirin 100mg", price: "8000" },
|
||||
{ id: 5, name: "Strepsils", price: "22000" },
|
||||
{ id: 6, name: "Azithromycin 500mg", price: "35000" },
|
||||
{ id: 7, name: "Aqualor Baby", price: "40000" },
|
||||
{ id: 8, name: "Vitamin C 1000mg", price: "15000" },
|
||||
{ id: 9, name: "Amoxicillin 500mg", price: "28000" },
|
||||
{ id: 10, name: "Immuno Plus", price: "30000" },
|
||||
];
|
||||
6
src/features/pill/lib/form.ts
Normal file
6
src/features/pill/lib/form.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import z from "zod";
|
||||
|
||||
export const createPillFormData = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
price: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
144
src/features/pill/ui/AddedPill.tsx
Normal file
144
src/features/pill/ui/AddedPill.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import type { 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";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
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 [displayPrice, setDisplayPrice] = useState<string>("");
|
||||
const form = useForm<z.infer<typeof createPillFormData>>({
|
||||
resolver: zodResolver(createPillFormData),
|
||||
defaultValues: {
|
||||
name: initialValues?.name || "",
|
||||
price: initialValues?.price || "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
setDisplayPrice(formatPrice(initialValues.price));
|
||||
}
|
||||
}, [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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Dori nomi</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Dori nomi"
|
||||
className="h-12 !text-md"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Label>Narxi</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="1 500 000"
|
||||
value={displayPrice}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/\D/g, "");
|
||||
const num = Number(raw);
|
||||
if (!isNaN(num)) {
|
||||
form.setValue("price", String(num));
|
||||
setDisplayPrice(raw ? formatPrice(num) : "");
|
||||
}
|
||||
}}
|
||||
className="h-12 !text-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||
disabled={load}
|
||||
>
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Saqlash"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddedPill;
|
||||
174
src/features/pill/ui/PillList.tsx
Normal file
174
src/features/pill/ui/PillList.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { FakePills, type PillType } from "@/features/pill/lib/data";
|
||||
import AddedPill from "@/features/pill/ui/AddedPill";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const PillList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<PillType[]>(FakePills);
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<PillType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const [nameFilter, setNameFilter] = useState<string>("");
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setPlans(plans.filter((p) => p.id !== 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">
|
||||
<h1 className="text-2xl font-bold">Dorilar ro'yxati</h1>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Dori nomi"
|
||||
className="h-12"
|
||||
value={nameFilter}
|
||||
onChange={(e) => setNameFilter(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 ? "Dorini tahrirlash" : "Yangi dorini qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddedPill
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</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-start">Narxi</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>{formatPrice(plan.price)}</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PillList;
|
||||
10
src/features/plans/lib/data.ts
Normal file
10
src/features/plans/lib/data.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { User } from "@/features/users/lib/data";
|
||||
|
||||
export interface Plan {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
user: User;
|
||||
status: "Bajarildi" | "Bajarilmagan";
|
||||
createdAt: Date;
|
||||
}
|
||||
7
src/features/plans/lib/form.ts
Normal file
7
src/features/plans/lib/form.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import z from "zod";
|
||||
|
||||
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" }),
|
||||
});
|
||||
165
src/features/plans/ui/AddedPlan.tsx
Normal file
165
src/features/plans/ui/AddedPlan.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import { createPlanFormData } from "@/features/plans/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} 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 { Textarea } from "@/shared/ui/textarea";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues?: Plan | null;
|
||||
setDialogOpen: (open: boolean) => void;
|
||||
setPlans: React.Dispatch<React.SetStateAction<Plan[]>>;
|
||||
}
|
||||
|
||||
const AddedPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
const [load, setLoad] = useState(false);
|
||||
const form = useForm<z.infer<typeof createPlanFormData>>({
|
||||
resolver: zodResolver(createPlanFormData),
|
||||
defaultValues: {
|
||||
name: initialValues?.name || "",
|
||||
description: initialValues?.description || "",
|
||||
user: initialValues ? String(initialValues.user.id) : "",
|
||||
},
|
||||
});
|
||||
|
||||
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);
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Reja nomi</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Reja nomi"
|
||||
className="h-12 !text-md"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Reja tavsifi</Label>
|
||||
<FormControl>
|
||||
<Textarea
|
||||
placeholder="Reja tavsifi"
|
||||
className="min-h-32 max-h-52 !text-md"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||
disabled={load}
|
||||
>
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Saqlash"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddedPlan;
|
||||
74
src/features/plans/ui/PlanDetail.tsx
Normal file
74
src/features/plans/ui/PlanDetail.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import clsx from "clsx";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
setDetail: Dispatch<SetStateAction<boolean>>;
|
||||
detail: boolean;
|
||||
plan: Plan | null;
|
||||
}
|
||||
|
||||
const PlanDetail = ({ detail, setDetail, plan }: Props) => {
|
||||
if (!plan) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={detail} onOpenChange={setDetail}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold">
|
||||
Reja haqida batafsil
|
||||
</DialogTitle>
|
||||
</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.name}</p>
|
||||
</div>
|
||||
|
||||
{/* Reja tavsifi */}
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Tavsifi:</p>
|
||||
<p>{plan.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Kimga tegishli */}
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Kimga tegishli:</p>
|
||||
<p>
|
||||
{plan.user.firstName} {plan.user.lastName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Reja statusi */}
|
||||
<div>
|
||||
<p className="font-semibold text-gray-900">Reja statusi:</p>
|
||||
|
||||
<Badge
|
||||
className={clsx(
|
||||
plan.status === "Bajarildi"
|
||||
? "bg-green-100 text-green-700"
|
||||
: "bg-yellow-100 text-yellow-700",
|
||||
"text-sm px-4 py-2 mt-2",
|
||||
)}
|
||||
>
|
||||
{plan.status}
|
||||
</Badge>
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlanDetail;
|
||||
308
src/features/plans/ui/PlansList.tsx
Normal file
308
src/features/plans/ui/PlansList.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
"use client";
|
||||
|
||||
import type { Plan } from "@/features/plans/lib/data";
|
||||
import AddedPlan from "@/features/plans/ui/AddedPlan";
|
||||
import PlanDetail from "@/features/plans/ui/PlanDetail";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Edit,
|
||||
Eye,
|
||||
Plus,
|
||||
Trash,
|
||||
} from "lucide-react";
|
||||
import { useMemo, 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 handleDelete = (id: number) => {
|
||||
setPlans(plans.filter((p) => p.id !== id));
|
||||
};
|
||||
|
||||
const filteredPlans = useMemo(() => {
|
||||
return plans.filter((item) => {
|
||||
// 1) Status (agar all bo'lsa filtrlanmaydi)
|
||||
const statusMatch =
|
||||
statusFilter === "all" || item.status === statusFilter;
|
||||
|
||||
// 2) Sana filtri: createdAt === tanlangan sana
|
||||
const dateMatch = dateFilter
|
||||
? item.createdAt.toDateString() === dateFilter.toDateString()
|
||||
: true;
|
||||
|
||||
// 3) User ism familiya bo'yicha qidiruv
|
||||
const userMatch = `${item.user.firstName} ${item.user.lastName}`
|
||||
.toLowerCase()
|
||||
.includes(searchUser.toLowerCase());
|
||||
|
||||
return statusMatch && dateMatch && userMatch;
|
||||
});
|
||||
}, [plans, statusFilter, 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">
|
||||
{/* Status filter */}
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="foydalanuvchi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Barchasi</SelectItem>
|
||||
<SelectItem value="Bajarildi">Bajarildi</SelectItem>
|
||||
<SelectItem value="Bajarilmagan">Bajarilmagan</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* Sana filter */}
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-48 justify-between font-normal h-12"
|
||||
>
|
||||
{dateFilter ? dateFilter.toDateString() : "Sana"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={dateFilter}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
setDateFilter(date);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
<div className="p-2 border-t bg-white">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setDateFilter(undefined);
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
Tozalash
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Foydalanuvchi ismi"
|
||||
className="h-12"
|
||||
value={searchUser}
|
||||
onChange={(e) => setSearchUser(e.target.value)}
|
||||
/>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500 h-12"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
{/* Form */}
|
||||
<AddedPlan
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
{/* Deail plan */}
|
||||
<PlanDetail detail={detail} setDetail={setDetail} plan={editingPlan} />
|
||||
</div>
|
||||
|
||||
{/* Table */}
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Reja nomi</TableHead>
|
||||
<TableHead className="text-start">Tavsifi</TableHead>
|
||||
<TableHead className="text-start">Kimga tegishli</TableHead>
|
||||
<TableHead className="text-start">Status</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredPlans.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.name}</TableCell>
|
||||
<TableCell>{plan.description}</TableCell>
|
||||
<TableCell>
|
||||
{plan.user.firstName + " " + plan.user.lastName}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className={clsx(
|
||||
plan.status === "Bajarildi"
|
||||
? "text-green-500"
|
||||
: "text-red-500",
|
||||
)}
|
||||
>
|
||||
{plan.status}
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-green-500 text-white cursor-pointer hover:bg-green-500 hover:text-white"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDetail(true);
|
||||
}}
|
||||
>
|
||||
<Eye className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PlansList;
|
||||
19
src/features/region/lib/data.ts
Normal file
19
src/features/region/lib/data.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export interface RegionType {
|
||||
id: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export const fakeRegionList: RegionType[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: "Toshkent",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "Samarqand",
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
name: "Andijon",
|
||||
},
|
||||
];
|
||||
5
src/features/region/lib/form.ts
Normal file
5
src/features/region/lib/form.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import z from "zod";
|
||||
|
||||
export const regionForm = z.object({
|
||||
name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
83
src/features/region/ui/AddedRegion.tsx
Normal file
83
src/features/region/ui/AddedRegion.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { RegionType } from "@/features/region/lib/data";
|
||||
import { regionForm } from "@/features/region/lib/form";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: RegionType | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setPlans: Dispatch<SetStateAction<RegionType[]>>;
|
||||
}
|
||||
|
||||
const AddedRegion = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
const form = useForm<z.infer<typeof regionForm>>({
|
||||
resolver: zodResolver(regionForm),
|
||||
defaultValues: { name: initialValues?.name || "" },
|
||||
});
|
||||
|
||||
function onSubmit(value: z.infer<typeof regionForm>) {
|
||||
setLoad(true);
|
||||
|
||||
setTimeout(() => {
|
||||
setPlans((prev) => {
|
||||
if (initialValues) {
|
||||
return prev.map((item) =>
|
||||
item.id === initialValues.id ? { ...item, ...value } : item,
|
||||
);
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{ id: prev.length ? prev[prev.length - 1].id + 1 : 1, ...value },
|
||||
];
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Nomi</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Nomi" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button className="w-full bg-blue-500 cursor-pointer hover:bg-blue-500">
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddedRegion;
|
||||
150
src/features/region/ui/RegionList.tsx
Normal file
150
src/features/region/ui/RegionList.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import { fakeRegionList, type RegionType } from "@/features/region/lib/data";
|
||||
import AddedRegion from "@/features/region/ui/AddedRegion";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const RegionList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<RegionType[]>(fakeRegionList);
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<RegionType | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
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">Hududlar ro'yxati</h1>
|
||||
|
||||
<div className="flex gap-2 mb-4">
|
||||
<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 ? "Hududni tahrirlash" : "Yangi hudud qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddedRegion
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</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>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plans.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>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegionList;
|
||||
69
src/features/reports/lib/data.ts
Normal file
69
src/features/reports/lib/data.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
export interface ReportsTypeList {
|
||||
id: number;
|
||||
pharm_name: string;
|
||||
amount: string;
|
||||
month: Date;
|
||||
}
|
||||
|
||||
export const ReportsData: ReportsTypeList[] = [
|
||||
{
|
||||
id: 1,
|
||||
pharm_name: "City Pharmacy",
|
||||
amount: "500000",
|
||||
month: new Date(2025, 0, 1),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
pharm_name: "Central Pharmacy",
|
||||
amount: "750000",
|
||||
month: new Date(2025, 0, 1),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
pharm_name: "Green Pharmacy",
|
||||
amount: "620000",
|
||||
month: new Date(2025, 1, 1),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
pharm_name: "HealthPlus Pharmacy",
|
||||
amount: "810000",
|
||||
month: new Date(2025, 1, 1),
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
pharm_name: "Optima Pharmacy",
|
||||
amount: "430000",
|
||||
month: new Date(2025, 2, 1),
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
pharm_name: "City Pharmacy",
|
||||
amount: "540000",
|
||||
month: new Date(2025, 2, 1),
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
pharm_name: "Central Pharmacy",
|
||||
amount: "770000",
|
||||
month: new Date(2025, 3, 1),
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
pharm_name: "Green Pharmacy",
|
||||
amount: "650000",
|
||||
month: new Date(2025, 3, 1),
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
pharm_name: "HealthPlus Pharmacy",
|
||||
amount: "820000",
|
||||
month: new Date(2025, 4, 1),
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
pharm_name: "Optima Pharmacy",
|
||||
amount: "460000",
|
||||
month: new Date(2025, 4, 1),
|
||||
},
|
||||
];
|
||||
6
src/features/reports/lib/form.ts
Normal file
6
src/features/reports/lib/form.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import z from "zod";
|
||||
|
||||
export const reportsForm = z.object({
|
||||
pharm_name: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
amount: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
126
src/features/reports/ui/AddedReport.tsx
Normal file
126
src/features/reports/ui/AddedReport.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import type { ReportsTypeList } from "@/features/reports/lib/data";
|
||||
import { reportsForm } from "@/features/reports/lib/form";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: ReportsTypeList | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setPlans: Dispatch<SetStateAction<ReportsTypeList[]>>;
|
||||
}
|
||||
|
||||
const AddedReport = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
const [displayPrice, setDisplayPrice] = useState<string>("");
|
||||
const form = useForm<z.infer<typeof reportsForm>>({
|
||||
resolver: zodResolver(reportsForm),
|
||||
defaultValues: {
|
||||
amount: initialValues?.amount || "",
|
||||
pharm_name: initialValues?.pharm_name || "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialValues) {
|
||||
setDisplayPrice(formatPrice(initialValues.amount));
|
||||
}
|
||||
}, [initialValues]);
|
||||
|
||||
function onSubmit(values: z.infer<typeof reportsForm>) {
|
||||
setLoad(true);
|
||||
const newReport: ReportsTypeList = {
|
||||
id: initialValues ? initialValues.id : Date.now(),
|
||||
amount: values.amount,
|
||||
pharm_name: values.pharm_name,
|
||||
month: new Date(),
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
setPlans((prev) => {
|
||||
if (initialValues) {
|
||||
return prev.map((item) =>
|
||||
item.id === initialValues.id ? newReport : item,
|
||||
);
|
||||
} else {
|
||||
return [...prev, newReport];
|
||||
}
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="pharm_name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Dorixona nomi</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Nomi" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="amount"
|
||||
render={() => (
|
||||
<FormItem>
|
||||
<Label>Berilgan summa</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
placeholder="1 500 000"
|
||||
value={displayPrice}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value.replace(/\D/g, "");
|
||||
const num = Number(raw);
|
||||
if (!isNaN(num)) {
|
||||
form.setValue("amount", String(num));
|
||||
setDisplayPrice(raw ? formatPrice(num) : "");
|
||||
}
|
||||
}}
|
||||
className="h-12 !text-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button className="w-full h-12 bg-blue-500 cursor-pointer hover:bg-blue-500">
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddedReport;
|
||||
158
src/features/reports/ui/ReportsList.tsx
Normal file
158
src/features/reports/ui/ReportsList.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { ReportsData, type ReportsTypeList } from "@/features/reports/lib/data";
|
||||
import AddedReport from "@/features/reports/ui/AddedReport";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
const ReportsList = () => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
const [plans, setPlans] = useState<ReportsTypeList[]>(ReportsData);
|
||||
|
||||
const [editingPlan, setEditingPlan] = useState<ReportsTypeList | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
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">Rejalarni boshqarish</h1>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">
|
||||
{editingPlan ? "Rejani tahrirlash" : "Yangi reja qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddedReport
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setPlans={setPlans}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Dorixoan nomi</TableHead>
|
||||
<TableHead className="text-start">To'langan summa</TableHead>
|
||||
<TableHead className="text-start">To'langan sanasi</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{plans.map((plan) => (
|
||||
<TableRow key={plan.id} className="text-start">
|
||||
<TableCell>{plan.id}</TableCell>
|
||||
<TableCell>{plan.pharm_name}</TableCell>
|
||||
<TableCell>{formatPrice(plan.amount, true)}</TableCell>
|
||||
<TableCell>
|
||||
{formatDate.format(plan.month, "DD-MM-YYYY")}
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
onClick={() => {
|
||||
setEditingPlan(plan);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="cursor-pointer"
|
||||
onClick={() => handleDelete(plan.id)}
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div className="mt-2 sticky bottom-0 bg-white flex justify-end gap-2 z-10 py-2 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === 1}
|
||||
className="cursor-pointer"
|
||||
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
|
||||
>
|
||||
<ChevronLeft />
|
||||
</Button>
|
||||
{Array.from({ length: totalPages }, (_, i) => (
|
||||
<Button
|
||||
key={i}
|
||||
variant={currentPage === i + 1 ? "default" : "outline"}
|
||||
size="icon"
|
||||
className={clsx(
|
||||
currentPage === i + 1
|
||||
? "bg-blue-500 hover:bg-blue-500"
|
||||
: " bg-none hover:bg-blue-200",
|
||||
"cursor-pointer",
|
||||
)}
|
||||
onClick={() => setCurrentPage(i + 1)}
|
||||
>
|
||||
{i + 1}
|
||||
</Button>
|
||||
))}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportsList;
|
||||
81
src/features/specifications/lib/data.ts
Normal file
81
src/features/specifications/lib/data.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { pharmData, type PharmType } from "@/features/pharm/lib/data";
|
||||
import { FakeUserList, type User } from "@/features/users/lib/data";
|
||||
|
||||
export interface SpecificationsPillType {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number; // numberga o‘zgartirdim
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface SpecificationsType {
|
||||
id: number;
|
||||
medicines: SpecificationsPillType[];
|
||||
pharm: PharmType;
|
||||
client: string;
|
||||
user: User;
|
||||
percentage: number;
|
||||
totalPrice: number;
|
||||
paidPrice: number;
|
||||
}
|
||||
|
||||
export const SpecificationsFakePills: SpecificationsPillType[] = [
|
||||
{ id: 1, name: "Paracetamol 500mg", price: 12000, count: 10 },
|
||||
{ id: 2, name: "Ibuprofen 200mg", price: 18000, count: 20 },
|
||||
{ id: 3, name: "Noshpa 40mg", price: 10000, count: 10 },
|
||||
{ id: 4, name: "Aspirin 100mg", price: 8000, count: 0 },
|
||||
{ id: 5, name: "Strepsils", price: 22000, count: 0 },
|
||||
{ id: 6, name: "Azithromycin 500mg", price: 35000, count: 0 },
|
||||
{ id: 7, name: "Aqualor Baby", price: 40000, count: 0 },
|
||||
{ id: 8, name: "Vitamin C 1000mg", price: 15000, count: 0 },
|
||||
{ id: 9, name: "Amoxicillin 500mg", price: 28000, count: 0 },
|
||||
{ id: 10, name: "Immuno Plus", price: 30000, count: 0 },
|
||||
];
|
||||
|
||||
export const FakeSpecifications: SpecificationsType[] = [
|
||||
{
|
||||
id: 1,
|
||||
medicines: [
|
||||
SpecificationsFakePills[0],
|
||||
SpecificationsFakePills[2],
|
||||
SpecificationsFakePills[9],
|
||||
SpecificationsFakePills[4],
|
||||
],
|
||||
pharm: pharmData[0],
|
||||
client: "Abdullayev Javlon",
|
||||
user: FakeUserList[0],
|
||||
percentage: 100,
|
||||
totalPrice: 98000,
|
||||
paidPrice: 98000,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
medicines: [
|
||||
SpecificationsFakePills[1],
|
||||
SpecificationsFakePills[5],
|
||||
SpecificationsFakePills[7],
|
||||
SpecificationsFakePills[9],
|
||||
],
|
||||
pharm: pharmData[0],
|
||||
client: "Qodirova Dilnoza",
|
||||
user: FakeUserList[1],
|
||||
percentage: 50,
|
||||
totalPrice: 62000,
|
||||
paidPrice: 31000,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
medicines: [
|
||||
SpecificationsFakePills[0],
|
||||
SpecificationsFakePills[3],
|
||||
SpecificationsFakePills[5],
|
||||
SpecificationsFakePills[9],
|
||||
],
|
||||
pharm: pharmData[0],
|
||||
client: "Saidov Mirjalol",
|
||||
user: FakeUserList[2],
|
||||
percentage: 20,
|
||||
totalPrice: 112000,
|
||||
paidPrice: 22400,
|
||||
},
|
||||
];
|
||||
20
src/features/specifications/lib/form.ts
Normal file
20
src/features/specifications/lib/form.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import z from "zod";
|
||||
|
||||
export const SpecificationsForm = z.object({
|
||||
pharm: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
user: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
client: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
percentage: z.number().min(0, { message: "Majburiy maydon" }),
|
||||
totalPrice: z.number().min(0),
|
||||
paidPrice: z.number().min(0),
|
||||
medicines: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
price: z.number(),
|
||||
count: z.number().min(0),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export type SpecificationsFormType = z.infer<typeof SpecificationsForm>;
|
||||
303
src/features/specifications/ui/AddedSpecification.tsx
Normal file
303
src/features/specifications/ui/AddedSpecification.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
"use client";
|
||||
|
||||
import { pharmData } from "@/features/pharm/lib/data";
|
||||
import {
|
||||
SpecificationsFakePills,
|
||||
type SpecificationsType,
|
||||
} from "@/features/specifications/lib/data";
|
||||
import {
|
||||
SpecificationsForm,
|
||||
type SpecificationsFormType,
|
||||
} from "@/features/specifications/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} 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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
interface Props {
|
||||
initialValues: SpecificationsType | null;
|
||||
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
setData: React.Dispatch<React.SetStateAction<SpecificationsType[]>>;
|
||||
}
|
||||
|
||||
export const AddedSpecification = ({
|
||||
setData,
|
||||
initialValues,
|
||||
setDialogOpen,
|
||||
}: Props) => {
|
||||
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) => ({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
count: 0,
|
||||
price: p.price,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
const medicines = form.watch("medicines");
|
||||
|
||||
const calculateTotal = () => {
|
||||
const medicines = form.getValues("medicines");
|
||||
const total = medicines.reduce(
|
||||
(acc, med) => acc + med.price * med.count,
|
||||
0,
|
||||
);
|
||||
form.setValue("totalPrice", total);
|
||||
|
||||
const percentage = form.getValues("percentage") || 0;
|
||||
const paid = Math.round((total * percentage) / 100);
|
||||
form.setValue("paidPrice", paid);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = form.watch((__, { name }) => {
|
||||
if (name?.startsWith("medicines") || name === "percentage")
|
||||
calculateTotal();
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [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],
|
||||
},
|
||||
]);
|
||||
}
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="client"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Xaridor Nomi</Label>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder="Xaridor nomi..." />
|
||||
</FormControl>{" "}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Medicines list */}
|
||||
<div className="space-y-2">
|
||||
{medicines.map((med, index) => (
|
||||
<div
|
||||
key={med.id}
|
||||
className="flex justify-between items-center space-x-2"
|
||||
>
|
||||
<p className="w-40">{med.name}</p>
|
||||
|
||||
<div className="flex gap-1">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Soni"
|
||||
className="w-20"
|
||||
value={med.count}
|
||||
onChange={(e) => {
|
||||
let value = parseInt(e.target.value, 10);
|
||||
if (isNaN(value)) value = 0;
|
||||
|
||||
const updatedMedicines = [...medicines];
|
||||
updatedMedicines[index].count = value;
|
||||
form.setValue("medicines", updatedMedicines);
|
||||
calculateTotal();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Narxi"
|
||||
className="w-24"
|
||||
value={formatPrice(med.price * med.count)}
|
||||
readOnly
|
||||
/>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="percentage"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>To'lov foizi (%)</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
className="w-full"
|
||||
onChange={(e) => {
|
||||
let value = parseInt(e.target.value, 10);
|
||||
if (isNaN(value)) value = 0;
|
||||
if (value < 0) value = 0;
|
||||
if (value > 100) value = 100;
|
||||
field.onChange(value);
|
||||
calculateTotal();
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="totalPrice"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Jami Narx</Label>
|
||||
<FormControl>
|
||||
<Input {...field} readOnly value={formatPrice(field.value)} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="paidPrice"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>To'lanadi</Label>
|
||||
<FormControl>
|
||||
<Input {...field} readOnly value={formatPrice(field.value)} />
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-500 hover:bg-blue-500 cursor-pointer h-12"
|
||||
>
|
||||
Saqlash
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
151
src/features/specifications/ui/SpecificationDetail .tsx
Normal file
151
src/features/specifications/ui/SpecificationDetail .tsx
Normal file
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import type { SpecificationsType } from "@/features/specifications/lib/data";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
specification: SpecificationsType | null;
|
||||
open: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const SpecificationDetail = ({
|
||||
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">
|
||||
Spetsifikatsiya tafsilotlari
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-6">
|
||||
{/* Asosiy ma'lumotlar - Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* 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">Xaridor</p>
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
{specification.client}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Farmasevtika */}
|
||||
<div className="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-4 border border-green-200">
|
||||
<p className="text-sm text-green-600 font-medium mb-1">
|
||||
Farmasevtika
|
||||
</p>
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
{specification.pharm.name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Foydalanuvchi */}
|
||||
<div className="bg-gradient-to-br 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.firstName} {specification.user.lastName}
|
||||
</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">
|
||||
<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}
|
||||
</span>
|
||||
Dorilar ro'yxati
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
{specification.medicines.map((med, index) => (
|
||||
<div
|
||||
key={med.id}
|
||||
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">
|
||||
<span className="bg-indigo-100 text-indigo-700 text-xs font-semibold px-2 py-1 rounded">
|
||||
#{index + 1}
|
||||
</span>
|
||||
<p className="font-semibold text-gray-800">
|
||||
{med.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||
<span>
|
||||
Miqdor: <strong>{med.count} ta</strong>
|
||||
</span>
|
||||
<span>×</span>
|
||||
<span>
|
||||
Narx: <strong>{formatPrice(med.price)}</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)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* To'lov ma'lumotlari */}
|
||||
<div className="bg-gradient-to-br from-slate-50 to-slate-100 rounded-lg p-5 border-2 border-slate-300">
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-4">
|
||||
To'lov ma'lumotlari
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<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)}
|
||||
</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:
|
||||
</span>
|
||||
<span className="text-lg font-semibold text-orange-600">
|
||||
{specification.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<span className="text-gray-700 font-bold text-lg">
|
||||
To'lanadi:
|
||||
</span>
|
||||
<span className="text-2xl font-bold text-green-600">
|
||||
{formatPrice(specification.paidPrice)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
187
src/features/specifications/ui/SpecificationsList.tsx
Normal file
187
src/features/specifications/ui/SpecificationsList.tsx
Normal file
@@ -0,0 +1,187 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
FakeSpecifications,
|
||||
type SpecificationsType,
|
||||
} from "@/features/specifications/lib/data";
|
||||
import { AddedSpecification } from "@/features/specifications/ui/AddedSpecification";
|
||||
import { SpecificationDetail } from "@/features/specifications/ui/SpecificationDetail ";
|
||||
import formatPrice from "@/shared/lib/formatPrice";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Pencil,
|
||||
Plus,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import { 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 [detailOpen, setDetailOpen] = useState<boolean>(false);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const totalPages = 5;
|
||||
|
||||
const handleDelete = (id: number) =>
|
||||
setData((prev) => prev.filter((e) => e.id !== id));
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h1 className="text-2xl font-bold">Spesifikatsiyalarni boshqarish</h1>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="bg-blue-500 hover:bg-blue-600"
|
||||
onClick={() => setEditingPlan(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg !h-[80vh] overflow-x-hidden">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingPlan ? "Tahrirlash" : "Yangi spesifikatsiya"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<AddedSpecification
|
||||
initialValues={editingPlan}
|
||||
setDialogOpen={setDialogOpen}
|
||||
setData={setData}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<SpecificationDetail
|
||||
specification={detail}
|
||||
open={detailOpen}
|
||||
setOpen={setDetailOpen}
|
||||
/>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SpecificationsList;
|
||||
60
src/features/tour-plan/lib/data.ts
Normal file
60
src/features/tour-plan/lib/data.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { FakeUserList, type User } from "@/features/users/lib/data";
|
||||
|
||||
export interface UserLocation {
|
||||
long: string;
|
||||
lat: string;
|
||||
}
|
||||
|
||||
export type TourStatus = "planned" | "completed";
|
||||
|
||||
export interface TourPlanType {
|
||||
id: number;
|
||||
district: string;
|
||||
user: User;
|
||||
userLocation?: UserLocation;
|
||||
date: Date;
|
||||
long: string;
|
||||
lat: string;
|
||||
status: TourStatus;
|
||||
}
|
||||
|
||||
export const fakeTourPlan: TourPlanType[] = [
|
||||
{
|
||||
id: 1,
|
||||
district: "Urgut",
|
||||
user: FakeUserList[0],
|
||||
date: new Date("2025-12-01T09:00:00"),
|
||||
long: "69.2401",
|
||||
lat: "41.3111",
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
district: "Yunusobod",
|
||||
user: FakeUserList[1],
|
||||
userLocation: { long: "69.2310", lat: "41.3210" },
|
||||
date: new Date("2025-12-02T14:30:00"),
|
||||
long: "69.2300",
|
||||
lat: "41.3200",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
district: "Bekobod",
|
||||
user: FakeUserList[2],
|
||||
date: new Date("2025-12-03T11:00:00"),
|
||||
long: "69.2500",
|
||||
lat: "41.3150",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
district: "Yunusobod",
|
||||
user: FakeUserList[3],
|
||||
userLocation: { long: "69.2460", lat: "41.3110" },
|
||||
date: new Date("2025-12-04T16:00:00"),
|
||||
long: "69.2450",
|
||||
lat: "41.3100",
|
||||
status: "planned",
|
||||
},
|
||||
];
|
||||
9
src/features/tour-plan/lib/form.ts
Normal file
9
src/features/tour-plan/lib/form.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import z from "zod";
|
||||
|
||||
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" }),
|
||||
});
|
||||
210
src/features/tour-plan/ui/AddedTourPlan.tsx
Normal file
210
src/features/tour-plan/ui/AddedTourPlan.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
import type { TourPlanType } from "@/features/tour-plan/lib/data";
|
||||
import { tourPlanForm } from "@/features/tour-plan/lib/form";
|
||||
import { FakeUserList } from "@/features/users/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} from "@/shared/ui/form";
|
||||
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 { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
interface Props {
|
||||
initialValues: TourPlanType | null;
|
||||
setDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
setPlans: Dispatch<SetStateAction<TourPlanType[]>>;
|
||||
}
|
||||
|
||||
const AddedTourPlan = ({ initialValues, setDialogOpen, setPlans }: Props) => {
|
||||
const [load, setLoad] = useState<boolean>(false);
|
||||
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",
|
||||
user: initialValues?.user.id.toString() || "",
|
||||
},
|
||||
});
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const lat = form.watch("lat");
|
||||
const long = form.watch("long");
|
||||
|
||||
const handleMapClick = (e: { get: (key: string) => number[] }) => {
|
||||
const coords = e.get("coords");
|
||||
form.setValue("lat", coords[0].toString());
|
||||
form.setValue("long", coords[1].toString());
|
||||
};
|
||||
|
||||
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];
|
||||
}
|
||||
});
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
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>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="district"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Manzil</Label>
|
||||
<FormControl>
|
||||
<Input placeholder="Manzil nomi" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label>Boradigan kuni</Label>
|
||||
<FormControl>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-full justify-between font-normal"
|
||||
>
|
||||
{field.value
|
||||
? new Date(field.value).toLocaleDateString()
|
||||
: "Boradigan kuni"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={field.value ? new Date(field.value) : undefined}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(val) => {
|
||||
field.onChange(val);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<div className="h-[300px] w-full border rounded-lg overflow-hidden">
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{ center: [Number(lat), Number(long)], zoom: 16 }}
|
||||
width="100%"
|
||||
height="300px"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[Number(lat), Number(long)]} />
|
||||
<Circle
|
||||
geometry={[[Number(lat), Number(long)], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0,150,255,0.2)",
|
||||
strokeColor: "rgba(0,150,255,0.8)",
|
||||
strokeWidth: 2,
|
||||
interactivityModel: "default#transparent",
|
||||
}}
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full bg-blue-500 hover:bg-blue-500 cursor-pointer"
|
||||
>
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialValues ? (
|
||||
"Tahrirlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddedTourPlan;
|
||||
100
src/features/tour-plan/ui/TourPlanDetailDialog.tsx
Normal file
100
src/features/tour-plan/ui/TourPlanDetailDialog.tsx
Normal file
@@ -0,0 +1,100 @@
|
||||
"use client";
|
||||
|
||||
import type { TourPlanType } from "@/features/tour-plan/lib/data";
|
||||
import { Badge } from "@/shared/ui/badge";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Circle, Map, Placemark, YMaps } from "@pbe/react-yandex-maps";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
plan: TourPlanType | null;
|
||||
}
|
||||
|
||||
const TourPlanDetailDialog = ({ open, setOpen, plan }: Props) => {
|
||||
if (!plan) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-bold">
|
||||
Tour Plan Detili
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Foydalanuvchi */}
|
||||
<div>
|
||||
<p className="font-semibold">Foydalanuvchi:</p>
|
||||
<p>
|
||||
{plan.user.firstName} {plan.user.lastName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* District */}
|
||||
<div>
|
||||
<p className="font-semibold">Hudud:</p>
|
||||
<p>{plan.district}</p>
|
||||
</div>
|
||||
|
||||
{/* Sana */}
|
||||
<div>
|
||||
<p className="font-semibold">Sana:</p>
|
||||
<p>{plan.date.toLocaleString()}</p>
|
||||
</div>
|
||||
|
||||
{/* Status */}
|
||||
<div>
|
||||
<p className="font-semibold">Status:</p>
|
||||
<Badge
|
||||
className={
|
||||
plan.status === "completed" ? "bg-green-600" : "bg-yellow-500"
|
||||
}
|
||||
>
|
||||
{plan.status === "completed" ? "Bajarilgan" : "Rejalashtirilgan"}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{plan.userLocation && (
|
||||
<YMaps>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [
|
||||
Number(plan.userLocation.lat),
|
||||
Number(plan.userLocation.long),
|
||||
],
|
||||
zoom: 16,
|
||||
}}
|
||||
width="100%"
|
||||
height="300px"
|
||||
>
|
||||
<Placemark
|
||||
geometry={[
|
||||
Number(plan.userLocation.lat),
|
||||
Number(plan.userLocation.long),
|
||||
]}
|
||||
/>
|
||||
<Circle
|
||||
geometry={[[Number(plan.lat), Number(plan.long)], 100]}
|
||||
options={{
|
||||
fillColor: "rgba(0, 150, 255, 0.2)",
|
||||
strokeColor: "rgba(0, 150, 255, 0.8)",
|
||||
strokeWidth: 2,
|
||||
}}
|
||||
/>
|
||||
</Map>
|
||||
</YMaps>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default TourPlanDetailDialog;
|
||||
271
src/features/tour-plan/ui/TourPlanList.tsx
Normal file
271
src/features/tour-plan/ui/TourPlanList.tsx
Normal file
@@ -0,0 +1,271 @@
|
||||
import { fakeTourPlan, type TourPlanType } from "@/features/tour-plan/lib/data";
|
||||
import AddedTourPlan from "@/features/tour-plan/ui/AddedTourPlan";
|
||||
import TourPlanDetailDialog from "@/features/tour-plan/ui/TourPlanDetailDialog";
|
||||
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";
|
||||
|
||||
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 [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 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>
|
||||
|
||||
<TourPlanDetailDialog
|
||||
plan={detail}
|
||||
setOpen={setDetailOpen}
|
||||
open={detailOpen}
|
||||
/>
|
||||
</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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TourPlanList;
|
||||
52
src/features/users/lib/data.ts
Normal file
52
src/features/users/lib/data.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export interface User {
|
||||
id: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
region: string;
|
||||
isActive: boolean;
|
||||
}
|
||||
|
||||
export const FakeUserList: User[] = [
|
||||
{
|
||||
id: 1,
|
||||
firstName: "Samandar",
|
||||
lastName: "Turgunboyev",
|
||||
region: "Toshkent",
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
firstName: "Azizbek",
|
||||
lastName: "Usmonov",
|
||||
region: "Samarqand",
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
firstName: "Ali",
|
||||
lastName: "Valiyev",
|
||||
region: "Buxoro",
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
firstName: "Laylo",
|
||||
lastName: "Karimova",
|
||||
region: "Namangan",
|
||||
isActive: false,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
firstName: "Davron",
|
||||
lastName: "Usmonov",
|
||||
region: "Andijon",
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
firstName: "Gulbahor",
|
||||
lastName: "Rashidova",
|
||||
region: "Farg‘ona",
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
8
src/features/users/lib/form.ts
Normal file
8
src/features/users/lib/form.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import z from "zod";
|
||||
|
||||
export const AddedUser = z.object({
|
||||
firstName: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
lastName: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
region: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
isActive: z.string().min(1, { message: "Majburiy maydon" }),
|
||||
});
|
||||
182
src/features/users/ui/AddUsers.tsx
Normal file
182
src/features/users/ui/AddUsers.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import type { User } from "@/features/users/lib/data";
|
||||
import { AddedUser } from "@/features/users/lib/form";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormMessage,
|
||||
} 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 { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
interface UserFormProps {
|
||||
initialData?: User | null;
|
||||
setUsers: React.Dispatch<React.SetStateAction<User[]>>;
|
||||
setDialogOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const AddUsers = ({ initialData, setUsers, setDialogOpen }: UserFormProps) => {
|
||||
const [load, setLoad] = useState(false);
|
||||
const form = useForm<z.infer<typeof AddedUser>>({
|
||||
resolver: zodResolver(AddedUser),
|
||||
defaultValues: {
|
||||
firstName: initialData?.firstName || "",
|
||||
lastName: initialData?.lastName || "",
|
||||
region: initialData?.region || "",
|
||||
isActive: initialData ? String(initialData.isActive) : "true",
|
||||
},
|
||||
});
|
||||
|
||||
function onSubmit(values: z.infer<typeof AddedUser>) {
|
||||
setLoad(true);
|
||||
if (initialData) {
|
||||
setTimeout(() => {
|
||||
setUsers((prev) =>
|
||||
prev.map((user) =>
|
||||
user.id === initialData.id
|
||||
? {
|
||||
...user,
|
||||
...values,
|
||||
isActive: values.isActive === "true" ? true : false,
|
||||
}
|
||||
: user,
|
||||
),
|
||||
);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setUsers((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: prev.length ? prev[prev.length - 1].id + 1 : 1,
|
||||
...values,
|
||||
isActive: values.isActive === "true" ? true : false,
|
||||
},
|
||||
]);
|
||||
setLoad(false);
|
||||
setDialogOpen(false);
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="firstName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Ismi</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Ismi"
|
||||
{...field}
|
||||
className="!h-12 !text-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="lastName"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Familiyasi</Label>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="Familiyasi"
|
||||
{...field}
|
||||
className="!h-12 !text-md"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="region"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md">Hududi</Label>
|
||||
<FormControl>
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<SelectTrigger className="w-full !h-12" value={field.value}>
|
||||
<SelectValue placeholder="Hududni tanlang" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Toshkent">Toshkent</SelectItem>
|
||||
<SelectItem value="Samarqand">Samarqand</SelectItem>
|
||||
<SelectItem value="Bekobod">Bekobod</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="isActive"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<Label className="text-md mr-4">Foydalanuvchi holati</Label>
|
||||
<FormControl>
|
||||
<Select
|
||||
value={String(field.value)}
|
||||
onValueChange={field.onChange}
|
||||
>
|
||||
<SelectTrigger className="w-full !h-12">
|
||||
<SelectValue placeholder="Foydalanuvchi holati" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="true">Faol</SelectItem>
|
||||
<SelectItem value="false">Faol emas</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer"
|
||||
>
|
||||
{load ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : initialData ? (
|
||||
"Saqlash"
|
||||
) : (
|
||||
"Qo'shish"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddUsers;
|
||||
295
src/features/users/ui/UsersList.tsx
Normal file
295
src/features/users/ui/UsersList.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import SidebarLayout from "@/SidebarLayout";
|
||||
import { FakeUserList, type User } from "@/features/users/lib/data";
|
||||
import AddUsers from "@/features/users/ui/AddUsers";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/shared/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
import clsx from "clsx";
|
||||
import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
const UsersList = () => {
|
||||
const [users, setUsers] = useState<User[]>(FakeUserList);
|
||||
|
||||
const [editingUser, setEditingUser] = useState<User | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [statusFilter, setStatusFilter] = useState<
|
||||
"all" | "active" | "inactive"
|
||||
>("all");
|
||||
const [regionFilter, setRegionFilter] = useState<string>("all");
|
||||
|
||||
// Pagination state
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 5;
|
||||
|
||||
const handleDelete = (id: number) => {
|
||||
setUsers(users.filter((u) => u.id !== id));
|
||||
};
|
||||
|
||||
// Filter & search users
|
||||
const filteredUsers = useMemo(() => {
|
||||
return users.filter((user) => {
|
||||
const matchesSearch =
|
||||
user.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
user.lastName.toLowerCase().includes(searchTerm.toLowerCase());
|
||||
|
||||
const matchesStatus =
|
||||
statusFilter === "all" ||
|
||||
(statusFilter === "active" && user.isActive) ||
|
||||
(statusFilter === "inactive" && !user.isActive);
|
||||
|
||||
const matchesRegion =
|
||||
regionFilter === "all" || user.region === regionFilter;
|
||||
|
||||
return matchesSearch && matchesStatus && matchesRegion;
|
||||
});
|
||||
}, [users, searchTerm, statusFilter, regionFilter]);
|
||||
|
||||
const totalPages = Math.ceil(filteredUsers.length / pageSize);
|
||||
const paginatedUsers = filteredUsers.slice(
|
||||
(currentPage - 1) * pageSize,
|
||||
currentPage * pageSize,
|
||||
);
|
||||
|
||||
// Hududlarni filter qilish uchun
|
||||
const regions = Array.from(new Set(users.map((u) => u.region)));
|
||||
|
||||
return (
|
||||
<SidebarLayout>
|
||||
<div className="flex flex-col h-full p-10 w-full">
|
||||
{/* Top Controls */}
|
||||
<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">Foydalanuvchilar ro'yxati</h1>
|
||||
|
||||
<div className="flex flex-wrap gap-2 items-center">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Ism yoki Familiyasi bo'yicha qidirish"
|
||||
className="border rounded px-3 py-2 text-sm w-64"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
|
||||
<Select
|
||||
value={statusFilter}
|
||||
onValueChange={(val) =>
|
||||
setStatusFilter(val as "all" | "active" | "inactive")
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Holati" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Barchasi</SelectItem>
|
||||
<SelectItem value="active">Faol</SelectItem>
|
||||
<SelectItem value="inactive">Faol emas</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
value={regionFilter}
|
||||
onValueChange={(val) => setRegionFilter(val)}
|
||||
>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue placeholder="Hududi" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">Barchasi</SelectItem>
|
||||
{regions.map((r) => (
|
||||
<SelectItem key={r} value={r}>
|
||||
{r}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-blue-500 cursor-pointer hover:bg-blue-500"
|
||||
onClick={() => setEditingUser(null)}
|
||||
>
|
||||
<Plus className="!h-5 !w-5" /> Qo'shish
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{editingUser
|
||||
? "Foydalanuvchini tahrirlash"
|
||||
: "Foydalanuvchi qo'shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<AddUsers
|
||||
initialData={editingUser}
|
||||
setUsers={setUsers}
|
||||
setDialogOpen={setDialogOpen}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="text-[16px] text-center">
|
||||
<TableHead className="text-start">ID</TableHead>
|
||||
<TableHead className="text-start">Ismi</TableHead>
|
||||
<TableHead className="text-start">Familiyasi</TableHead>
|
||||
<TableHead className="text-start">Hududi</TableHead>
|
||||
<TableHead className="text-center">Holati</TableHead>
|
||||
<TableHead className="text-right">Harakatlar</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedUsers.length > 0 ? (
|
||||
paginatedUsers.map((user) => (
|
||||
<TableRow key={user.id} className="text-[14px] text-start">
|
||||
<TableCell>{user.id}</TableCell>
|
||||
<TableCell>{user.firstName}</TableCell>
|
||||
<TableCell>{user.lastName}</TableCell>
|
||||
<TableCell>{user.region}</TableCell>
|
||||
<TableCell>
|
||||
<Select
|
||||
value={user.isActive ? "active" : "inactive"}
|
||||
onValueChange={(val) => {
|
||||
setUsers(
|
||||
users.map((u) =>
|
||||
u.id === user.id
|
||||
? { ...u, isActive: val === "active" }
|
||||
: u,
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger
|
||||
className={clsx(
|
||||
"w-[180px] mx-auto",
|
||||
user.isActive
|
||||
? "bg-green-100 text-green-800"
|
||||
: "bg-red-100 text-red-800",
|
||||
)}
|
||||
>
|
||||
<SelectValue placeholder="Holati" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
value="active"
|
||||
className="text-green-500 hover:!text-green-500"
|
||||
>
|
||||
Faol
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
value="inactive"
|
||||
className="text-red-500 hover:!text-red-500"
|
||||
>
|
||||
Faol emas
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
<TableCell className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingUser(user);
|
||||
setDialogOpen(true);
|
||||
}}
|
||||
className="bg-blue-500 text-white hover:bg-blue-500 hover:text-white cursor-pointer"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(user.id)}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="text-center py-4 text-lg">
|
||||
Foydalanuvchilar topilmadi.
|
||||
</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
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="cursor-pointer"
|
||||
disabled={currentPage === 1}
|
||||
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"
|
||||
className="cursor-pointer"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() =>
|
||||
setCurrentPage((prev) => Math.min(prev + 1, totalPages))
|
||||
}
|
||||
>
|
||||
<ChevronRight />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default UsersList;
|
||||
Reference in New Issue
Block a user