Compare commits
13 Commits
0c647ff5ff
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6580761cdd | ||
|
|
19d86d619b | ||
|
|
cbbe2f83e2 | ||
|
|
9d84fc38f3 | ||
|
|
db6bfa7e40 | ||
|
|
21725762c6 | ||
|
|
ba419b8de4 | ||
|
|
2fb567d93f | ||
|
|
d4788c7cb2 | ||
|
|
5c4e1327be | ||
|
|
6cf0a4200b | ||
|
|
d224cbeb38 | ||
|
|
85c0eee6dd |
17
package-lock.json
generated
17
package-lock.json
generated
@@ -37,6 +37,7 @@
|
|||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
@@ -4007,6 +4008,22 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cmdk": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-compose-refs": "^1.1.1",
|
||||||
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
|
"@radix-ui/react-id": "^1.1.0",
|
||||||
|
"@radix-ui/react-primitive": "^2.0.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color-convert": {
|
"node_modules/color-convert": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "npm install --legacy-peer-deps && tsc -b && vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"lint": "eslint src --fix",
|
"lint": "eslint src --fix",
|
||||||
"prettier": "prettier src --write",
|
"prettier": "prettier src --write",
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"cmdk": "^1.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dayjs": "^1.11.18",
|
"dayjs": "^1.11.18",
|
||||||
"framer-motion": "^12.23.24",
|
"framer-motion": "^12.23.24",
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ import z from "zod";
|
|||||||
import { auth_api } from "../lib/api";
|
import { auth_api } from "../lib/api";
|
||||||
import { loginform } from "../lib/form";
|
import { loginform } from "../lib/form";
|
||||||
|
|
||||||
export default function LoginForm() {
|
interface LoginFormProps {
|
||||||
|
onLogin: (body: { telegram_id: string }) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function LoginForm({ onLogin }: LoginFormProps) {
|
||||||
const { user, setLoginUser } = userInfoStore();
|
const { user, setLoginUser } = userInfoStore();
|
||||||
|
|
||||||
const form = useForm<z.infer<typeof loginform>>({
|
const form = useForm<z.infer<typeof loginform>>({
|
||||||
@@ -66,6 +70,7 @@ export default function LoginForm() {
|
|||||||
first_name: form.getValues("firstName"),
|
first_name: form.getValues("firstName"),
|
||||||
last_name: form.getValues("lastName"),
|
last_name: form.getValues("lastName"),
|
||||||
});
|
});
|
||||||
|
onLogin({ telegram_id: user.user_id });
|
||||||
},
|
},
|
||||||
onError: (error: AxiosError) => {
|
onError: (error: AxiosError) => {
|
||||||
const data = error.response?.data as { message?: string };
|
const data = error.response?.data as { message?: string };
|
||||||
|
|||||||
21
src/features/distributed/lib/api.ts
Normal file
21
src/features/distributed/lib/api.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { DistributedList } from "@/features/distributed/lib/data";
|
||||||
|
import httpClient from "@/shared/config/api/httpClient";
|
||||||
|
import { DISTRIBUTED_CREATE, DISTRIBUTED_LIST } from "@/shared/config/api/URLs";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
export const distributed_api = {
|
||||||
|
async list(): Promise<AxiosResponse<DistributedList>> {
|
||||||
|
const res = await httpClient.get(DISTRIBUTED_LIST);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(body: {
|
||||||
|
product_id: number;
|
||||||
|
date: string;
|
||||||
|
employee_name: string;
|
||||||
|
quantity: number;
|
||||||
|
}) {
|
||||||
|
const res = await httpClient.post(DISTRIBUTED_CREATE, body);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
75
src/features/distributed/lib/column.tsx
Normal file
75
src/features/distributed/lib/column.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import type { DistributedListData } from "@/features/distributed/lib/data";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from "@/shared/ui/dropdown-menu";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { EllipsisVertical, Eye } from "lucide-react";
|
||||||
|
|
||||||
|
interface ColumnProps {
|
||||||
|
handleEdit: (district: DistributedListData) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const columnsDistributed = ({
|
||||||
|
handleEdit,
|
||||||
|
}: ColumnProps): ColumnDef<DistributedListData>[] => [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: () => <div className="text-center">№</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center font-medium">{row.index + 1}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: () => <div className="text-center">Xaridoring ismi</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center font-medium">
|
||||||
|
{row.original.employee_name}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name-product",
|
||||||
|
header: () => <div className="text-center">Mahsulot nomi</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center font-medium">{row.original.product.name}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name-product",
|
||||||
|
header: () => <div className="text-center">Soni</div>,
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="text-center font-medium">{row.original.quantity}</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "actions",
|
||||||
|
header: () => <div className="text-center">Amallar</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const district = row.original;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button variant="ghost" className="w-full h-full hover:bg-gray-100">
|
||||||
|
<EllipsisVertical className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="center" className="flex flex-col gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
className="flex items-center gap-1 px-4 py-2 w-full text-left hover:bg-green-400 hover:text-white text-white bg-green-400"
|
||||||
|
onClick={() => handleEdit(district)}
|
||||||
|
>
|
||||||
|
<Eye size={16} />
|
||||||
|
<p>Batafsil</p>
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
80
src/features/distributed/lib/data-table.tsx
Normal file
80
src/features/distributed/lib/data-table.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
type ColumnDef,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/shared/ui/table";
|
||||||
|
|
||||||
|
interface DataTableProps<MyDiscrictData, TValue> {
|
||||||
|
columns: ColumnDef<MyDiscrictData, TValue>[];
|
||||||
|
data: MyDiscrictData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableDistributed<TData, TValue>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: DataTableProps<TData, TValue>) {
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-md border">
|
||||||
|
<Table className="">
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id} className="border-r">
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className="border-r">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
Hech narsa yo'q
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
24
src/features/distributed/lib/data.ts
Normal file
24
src/features/distributed/lib/data.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
export interface DistributedList {
|
||||||
|
status_code: number;
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
count: number;
|
||||||
|
next: null | string;
|
||||||
|
previous: null | string;
|
||||||
|
results: DistributedListData[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DistributedListData {
|
||||||
|
id: number;
|
||||||
|
product: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
price: number;
|
||||||
|
};
|
||||||
|
quantity: number;
|
||||||
|
employee_name: string;
|
||||||
|
created_at: string;
|
||||||
|
date: string;
|
||||||
|
}
|
||||||
208
src/features/distributed/ui/DistributedAddModal.tsx
Normal file
208
src/features/distributed/ui/DistributedAddModal.tsx
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
import { distributed_api } from "@/features/distributed/lib/api";
|
||||||
|
import { order_api } from "@/features/specification/lib/api";
|
||||||
|
import formatDate from "@/shared/lib/formatDate";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import { Calendar } from "@/shared/ui/calendar";
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from "@/shared/ui/command";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
import { Input } from "@/shared/ui/input";
|
||||||
|
import { Label } from "@/shared/ui/label";
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { ChevronDownIcon, ChevronsUpDown } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: (v: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DistributedAddModal({ open, setOpen }: Props) {
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
product_id: 0,
|
||||||
|
date: "",
|
||||||
|
employee_name: "",
|
||||||
|
quantity: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = useQueryClient();
|
||||||
|
|
||||||
|
const { data: product } = useQuery({
|
||||||
|
queryKey: ["product_list"],
|
||||||
|
queryFn: () => order_api.product_list(),
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const [openProduct, setOpenProduct] = useState(false);
|
||||||
|
const [searchProduct, setSearchProduct] = useState("");
|
||||||
|
const [openDate, setOpenData] = useState(false);
|
||||||
|
|
||||||
|
const selectedProduct = product?.find((x) => x.id === form.product_id);
|
||||||
|
|
||||||
|
const createMutation = useMutation({
|
||||||
|
mutationFn: () => distributed_api.create(form),
|
||||||
|
onSuccess: () => {
|
||||||
|
client.invalidateQueries({ queryKey: ["distributed_list"] });
|
||||||
|
setOpen(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
createMutation.mutate();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="space-y-4 max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Yangi topshirilgan mahsulot qo‘shish</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 space-y-3">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label>Topshirilga mahsulot</Label>
|
||||||
|
<Popover open={openProduct} onOpenChange={setOpenProduct}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
aria-expanded={openProduct}
|
||||||
|
className="w-full h-11 justify-between"
|
||||||
|
>
|
||||||
|
{selectedProduct ? selectedProduct.name : "Mahsulot tanlang"}
|
||||||
|
<ChevronsUpDown className="ml-2 h-4 w-4 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
className="w-[--radix-popover-trigger-width] p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Command shouldFilter={false}>
|
||||||
|
<CommandInput
|
||||||
|
placeholder="Qidirish..."
|
||||||
|
className="h-9"
|
||||||
|
value={searchProduct}
|
||||||
|
onValueChange={setSearchProduct}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CommandList>
|
||||||
|
{product && product.length > 0 ? (
|
||||||
|
<CommandGroup>
|
||||||
|
{product
|
||||||
|
.filter((p) =>
|
||||||
|
p.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(searchProduct.toLowerCase()),
|
||||||
|
)
|
||||||
|
.map((p) => (
|
||||||
|
<CommandItem
|
||||||
|
key={p.id}
|
||||||
|
value={`${p.id}`}
|
||||||
|
onSelect={() => {
|
||||||
|
setForm({ ...form, product_id: p.id });
|
||||||
|
setOpenProduct(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{p.name}
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
) : (
|
||||||
|
<CommandEmpty>Mahsulot topilmadi</CommandEmpty>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label>Mahsulot soni</Label>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="Miqdori"
|
||||||
|
value={form.quantity}
|
||||||
|
onChange={(e) => {
|
||||||
|
const val = e.target.value;
|
||||||
|
setForm({ ...form, quantity: val === "" ? 0 : Number(val) });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label>Xaridorning ismi</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Xodim ismi"
|
||||||
|
value={form.employee_name}
|
||||||
|
onChange={(e) =>
|
||||||
|
setForm({ ...form, employee_name: e.target.value })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label htmlFor="date" className="px-1">
|
||||||
|
Topshirilgan sana
|
||||||
|
</Label>
|
||||||
|
|
||||||
|
<Popover open={openDate} onOpenChange={setOpenData}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
id="date"
|
||||||
|
className="w-full h-12 justify-between font-normal"
|
||||||
|
>
|
||||||
|
{form.date ? form.date : "Sanani kiriting"}
|
||||||
|
<ChevronDownIcon />
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent
|
||||||
|
className="w-auto overflow-hidden p-0"
|
||||||
|
align="start"
|
||||||
|
>
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={new Date(form.date)}
|
||||||
|
captionLayout="dropdown"
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (date) {
|
||||||
|
setForm({
|
||||||
|
...form,
|
||||||
|
date: formatDate.format(date, "YYYY-MM-DD"),
|
||||||
|
});
|
||||||
|
setOpenData(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={handleSubmit}
|
||||||
|
disabled={createMutation.isPending}
|
||||||
|
className="h-12"
|
||||||
|
>
|
||||||
|
{createMutation.isPending ? "Saqlanmoqda..." : "Saqlash"}
|
||||||
|
</Button>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
src/features/distributed/ui/DistributedDetail.tsx
Normal file
82
src/features/distributed/ui/DistributedDetail.tsx
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import type { DistributedListData } from "@/features/distributed/lib/data";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
import { type Dispatch, type SetStateAction } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
|
specification: DistributedListData | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DistributedDetail = ({ open, setOpen, specification }: Props) => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
|
<DialogContent className="max-w-[90%] max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader className="border-b pb-4">
|
||||||
|
<DialogTitle className="text-2xl font-bold text-gray-800">
|
||||||
|
Tafsilot
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-6 mt-6">
|
||||||
|
{/* Asosiy ma'lumotlar - Grid */}
|
||||||
|
<div className="grid grid-cols-1">
|
||||||
|
{/* Xaridor */}
|
||||||
|
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
|
||||||
|
<p className="text-sm text-blue-600 font-medium mb-1">
|
||||||
|
Xaridorning ismi
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-800">
|
||||||
|
{specification?.employee_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Foydalanuvchi */}
|
||||||
|
|
||||||
|
<div className="bg-gradient-to-br mt-5 from-green-50 to-green-100 rounded-lg p-4 border border-green-200 md:col-span-2">
|
||||||
|
<p className="text-sm text-green-600 font-medium mb-1">
|
||||||
|
Topshirilgan sanasi
|
||||||
|
</p>
|
||||||
|
<p className="text-lg font-semibold text-gray-800">
|
||||||
|
{specification?.date}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Dorilar ro'yxati */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
||||||
|
<h3 className="text-lg font-bold text-gray-800 mb-4 flex items-center">
|
||||||
|
Topshirilgan dori
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="bg-white rounded-lg p-4 border border-gray-200 hover:border-indigo-300 transition-colors">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div className="flex-1">
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<p className="font-semibold text-gray-800">
|
||||||
|
{specification?.product.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||||
|
<span>
|
||||||
|
Miqdor: <strong>{specification?.quantity} ta</strong>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DistributedDetail;
|
||||||
66
src/features/distributed/ui/DistributedList.tsx
Normal file
66
src/features/distributed/ui/DistributedList.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { distributed_api } from "@/features/distributed/lib/api";
|
||||||
|
import { columnsDistributed } from "@/features/distributed/lib/column";
|
||||||
|
import type { DistributedListData } from "@/features/distributed/lib/data";
|
||||||
|
import { DataTableDistributed } from "@/features/distributed/lib/data-table";
|
||||||
|
import DistributedAddModal from "@/features/distributed/ui/DistributedAddModal";
|
||||||
|
import DistributedDetail from "@/features/distributed/ui/DistributedDetail";
|
||||||
|
import AddedButton from "@/shared/ui/added-button";
|
||||||
|
import { Skeleton } from "@/shared/ui/skeleton";
|
||||||
|
import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const DistributedList = () => {
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["distributed_list"],
|
||||||
|
queryFn: () => distributed_api.list(),
|
||||||
|
});
|
||||||
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [added, setAdded] = useState<boolean>(false);
|
||||||
|
const [detail, setDetail] = useState<DistributedListData | null>(null);
|
||||||
|
|
||||||
|
const handleEdit = (district: DistributedListData) => {
|
||||||
|
setDetail(district);
|
||||||
|
setOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const columns = columnsDistributed({
|
||||||
|
handleEdit,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout link="/">
|
||||||
|
<AddedButton onClick={() => setAdded(true)} />
|
||||||
|
<>
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-3xl font-bold">Topshirilgan mahsulotlar</h1>
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-12 w-full rounded-md" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<p className="text-red-500">
|
||||||
|
Tumanlar yuklanmadi. Qayta urinib ko‘ring.
|
||||||
|
</p>
|
||||||
|
) : data ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<DataTableDistributed
|
||||||
|
columns={columns}
|
||||||
|
data={data.data.data.results}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">Tumanlar mavjud emas</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
<DistributedDetail open={open} setOpen={setOpen} specification={detail} />
|
||||||
|
<DistributedAddModal open={added} setOpen={setAdded} />
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DistributedList;
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/ui/dropdown-menu";
|
} from "@/shared/ui/dropdown-menu";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Edit, EllipsisVertical, Trash } from "lucide-react";
|
import { Edit, EllipsisVertical } from "lucide-react";
|
||||||
|
|
||||||
interface ColumnProps {
|
interface ColumnProps {
|
||||||
handleEdit: (district: MyDiscrictData) => void;
|
handleEdit: (district: MyDiscrictData) => void;
|
||||||
@@ -15,7 +15,6 @@ interface ColumnProps {
|
|||||||
|
|
||||||
export const columnsDistrict = ({
|
export const columnsDistrict = ({
|
||||||
handleEdit,
|
handleEdit,
|
||||||
onDeleteClick,
|
|
||||||
}: ColumnProps): ColumnDef<MyDiscrictData>[] => [
|
}: ColumnProps): ColumnDef<MyDiscrictData>[] => [
|
||||||
{
|
{
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
@@ -52,13 +51,13 @@ export const columnsDistrict = ({
|
|||||||
>
|
>
|
||||||
<Edit size={16} /> Tahrirlash
|
<Edit size={16} /> Tahrirlash
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{/* <Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="flex items-center gap-1 px-4 py-2 w-full text-left hover:bg-red-400 hover:text-white text-white bg-red-400"
|
className="flex items-center gap-1 px-4 py-2 w-full text-left hover:bg-red-400 hover:text-white text-white bg-red-400"
|
||||||
onClick={() => onDeleteClick(district)} // faqat signal yuboradi
|
onClick={() => onDeleteClick(district)}
|
||||||
>
|
>
|
||||||
<Trash size={16} /> O‘chirish
|
<Trash size={16} /> O‘chirish
|
||||||
</Button>
|
</Button> */}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function DataTableDistruct<TData, TValue>({
|
|||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
No results.
|
Tuman mavjud emas
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -190,13 +190,9 @@ export default function District() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="space-y-6">
|
<>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-foreground">Tumanlar</h1>
|
|
||||||
<p className="text-muted-foreground mt-1">Tumanlarni boshqarish</p>
|
|
||||||
</div>
|
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<AddedButton onClick={() => form.reset({ name: "" })} />
|
<AddedButton onClick={() => form.reset({ name: "" })} />
|
||||||
@@ -260,7 +256,7 @@ export default function District() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6 mt-5">
|
<div className="space-y-6">
|
||||||
<h1 className="text-3xl font-bold">Tumanlar ro‘yxati</h1>
|
<h1 className="text-3xl font-bold">Tumanlar ro‘yxati</h1>
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
@@ -282,7 +278,7 @@ export default function District() {
|
|||||||
<p className="text-gray-500">Tumanlar mavjud emas</p>
|
<p className="text-gray-500">Tumanlar mavjud emas</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
|
||||||
<Dialog open={deleteDialog} onOpenChange={setDeleteDialog}>
|
<Dialog open={deleteDialog} onOpenChange={setDeleteDialog}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/shared/ui/dropdown-menu";
|
} from "@/shared/ui/dropdown-menu";
|
||||||
import type { ColumnDef } from "@tanstack/react-table";
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
import { Edit, EllipsisVertical, Eye, Trash } from "lucide-react";
|
import { Edit, EllipsisVertical, Eye } from "lucide-react";
|
||||||
import type { NavigateFunction } from "react-router-dom";
|
import type { NavigateFunction } from "react-router-dom";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -15,10 +15,7 @@ interface Props {
|
|||||||
onDeleteClick: (district: DoctorListData) => void;
|
onDeleteClick: (district: DoctorListData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const columns = ({
|
export const columns = ({ router }: Props): ColumnDef<DoctorListData>[] => [
|
||||||
router,
|
|
||||||
onDeleteClick,
|
|
||||||
}: Props): ColumnDef<DoctorListData>[] => [
|
|
||||||
{
|
{
|
||||||
accessorKey: "id",
|
accessorKey: "id",
|
||||||
header: () => <div className="text-center">№</div>,
|
header: () => <div className="text-center">№</div>,
|
||||||
@@ -95,14 +92,14 @@ export const columns = ({
|
|||||||
>
|
>
|
||||||
<Edit size={16} /> Tahrirlash
|
<Edit size={16} /> Tahrirlash
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{/* <Button
|
||||||
variant={"destructive"}
|
variant={"destructive"}
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => onDeleteClick(obj)}
|
onClick={() => onDeleteClick(obj)}
|
||||||
>
|
>
|
||||||
<Trash size={16} /> {"O'chirish"}
|
<Trash size={16} /> {"O'chirish"}
|
||||||
</Button>
|
</Button> */}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function DataTable<TData, TValue>({
|
|||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
No results.
|
Shifokor mavjud emas
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -345,7 +345,7 @@ const CreateDoctor = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/doctor">
|
||||||
<div className="max-w-3xl mx-auto space-y-6">
|
<div className="max-w-3xl mx-auto space-y-6">
|
||||||
<h1 className="text-3xl font-bold">{"Qo'shish"}</h1>
|
<h1 className="text-3xl font-bold">{"Qo'shish"}</h1>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -509,7 +509,7 @@ const CreateDoctor = () => {
|
|||||||
name="streets"
|
name="streets"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{"Ko'cha"}</FormLabel>
|
<FormLabel>{"Obyekt"}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
key={field.value}
|
key={field.value}
|
||||||
@@ -520,7 +520,7 @@ const CreateDoctor = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full h-12">
|
<SelectTrigger className="w-full h-12">
|
||||||
<SelectValue placeholder="Ko'chalar" />
|
<SelectValue placeholder="Obyektlar" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{streets?.map((s) => (
|
{streets?.map((s) => (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const DoctorDetail = () => {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/doctor">
|
||||||
<div className="flex justify-center py-20">
|
<div className="flex justify-center py-20">
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<div className="h-10 w-10 rounded-full border-4 border-gray-300 border-t-primary animate-spin"></div>
|
<div className="h-10 w-10 rounded-full border-4 border-gray-300 border-t-primary animate-spin"></div>
|
||||||
@@ -36,7 +36,7 @@ const DoctorDetail = () => {
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/doctor">
|
||||||
<div className="flex flex-col items-center py-20 gap-4">
|
<div className="flex flex-col items-center py-20 gap-4">
|
||||||
<p className="text-red-500 font-medium">
|
<p className="text-red-500 font-medium">
|
||||||
Ma'lumot yuklashda xatolik yuz berdi.
|
Ma'lumot yuklashda xatolik yuz berdi.
|
||||||
@@ -49,7 +49,7 @@ const DoctorDetail = () => {
|
|||||||
|
|
||||||
if (!doctor) {
|
if (!doctor) {
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/doctor">
|
||||||
<div className="flex flex-col items-center py-20 gap-3 text-center">
|
<div className="flex flex-col items-center py-20 gap-3 text-center">
|
||||||
<p className="text-gray-500 text-lg">Shifokor topilmadi.</p>
|
<p className="text-gray-500 text-lg">Shifokor topilmadi.</p>
|
||||||
<Button onClick={() => router("/physician")}>
|
<Button onClick={() => router("/physician")}>
|
||||||
@@ -61,7 +61,7 @@ const DoctorDetail = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/doctor">
|
||||||
<div className="max-w-3xl mx-auto bg-white border rounded-xl shadow-sm p-6">
|
<div className="max-w-3xl mx-auto bg-white border rounded-xl shadow-sm p-6">
|
||||||
<h1 className="text-2xl font-bold mb-2">
|
<h1 className="text-2xl font-bold mb-2">
|
||||||
{doctor.first_name} {doctor.last_name}
|
{doctor.first_name} {doctor.last_name}
|
||||||
|
|||||||
@@ -11,19 +11,16 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
import { DashboardLayout } from "@/widgets/dashboard-layout/ui/DashboardLayout";
|
import { DashboardLayout } from "@/widgets/dashboard-layout/ui/DashboardLayout";
|
||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { AxiosError } from "axios";
|
|
||||||
import { Loader2 } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
|
||||||
import { doctor_api } from "../lib/api";
|
import { doctor_api } from "../lib/api";
|
||||||
import { columns } from "../lib/column";
|
import { columns } from "../lib/column";
|
||||||
import { DataTable } from "../lib/data-table";
|
import { DataTable } from "../lib/data-table";
|
||||||
|
|
||||||
const Doctor = () => {
|
const Doctor = () => {
|
||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
const queryClinent = useQueryClient();
|
// const queryClinent = useQueryClient();
|
||||||
const { data, isLoading, isError, refetch } = useQuery({
|
const { data, isLoading, isError, refetch } = useQuery({
|
||||||
queryKey: ["doctor_list"],
|
queryKey: ["doctor_list"],
|
||||||
queryFn: () => doctor_api.list(),
|
queryFn: () => doctor_api.list(),
|
||||||
@@ -32,41 +29,41 @@ const Doctor = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: deleted, isPending: deletedPending } = useMutation({
|
// const { mutate: deleted, isPending: deletedPending } = useMutation({
|
||||||
mutationFn: ({ id }: { id: number }) => doctor_api.delete({ id }),
|
// mutationFn: ({ id }: { id: number }) => doctor_api.delete({ id }),
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
router("/physician");
|
// router("/physician");
|
||||||
queryClinent.refetchQueries({ queryKey: ["doctor_list"] });
|
// queryClinent.refetchQueries({ queryKey: ["doctor_list"] });
|
||||||
setSelectedDistrict(null);
|
// setSelectedDistrict(null);
|
||||||
setDeleteDialog(false);
|
// setDeleteDialog(false);
|
||||||
},
|
// },
|
||||||
onError: (error: AxiosError) => {
|
// onError: (error: AxiosError) => {
|
||||||
const data = error.response?.data as { message?: string };
|
// const data = error.response?.data as { message?: string };
|
||||||
const errorData = error.response?.data as {
|
// const errorData = error.response?.data as {
|
||||||
messages?: {
|
// messages?: {
|
||||||
token_class: string;
|
// token_class: string;
|
||||||
token_type: string;
|
// token_type: string;
|
||||||
message: string;
|
// message: string;
|
||||||
}[];
|
// }[];
|
||||||
};
|
// };
|
||||||
const errorName = error.response?.data as {
|
// const errorName = error.response?.data as {
|
||||||
data?: {
|
// data?: {
|
||||||
name: string[];
|
// name: string[];
|
||||||
};
|
// };
|
||||||
};
|
// };
|
||||||
|
|
||||||
const message =
|
// const message =
|
||||||
Array.isArray(errorName.data?.name) && errorName.data.name.length
|
// Array.isArray(errorName.data?.name) && errorName.data.name.length
|
||||||
? errorName.data.name[0]
|
// ? errorName.data.name[0]
|
||||||
: data?.message ||
|
// : data?.message ||
|
||||||
(Array.isArray(errorData?.messages) && errorData.messages.length
|
// (Array.isArray(errorData?.messages) && errorData.messages.length
|
||||||
? errorData.messages[0].message
|
// ? errorData.messages[0].message
|
||||||
: undefined) ||
|
// : undefined) ||
|
||||||
"Xatolik yuz berdi";
|
// "Xatolik yuz berdi";
|
||||||
|
|
||||||
toast.error(message);
|
// toast.error(message);
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
|
|
||||||
const [deleteDialog, setDeleteDialog] = useState<boolean>(false);
|
const [deleteDialog, setDeleteDialog] = useState<boolean>(false);
|
||||||
const [selectedDistrict, setSelectedDistrict] =
|
const [selectedDistrict, setSelectedDistrict] =
|
||||||
@@ -81,7 +78,7 @@ const Doctor = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<AddedButton onClick={() => router("/physician/added")} />
|
<AddedButton onClick={() => router("/physician/added")} />
|
||||||
</div>
|
</div>
|
||||||
@@ -125,7 +122,7 @@ const Doctor = () => {
|
|||||||
<Button variant="secondary" onClick={() => setDeleteDialog(false)}>
|
<Button variant="secondary" onClick={() => setDeleteDialog(false)}>
|
||||||
Bekor qilish
|
Bekor qilish
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{/* <Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
selectedDistrict && deleted({ id: selectedDistrict.id })
|
selectedDistrict && deleted({ id: selectedDistrict.id })
|
||||||
@@ -136,7 +133,7 @@ const Doctor = () => {
|
|||||||
) : (
|
) : (
|
||||||
"O'chirish"
|
"O'chirish"
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button> */}
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -4,16 +4,18 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
Banknote,
|
||||||
Calendar,
|
Calendar,
|
||||||
Clipboard,
|
|
||||||
FileText,
|
FileText,
|
||||||
HomeIcon,
|
Loader2,
|
||||||
Layers,
|
MapPinCheck,
|
||||||
List,
|
MapPinHouse,
|
||||||
MapPin,
|
MapPinned,
|
||||||
Syringe,
|
Pill,
|
||||||
User,
|
User,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ const navItems: NavItem[] = [
|
|||||||
id: "lokatsiya",
|
id: "lokatsiya",
|
||||||
title: "Lokatsiya jo'natish",
|
title: "Lokatsiya jo'natish",
|
||||||
link: "/location",
|
link: "/location",
|
||||||
icon: <MapPin className="w-8 h-8" />,
|
icon: <MapPinCheck className="w-8 h-8" />,
|
||||||
description: "Manzilni jo'natish",
|
description: "Manzilni jo'natish",
|
||||||
featured: true,
|
featured: true,
|
||||||
},
|
},
|
||||||
@@ -43,25 +45,27 @@ const navItems: NavItem[] = [
|
|||||||
description: "Hujjatlar",
|
description: "Hujjatlar",
|
||||||
featured: true,
|
featured: true,
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
id: "tour_plan",
|
// id: "tour_plan",
|
||||||
title: "Tur plan",
|
// title: "Tur plan",
|
||||||
link: "/tour-plan",
|
// link: "/tour-plan",
|
||||||
icon: <List className="w-6 h-6" />,
|
// icon: <List className="w-6 h-6" />,
|
||||||
description: "Rejalar",
|
// description: "Rejalar",
|
||||||
featured: true,
|
// featured: true,
|
||||||
},
|
// },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const featuredItems = navItems.filter((item) => item.featured);
|
const featuredItems = navItems.filter((item) => item.featured);
|
||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
|
const [locationLoad, setLocationLoad] = useState<boolean>(false);
|
||||||
|
|
||||||
const { mutate } = useMutation({
|
const { mutate } = useMutation({
|
||||||
mutationFn: (body: SendLocation) => location_api.send_loaction(body),
|
mutationFn: (body: SendLocation) => location_api.send_loaction(body),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Lokatsiya jo'natildi");
|
toast.success("Lokatsiya jo'natildi");
|
||||||
router("/location");
|
router("/location");
|
||||||
|
setLocationLoad(false);
|
||||||
},
|
},
|
||||||
onError: (error: AxiosError) => {
|
onError: (error: AxiosError) => {
|
||||||
const data = error.response?.data as { message?: string };
|
const data = error.response?.data as { message?: string };
|
||||||
@@ -77,6 +81,7 @@ export default function Home() {
|
|||||||
name: string[];
|
name: string[];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
setLocationLoad(false);
|
||||||
|
|
||||||
const message =
|
const message =
|
||||||
Array.isArray(errorName.data?.name) && errorName.data.name.length
|
Array.isArray(errorName.data?.name) && errorName.data.name.length
|
||||||
@@ -92,6 +97,7 @@ export default function Home() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleLocationClick = () => {
|
const handleLocationClick = () => {
|
||||||
|
setLocationLoad(true);
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
(pos) => {
|
(pos) => {
|
||||||
mutate({
|
mutate({
|
||||||
@@ -125,41 +131,48 @@ export default function Home() {
|
|||||||
<div className="px-4 sm:px-6 mt-5 lg:px-4 bg-background">
|
<div className="px-4 sm:px-6 mt-5 lg:px-4 bg-background">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<p className="text-md font-bold text-black mb-5 uppercase">Asosiy</p>
|
<p className="text-md font-bold text-black mb-5 uppercase">Asosiy</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||||
{featuredItems.map((item) => {
|
{featuredItems.map((item) => {
|
||||||
const isLocation = item.id === "lokatsiya";
|
const isLocation = item.id === "lokatsiya";
|
||||||
|
const specification = item.id === "spetsifikatsiya";
|
||||||
|
const tourPlan = item.id === "tour_plan";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={item.id}
|
key={item.id}
|
||||||
onClick={isLocation ? handleLocationClick : undefined}
|
onClick={isLocation ? handleLocationClick : undefined}
|
||||||
className="group relative px-4 cursor-pointer"
|
className="group relative cursor-pointer flex justify-start"
|
||||||
>
|
>
|
||||||
{!isLocation ? (
|
{!isLocation ? (
|
||||||
<Link to={item.link} className="absolute inset-0"></Link>
|
<Link
|
||||||
|
to={item.link}
|
||||||
|
className="absolute inset-0 z-50"
|
||||||
|
></Link>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex items-start gap-6 pb-2 border-b border-border hover:border-primary/30 transition-colors",
|
"flex rounded-xl shadow-sm items-start gap-2 border-b p-2 pb-2 border-border hover:border-primary/30 transition-colors",
|
||||||
isLocation && "shadow-sm p-2 rounded-xl scale-[115%]",
|
isLocation
|
||||||
|
? "w-full"
|
||||||
|
: specification
|
||||||
|
? "w-[95%]"
|
||||||
|
: tourPlan && "w-[85%]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"shrink-0 w-12 h-12 rounded-lg text-primary flex items-center justify-center",
|
"shrink-0 w-12 h-12 rounded-lg flex items-center justify-center bg-primary text-white",
|
||||||
isLocation && "bg-primary text-white",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{isLocation && locationLoad ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
item.icon
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3
|
<h3 className={clsx("text-lg font-bold text-primary")}>
|
||||||
className={clsx(
|
|
||||||
"text-lg font-bold text-foreground",
|
|
||||||
isLocation && "text-primary",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
@@ -210,7 +223,7 @@ export default function Home() {
|
|||||||
<Link to={"/district"} className="border border-border rounded-lg">
|
<Link to={"/district"} className="border border-border rounded-lg">
|
||||||
<div className="flex flex-col items-center text-center p-4">
|
<div className="flex flex-col items-center text-center p-4">
|
||||||
<div className="text-primary/60 group-hover:text-primary transition-colors mb-3">
|
<div className="text-primary/60 group-hover:text-primary transition-colors mb-3">
|
||||||
<Layers className="w-6 h-6" />
|
<MapPinned className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-foreground text-sm">Tuman</h4>
|
<h4 className="font-semibold text-foreground text-sm">Tuman</h4>
|
||||||
</div>
|
</div>
|
||||||
@@ -218,7 +231,7 @@ export default function Home() {
|
|||||||
<Link to={"/object"} className="border border-border rounded-lg">
|
<Link to={"/object"} className="border border-border rounded-lg">
|
||||||
<div className="flex flex-col items-center text-center p-4">
|
<div className="flex flex-col items-center text-center p-4">
|
||||||
<div className="text-primary/60 group-hover:text-primary transition-colors mb-3">
|
<div className="text-primary/60 group-hover:text-primary transition-colors mb-3">
|
||||||
<HomeIcon className="w-6 h-6" />
|
<MapPinHouse className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-foreground text-sm">
|
<h4 className="font-semibold text-foreground text-sm">
|
||||||
Obyekt
|
Obyekt
|
||||||
@@ -242,7 +255,7 @@ export default function Home() {
|
|||||||
<Link to={"/pharmacy"} className="border border-border rounded-lg">
|
<Link to={"/pharmacy"} className="border border-border rounded-lg">
|
||||||
<div className="flex flex-col items-center text-center p-4">
|
<div className="flex flex-col items-center text-center p-4">
|
||||||
<div className="text-primary/60 group-hover:text-primary transition-colors mb-3">
|
<div className="text-primary/60 group-hover:text-primary transition-colors mb-3">
|
||||||
<Syringe className="w-6 h-6" />
|
<Pill className="w-6 h-6" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-foreground text-sm">
|
<h4 className="font-semibold text-foreground text-sm">
|
||||||
Dorixona
|
Dorixona
|
||||||
@@ -257,15 +270,32 @@ export default function Home() {
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col items-center text-center p-4">
|
<div className="flex flex-col items-center text-center p-4">
|
||||||
<div className="text-primary/60 group-hover:text-primary transition-colors mb-3">
|
<div className="text-primary/60 group-hover:text-primary transition-colors mb-3">
|
||||||
<Clipboard className="w-14 h-14" />
|
<Banknote className="w-14 h-14" />
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-semibold text-foreground text-lg">
|
<h4 className="font-semibold text-foreground text-lg">
|
||||||
Hisobotlar
|
To'lovlar
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex p-2 gap-2">
|
||||||
|
<Link
|
||||||
|
to={"/support"}
|
||||||
|
className="relative text-primary/60 rounded-lg flex flex-col gap-1 justify-center items-center border border-border w-[50%] h-20"
|
||||||
|
>
|
||||||
|
<AlertCircle className="w-8 h-8" />
|
||||||
|
<p className="text-black font-medium">Yordam</p>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
to={"/distributed-product"}
|
||||||
|
className="relative rounded-lg flex flex-col gap-1 text-primary/60 justify-center items-center border border-border w-[50%] h-20"
|
||||||
|
>
|
||||||
|
<Pill className="w-8 h-8" />
|
||||||
|
<p className="text-black font-medium">Tarqatilgan dorilar</p>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ const MyLocation: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-col">
|
<CardHeader className="flex flex-col">
|
||||||
@@ -346,7 +346,7 @@ const MyLocation: React.FC = () => {
|
|||||||
colSpan={columns.length}
|
colSpan={columns.length}
|
||||||
className="h-24 text-center"
|
className="h-24 text-center"
|
||||||
>
|
>
|
||||||
No results.
|
Hech qanday lokatsiya jo'natilmagan
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function DataTableObject<ObjectAllData, TValue>({
|
|||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
No results.
|
Obyekt mavjud emas
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ const CreateObject = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/object">
|
||||||
<div className="max-w-3xl mx-auto space-y-6">
|
<div className="max-w-3xl mx-auto space-y-6">
|
||||||
<h1 className="text-3xl font-bold">{"Yangi obyekt qo'shish"}</h1>
|
<h1 className="text-3xl font-bold">{"Yangi obyekt qo'shish"}</h1>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
|
import { Skeleton } from "@/shared/ui/skeleton";
|
||||||
import { DashboardLayout } from "@/widgets/dashboard-layout/ui/DashboardLayout";
|
import { DashboardLayout } from "@/widgets/dashboard-layout/ui/DashboardLayout";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
@@ -61,7 +62,7 @@ const ObjectList = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<AddedButton onClick={() => router("/object/added")} />
|
<AddedButton onClick={() => router("/object/added")} />
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h1 className="text-3xl font-bold">Obyektlar ro‘yxati</h1>
|
<h1 className="text-3xl font-bold">Obyektlar ro‘yxati</h1>
|
||||||
@@ -80,16 +81,22 @@ const ObjectList = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !isError && objects && objects.length > 0 && (
|
{isLoading ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-12 w-full rounded-md" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<p className="text-red-500">
|
||||||
|
Tumanlar yuklanmadi. Qayta urinib ko‘ring.
|
||||||
|
</p>
|
||||||
|
) : objects ? (
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<DataTableObject columns={columns} data={objects} />
|
<DataTableObject columns={columns} data={objects} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
) : (
|
||||||
|
<p className="text-gray-500">Tumanlar mavjud emas</p>
|
||||||
{!isLoading && !isError && objects && objects.length === 0 && (
|
|
||||||
<div className="flex justify-center items-center h-64">
|
|
||||||
<span className="text-gray-500">Hech qanday obyekt topilmadi.</span>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,7 @@ const ObjectMapPage = () => {
|
|||||||
const L = (await import("leaflet")).default;
|
const L = (await import("leaflet")).default;
|
||||||
await import("leaflet/dist/leaflet.css");
|
await import("leaflet/dist/leaflet.css");
|
||||||
await import("leaflet-routing-machine");
|
await import("leaflet-routing-machine");
|
||||||
await import(
|
await import("leaflet-routing-machine/dist/leaflet-routing-machine.css");
|
||||||
"leaflet-routing-machine/dist/leaflet-routing-machine.css"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Xaritani yaratish
|
// Xaritani yaratish
|
||||||
if (!mapInstance.current) {
|
if (!mapInstance.current) {
|
||||||
@@ -298,13 +296,13 @@ const ObjectMapPage = () => {
|
|||||||
|
|
||||||
if (!lat || !lon)
|
if (!lat || !lon)
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/object">
|
||||||
<p className="text-red-600">Koordinatalar mavjud emas</p>
|
<p className="text-red-600">Koordinatalar mavjud emas</p>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/object">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="rounded-lg bg-blue-50 p-4">
|
<div className="rounded-lg bg-blue-50 p-4">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
export interface CreatePharmacyReq {
|
export interface CreatePharmacyReq {
|
||||||
name: string;
|
name: string;
|
||||||
inn: string;
|
inn?: string;
|
||||||
owner_phone: string;
|
owner_phone: string;
|
||||||
responsible_phone: string;
|
responsible_phone?: string;
|
||||||
district_id: number;
|
district_id: number;
|
||||||
place_id: number;
|
place_id: number;
|
||||||
longitude: number;
|
longitude: number;
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ export const objectForm = z.object({
|
|||||||
}),
|
}),
|
||||||
latitude: z.number({ error: "Majburiy maydon" }),
|
latitude: z.number({ error: "Majburiy maydon" }),
|
||||||
longitude: z.number({ error: "Majburiy maydon" }),
|
longitude: z.number({ error: "Majburiy maydon" }),
|
||||||
inn: z.string().min(1, { message: "Majburiy maydon" }),
|
inn: z.string().optional(),
|
||||||
phoneDirector: z.string().min(17, { message: "Majburiy maydon" }),
|
phoneDirector: z.string().min(17, { message: "Majburiy maydon" }),
|
||||||
phonePharmacy: z.string().min(17, { message: "Majburiy maydon" }),
|
phonePharmacy: z.string().optional(),
|
||||||
nearbyCoords: z
|
nearbyCoords: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -218,18 +218,20 @@ const CreatePharmacy = () => {
|
|||||||
latitude: values.latitude,
|
latitude: values.latitude,
|
||||||
longitude: values.longitude,
|
longitude: values.longitude,
|
||||||
},
|
},
|
||||||
inn: values.inn,
|
...(values.inn && { inn: values.inn }),
|
||||||
latitude: values.latitude,
|
latitude: values.latitude,
|
||||||
longitude: values.longitude,
|
longitude: values.longitude,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
owner_phone: onlyNumber(values.phoneDirector),
|
owner_phone: onlyNumber(values.phoneDirector),
|
||||||
place_id: Number(values.streets),
|
place_id: Number(values.streets),
|
||||||
responsible_phone: onlyNumber(values.phonePharmacy),
|
...(values.phonePharmacy && {
|
||||||
|
responsible_phone: onlyNumber(values.phonePharmacy),
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/pharmacy">
|
||||||
<div className="max-w-3xl mx-auto space-y-6">
|
<div className="max-w-3xl mx-auto space-y-6">
|
||||||
<h1 className="text-3xl font-bold">{"Qo'shish"}</h1>
|
<h1 className="text-3xl font-bold">{"Qo'shish"}</h1>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -357,7 +359,7 @@ const CreatePharmacy = () => {
|
|||||||
name="streets"
|
name="streets"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>{"Ko'cha"}</FormLabel>
|
<FormLabel>{"Obyekt"}</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Select
|
<Select
|
||||||
key={field.value}
|
key={field.value}
|
||||||
@@ -368,7 +370,7 @@ const CreatePharmacy = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-full h-12">
|
<SelectTrigger className="w-full h-12">
|
||||||
<SelectValue placeholder="Ko'chalar" />
|
<SelectValue placeholder="Obyektlar" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{streets?.map((s) => (
|
{streets?.map((s) => (
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const PharmacyList = () => {
|
|||||||
const [deleteDialog, setDeleteDialog] = useState(false);
|
const [deleteDialog, setDeleteDialog] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<AddedButton onClick={() => router("/pharmacy/added")} />
|
<AddedButton onClick={() => router("/pharmacy/added")} />
|
||||||
</div>
|
</div>
|
||||||
@@ -46,8 +46,8 @@ const PharmacyList = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && !isError && data?.length === 0 && (
|
{!isLoading && !isError && data?.length === 0 && (
|
||||||
<div className="flex justify-center items-center py-20">
|
<div className="h-[80vh] flex justify-center items-center w-[90%] fixed">
|
||||||
<p className="text-gray-500">Hech qanday dorixona topilmadi</p>
|
<p>Dorixona mavjud emas</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -296,13 +296,13 @@ const ObjectMapPage = () => {
|
|||||||
|
|
||||||
if (!lat || !lon)
|
if (!lat || !lon)
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/pharmacy">
|
||||||
<p className="text-red-600">Koordinatalar mavjud emas</p>
|
<p className="text-red-600">Koordinatalar mavjud emas</p>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/pharmacy">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{loading && (
|
{loading && (
|
||||||
<div className="rounded-lg bg-blue-50 p-4">
|
<div className="rounded-lg bg-blue-50 p-4">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { order_api } from "@/features/specification/lib/api";
|
import { order_api } from "@/features/specification/lib/api";
|
||||||
import type { OrderListData } from "@/features/specification/lib/data";
|
import type { OrderListData } from "@/features/specification/lib/data";
|
||||||
import { LanguageRoutes } from "@/shared/config/i18n/types";
|
|
||||||
import { formatPrice } from "@/shared/lib/formatPrice";
|
import { formatPrice } from "@/shared/lib/formatPrice";
|
||||||
import { Alert, AlertDescription } from "@/shared/ui/alert";
|
import { Alert, AlertDescription } from "@/shared/ui/alert";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
@@ -36,33 +35,18 @@ const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
|
|||||||
const [tempAmount, setTempAmount] = useState<string>("");
|
const [tempAmount, setTempAmount] = useState<string>("");
|
||||||
const [displayPrice, setDisplayPrice] = useState("");
|
const [displayPrice, setDisplayPrice] = useState("");
|
||||||
|
|
||||||
const formatCurrency = (amount: number): string =>
|
const getMonthName = (pharmacies: OrderListData[]): number => {
|
||||||
new Intl.NumberFormat("uz-UZ").format(amount) + " so'm";
|
return pharmacies.reduce(
|
||||||
|
(total, item) => total + (Number(item.overdue_price) || 0),
|
||||||
const getMonthName = (monthKey: string): string => {
|
0,
|
||||||
const months = [
|
);
|
||||||
"Yanvar",
|
|
||||||
"Fevral",
|
|
||||||
"Mart",
|
|
||||||
"Aprel",
|
|
||||||
"May",
|
|
||||||
"Iyun",
|
|
||||||
"Iyul",
|
|
||||||
"Avgust",
|
|
||||||
"Sentyabr",
|
|
||||||
"Oktyabr",
|
|
||||||
"Noyabr",
|
|
||||||
"Dekabr",
|
|
||||||
];
|
|
||||||
const [year, month] = monthKey.split("-");
|
|
||||||
return `${months[parseInt(month) - 1]} ${year}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { mutate, isPending } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: ({ body, id }: { id: number; body: { paid_price: number } }) =>
|
mutationFn: ({ body, id }: { id: number; body: { paid_price: number } }) =>
|
||||||
order_api.update({ body, id }),
|
order_api.update({ body, id }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Lokatsiya jo'natildi");
|
toast.success("To'landi");
|
||||||
queryClient.refetchQueries({ queryKey: ["order_list"] });
|
queryClient.refetchQueries({ queryKey: ["order_list"] });
|
||||||
setEditingId(null);
|
setEditingId(null);
|
||||||
setTempAmount("");
|
setTempAmount("");
|
||||||
@@ -103,8 +87,11 @@ const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
|
|||||||
setTempAmount(currentAmount.toString());
|
setTempAmount(currentAmount.toString());
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSave = (pharmacyId: number) => {
|
const handleSave = (pharmacyId: number, paid_price: number) => {
|
||||||
const amount = parseInt(tempAmount) || 0;
|
const current = Number(tempAmount) || 0;
|
||||||
|
const old = Number(paid_price) || 0;
|
||||||
|
|
||||||
|
const amount = current + old;
|
||||||
|
|
||||||
mutate({
|
mutate({
|
||||||
body: {
|
body: {
|
||||||
@@ -123,24 +110,15 @@ const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
|
|||||||
pharmacy.monthlyData[selectedMonth]?.locked === true ||
|
pharmacy.monthlyData[selectedMonth]?.locked === true ||
|
||||||
selectedMonth !== currentMonthKey;
|
selectedMonth !== currentMonthKey;
|
||||||
|
|
||||||
const getTotalForMonth = (): number =>
|
|
||||||
pharmacies.reduce(
|
|
||||||
(total, pharmacy) =>
|
|
||||||
total + (pharmacy.monthlyData[selectedMonth]?.amount || 0),
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Card className="mb-6 shadow-lg border-0 bg-gradient-to-r from-green-600 to-emerald-600 text-white">
|
<Card className="mb-6 shadow-lg border-0 bg-gradient-to-r from-green-600 to-emerald-600 text-white">
|
||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-green-100 mb-1">
|
<p className="text-green-100 mb-1">Jami qolgan summalar</p>
|
||||||
Jami summa ({getMonthName(selectedMonth)})
|
|
||||||
</p>
|
|
||||||
<p className="text-3xl font-bold">
|
<p className="text-3xl font-bold">
|
||||||
{formatCurrency(getTotalForMonth())}
|
{formatPrice(getMonthName(pharmacies))}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<DollarSign className="h-16 w-16 opacity-20" />
|
<DollarSign className="h-16 w-16 opacity-20" />
|
||||||
@@ -149,8 +127,6 @@ const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
|
|||||||
</Card>
|
</Card>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
{pharmacies.map((pharmacy) => {
|
{pharmacies.map((pharmacy) => {
|
||||||
const monthData = pharmacy.monthlyData[selectedMonth];
|
|
||||||
const amount = monthData?.amount || 0;
|
|
||||||
const locked = isLocked(pharmacy);
|
const locked = isLocked(pharmacy);
|
||||||
const isEditing = editingId === pharmacy.id;
|
const isEditing = editingId === pharmacy.id;
|
||||||
|
|
||||||
@@ -173,6 +149,13 @@ const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
|
<Building2 className="h-4 w-4 mr-2 text-blue-600" />
|
||||||
|
Umumiy summa: {formatPrice(pharmacy.total_price)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center text-sm text-gray-600">
|
<div className="flex items-center text-sm text-gray-600">
|
||||||
<Banknote className="h-4 w-4 mr-2 text-blue-600" />
|
<Banknote className="h-4 w-4 mr-2 text-blue-600" />
|
||||||
@@ -201,10 +184,16 @@ const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
|
|||||||
value={displayPrice}
|
value={displayPrice}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const raw = e.target.value.replace(/\D/g, "");
|
const raw = e.target.value.replace(/\D/g, "");
|
||||||
const num = Number(raw);
|
const rawNumber = Number(raw);
|
||||||
if (!isNaN(num)) {
|
|
||||||
setTempAmount(String(num));
|
const limited =
|
||||||
setDisplayPrice(raw ? formatPrice(num) : "");
|
rawNumber <= Number(pharmacy.overdue_price)
|
||||||
|
? rawNumber
|
||||||
|
: Number(pharmacy.overdue_price);
|
||||||
|
|
||||||
|
if (!isNaN(limited)) {
|
||||||
|
setTempAmount(String(limited));
|
||||||
|
setDisplayPrice(raw ? formatPrice(limited) : "");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="h-12 text-md"
|
className="h-12 text-md"
|
||||||
@@ -212,7 +201,12 @@ const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
|
|||||||
|
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handleSave(pharmacy.id)}
|
onClick={() =>
|
||||||
|
handleSave(
|
||||||
|
pharmacy.id,
|
||||||
|
Number(pharmacy.paid_price),
|
||||||
|
)
|
||||||
|
}
|
||||||
className="flex-1 bg-green-600 hover:bg-green-700"
|
className="flex-1 bg-green-600 hover:bg-green-700"
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4 mr-2" />
|
<Check className="h-4 w-4 mr-2" />
|
||||||
@@ -236,7 +230,7 @@ const PlanPrice = ({ selectedMonth, pharmacies }: PlanPriceProps) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 items-center rounded-lg">
|
<div className="flex flex-col gap-4 items-center rounded-lg">
|
||||||
<span className="text-2xl font-bold text-gray-900">
|
<span className="text-2xl font-bold text-gray-900">
|
||||||
{formatPrice(amount, "uz" as LanguageRoutes, true)}
|
To'langan: {formatPrice(pharmacy.paid_price)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{!locked && (
|
{!locked && (
|
||||||
|
|||||||
@@ -89,13 +89,13 @@ const PlanTour = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
<h1 className="text-4xl font-bold text-gray-900 mb-2">
|
||||||
Oylik hisobotlar
|
Oylik to'lovlar
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
Dorixonalar uchun oylik summalarni boshqaring
|
Dorixonalar uchun oylik to'lovlarni boshqaring
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
@@ -110,41 +110,48 @@ const PlanTour = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Card className="mb-4 border-0 p-0 mt-5">
|
{!isLoading && !isError && data && data.length === 0 ? (
|
||||||
<CardHeader className="bg-blue-500 p-3 text-white">
|
<div className="h-[80vh] flex justify-center items-center w-[90%] fixed">
|
||||||
<CardTitle className="flex items-center gap-2 justify-center">
|
<p>Hech qanday to'lovlar yo'q</p>
|
||||||
<Calendar className="h-5 w-5" />
|
</div>
|
||||||
Oy tanlash
|
) : (
|
||||||
</CardTitle>
|
<>
|
||||||
</CardHeader>
|
<Card className="mb-4 border-0 p-0 mt-5">
|
||||||
|
<CardHeader className="bg-blue-500 p-3 text-white">
|
||||||
|
<CardTitle className="flex items-center gap-2 justify-center">
|
||||||
|
<Calendar className="h-5 w-5" />
|
||||||
|
Oy tanlash
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="pb-6">
|
<CardContent className="pb-6">
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{availableMonths().map((month) => (
|
{availableMonths().map((month) => (
|
||||||
<Button
|
<Button
|
||||||
key={month}
|
key={month}
|
||||||
onClick={() => setSelectedMonth(month)}
|
onClick={() => setSelectedMonth(month)}
|
||||||
variant={selectedMonth === month ? "default" : "outline"}
|
variant={selectedMonth === month ? "default" : "outline"}
|
||||||
className={
|
className={
|
||||||
selectedMonth === month
|
selectedMonth === month
|
||||||
? "bg-blue-600 hover:bg-blue-700 text-white"
|
? "bg-blue-600 hover:bg-blue-700 text-white"
|
||||||
: "hover:bg-blue-50"
|
: "hover:bg-blue-50"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{getMonthName(month)}
|
{getMonthName(month)}
|
||||||
{month === currentMonthKey && (
|
{month === currentMonthKey && (
|
||||||
<span className="ml-2 text-xs bg-green-500 text-white px-2 py-0.5 rounded-full">
|
<span className="ml-2 text-xs bg-green-500 text-white px-2 py-0.5 rounded-full">
|
||||||
Joriy
|
Joriy
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 🔥 Oylik narxlar */}
|
<PlanPrice selectedMonth={selectedMonth} pharmacies={pharmacies} />
|
||||||
<PlanPrice selectedMonth={selectedMonth} pharmacies={pharmacies} />
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export const plans_api = {
|
|||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
async planIsDone(id: number) {
|
async planIsDone({ id, body }: { id: number; body: { comment: string } }) {
|
||||||
const res = await httpClient.post(`${PLANS}${id}/complite/`);
|
const res = await httpClient.post(`${PLANS}${id}/complite/`, body);
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
export interface CreatePlansReq {
|
export interface CreatePlansReq {
|
||||||
title: string;
|
title?: string;
|
||||||
description: string;
|
description: string;
|
||||||
date: string; // '2025-11-26' shu kabi jonatish kera apiga
|
date: string; // "2025-12-05";
|
||||||
is_done: boolean;
|
doctor_id: number | null;
|
||||||
|
pharmacy_id: number | null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
extra_location: {
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetMyPlansRes {
|
export interface GetMyPlansRes {
|
||||||
@@ -14,7 +21,12 @@ export interface GetMyPlansRes {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
date: string;
|
date: string;
|
||||||
is_done: boolean;
|
comment: string | null;
|
||||||
|
doctor: null;
|
||||||
|
pharmacy: null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
extra_location: null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
@@ -24,6 +36,11 @@ export interface Task {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
date: string;
|
date: string;
|
||||||
is_done: boolean;
|
comment: string | null;
|
||||||
|
doctor: null;
|
||||||
|
pharmacy: null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
extra_location: null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
export const plansForm = z.object({
|
export const plansForm = z.object({
|
||||||
title: z.string().min(1, { error: "Majburiy maydon" }),
|
title: z.string().optional(),
|
||||||
description: z.string().min(1, { error: "Majburiy maydon" }),
|
description: z.string().min(1, { error: "Majburiy maydon" }),
|
||||||
date: z.string().optional(),
|
date: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/shared/ui/dialog";
|
} from "@/shared/ui/dialog";
|
||||||
|
import { Textarea } from "@/shared/ui/textarea";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { plans_api } from "../lib/api";
|
import { plans_api } from "../lib/api";
|
||||||
|
|
||||||
@@ -17,7 +19,12 @@ interface Task {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
date: string;
|
date: string;
|
||||||
is_done: boolean;
|
comment: string | null;
|
||||||
|
doctor: null;
|
||||||
|
pharmacy: null;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
extra_location: null;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,11 +40,17 @@ export function PlanDetailsDialog({
|
|||||||
task,
|
task,
|
||||||
}: PlanDetailsDialogProps) {
|
}: PlanDetailsDialogProps) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const [commentOpen, setCommnetOpen] = useState<boolean>(false);
|
||||||
|
const [commentText, setCommnetText] = useState<string>("");
|
||||||
|
|
||||||
const { mutate, isPending } = useMutation({
|
const { mutate, isPending } = useMutation({
|
||||||
mutationFn: (id: number) => plans_api.planIsDone(id),
|
mutationFn: ({ id, body }: { id: number; body: { comment: string } }) =>
|
||||||
|
plans_api.planIsDone({ body, id }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Rejangiz bajarildi bo'lib o'zgardi");
|
toast.success("Rejangiz bajarildi bo'lib o'zgardi");
|
||||||
onOpenChange(false);
|
onOpenChange(false);
|
||||||
|
setCommnetOpen(false);
|
||||||
|
setCommnetText("");
|
||||||
queryClient.refetchQueries({ queryKey: ["my_plans"] });
|
queryClient.refetchQueries({ queryKey: ["my_plans"] });
|
||||||
},
|
},
|
||||||
onError: (error: AxiosError) => {
|
onError: (error: AxiosError) => {
|
||||||
@@ -75,74 +88,105 @@ export function PlanDetailsDialog({
|
|||||||
<DialogContent className="w-[95%] max-w-md">
|
<DialogContent className="w-[95%] max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="text-2xl font-bold">
|
<DialogTitle className="text-2xl font-bold">
|
||||||
{"Rejani ko'rish"}
|
{commentOpen ? "Reja qanday bajarildi ma'lumot" : "Rejani ko'rish"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
<div className="space-y-4 py-4">
|
||||||
{/* Status */}
|
{!commentOpen && (
|
||||||
<div className="flex items-center gap-2">
|
<>
|
||||||
|
{/* Status */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-muted-foreground">Xolati</p>
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
"font-semibold",
|
||||||
|
task.comment ? "text-green-500" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{task.comment ? "Tugallangan ✓" : "Bajarilmagan"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
{/* <div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Sarlavha</p>
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
"text-lg font-semibold",
|
||||||
|
task.comment ? "text-green-500" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{task.title}
|
||||||
|
</p>
|
||||||
|
</div> */}
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Tavsifi</p>
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
"text-lg font-semibold",
|
||||||
|
task.comment ? "text-green-500" : "text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{task.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-sm text-muted-foreground">Sana</p>
|
||||||
|
<p className="text-base font-medium text-foreground">
|
||||||
|
{format(task.date, "dd.MM.yyyy")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{commentOpen && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm text-muted-foreground">Xolati</p>
|
<Textarea
|
||||||
<p
|
className="min-h-32 max-h-52"
|
||||||
className={clsx(
|
placeholder="Reja qanday bajarildi yozing"
|
||||||
"font-semibold",
|
value={commentText}
|
||||||
task.is_done ? "text-green-500" : "text-foreground",
|
onChange={(e) => setCommnetText(e.target.value)}
|
||||||
)}
|
/>
|
||||||
>
|
|
||||||
{task.is_done ? "Tugallangan ✓" : "Bajarilmagan"}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Title */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm text-muted-foreground">Sarlavha</p>
|
|
||||||
<p
|
|
||||||
className={clsx(
|
|
||||||
"text-lg font-semibold break-words",
|
|
||||||
task.is_done ? "text-green-500" : "text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{task.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm text-muted-foreground">Tavsifi</p>
|
|
||||||
<p
|
|
||||||
className={clsx(
|
|
||||||
"text-base break-words leading-relaxed",
|
|
||||||
task.is_done ? "text-green-500" : "text-foreground",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{task.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Date */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<p className="text-sm text-muted-foreground">Sana</p>
|
|
||||||
<p className="text-base font-medium text-foreground">
|
|
||||||
{format(task.date, "dd.MM.yyyy")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<div className="flex justify-end gap-2 pt-4">
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
<Button
|
{commentOpen ? (
|
||||||
onClick={() => {
|
<Button
|
||||||
mutate(task.id);
|
disabled={
|
||||||
}}
|
(task.comment && task.comment.length > 0) || isPending
|
||||||
disabled={task.is_done || isPending}
|
}
|
||||||
className="w-fit p-5 rounded-lg"
|
onClick={() =>
|
||||||
>
|
mutate({ body: { comment: commentText }, id: task.id })
|
||||||
Bajarildi
|
}
|
||||||
</Button>
|
className="w-fit p-5 rounded-lg"
|
||||||
|
>
|
||||||
|
Tasdiqlash
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
disabled={
|
||||||
|
(task.comment && task.comment.length > 0) || isPending
|
||||||
|
}
|
||||||
|
onClick={() => setCommnetOpen(true)}
|
||||||
|
className="w-fit p-5 rounded-lg"
|
||||||
|
>
|
||||||
|
Bajarildi
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
disabled={isPending}
|
disabled={isPending}
|
||||||
onClick={() => onOpenChange(false)}
|
onClick={() => {
|
||||||
|
onOpenChange(false);
|
||||||
|
setCommnetOpen(false);
|
||||||
|
setCommnetText("");
|
||||||
|
}}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="p-5 rounded-lg"
|
className="p-5 rounded-lg"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import type { CreatePlansReq } from "@/features/plan/lib/data";
|
import { doctor_api } from "@/features/doctor/lib/api";
|
||||||
|
import type { DoctorListData } from "@/features/doctor/lib/data";
|
||||||
|
import { pharmacy_api } from "@/features/phamarcy/lib/api";
|
||||||
|
import type { PharmacyListData } from "@/features/phamarcy/lib/data";
|
||||||
|
import type { CreatePlansReq, Task } from "@/features/plan/lib/data";
|
||||||
import formatDate from "@/shared/lib/formatDate";
|
import formatDate from "@/shared/lib/formatDate";
|
||||||
import AddedButton from "@/shared/ui/added-button";
|
import AddedButton from "@/shared/ui/added-button";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
@@ -22,10 +26,9 @@ import {
|
|||||||
} from "@/shared/ui/form";
|
} from "@/shared/ui/form";
|
||||||
import { Input } from "@/shared/ui/input";
|
import { Input } from "@/shared/ui/input";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { format } from "date-fns";
|
import { CalendarIcon, Loader2, MapPin } from "lucide-react";
|
||||||
import { CalendarIcon, Loader2 } from "lucide-react";
|
|
||||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
@@ -33,20 +36,11 @@ import z from "zod";
|
|||||||
import { plans_api } from "../lib/api";
|
import { plans_api } from "../lib/api";
|
||||||
|
|
||||||
const plansForm = z.object({
|
const plansForm = z.object({
|
||||||
title: z.string().min(1, "Sarlavha kiritish majburiy"),
|
title: z.string().optional(),
|
||||||
description: z.string().min(1, "Tavsif kiritish majburiy"),
|
description: z.string().min(1, "Tavsif kiritish majburiy"),
|
||||||
date: z.string().optional(),
|
date: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
interface Task {
|
|
||||||
id: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
date: string;
|
|
||||||
is_done: boolean;
|
|
||||||
created_at: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isDialogOpen: boolean;
|
isDialogOpen: boolean;
|
||||||
setIsDialogOpen: Dispatch<SetStateAction<boolean>>;
|
setIsDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
@@ -62,21 +56,41 @@ export const AddPlans = ({
|
|||||||
isDialogOpen,
|
isDialogOpen,
|
||||||
setIsDialogOpen,
|
setIsDialogOpen,
|
||||||
newTask,
|
newTask,
|
||||||
setNewTask,
|
|
||||||
setTaskEdit,
|
setTaskEdit,
|
||||||
|
setNewTask,
|
||||||
taskEdit,
|
taskEdit,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [isDateDialogOpen, setIsDateDialogOpen] = useState(false);
|
const [isDateDialogOpen, setIsDateDialogOpen] = useState(false);
|
||||||
|
const [showMainModal, setShowMainModal] = useState<boolean>(false);
|
||||||
|
const [showSelectModal, setShowSelectModal] = useState<boolean>(false);
|
||||||
|
const [selectType, setSelectType] = useState<"doctor" | "pharm" | null>(null);
|
||||||
|
const [doctorId, setDoctorId] = useState<DoctorListData | null>(null);
|
||||||
|
const [pharmId, setPharmId] = useState<PharmacyListData | null>(null);
|
||||||
|
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const form = useForm<z.infer<typeof plansForm>>({
|
const form = useForm<z.infer<typeof plansForm>>({
|
||||||
resolver: zodResolver(plansForm),
|
resolver: zodResolver(plansForm),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
// title: "",
|
||||||
description: "",
|
description: "",
|
||||||
date: formatDate.format(new Date(), "YYYY-MM-DD"),
|
date: formatDate.format(new Date(), "YYYY-MM-DD"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: doctor, isLoading: isDoctorsLoading } = useQuery({
|
||||||
|
queryKey: ["doctor_list"],
|
||||||
|
queryFn: () => doctor_api.list(),
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: pharm, isLoading: isPharmLoading } = useQuery({
|
||||||
|
queryKey: ["pharmacy_list"],
|
||||||
|
queryFn: () => pharmacy_api.list(),
|
||||||
|
select: (data) => data.data.data,
|
||||||
|
});
|
||||||
|
|
||||||
const { mutate: added, isPending } = useMutation({
|
const { mutate: added, isPending } = useMutation({
|
||||||
mutationFn: (body: CreatePlansReq) => plans_api.createPlans(body),
|
mutationFn: (body: CreatePlansReq) => plans_api.createPlans(body),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -152,133 +166,299 @@ export const AddPlans = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (taskEdit) {
|
if (taskEdit) {
|
||||||
form.setValue("title", taskEdit.title);
|
// form.setValue("title", taskEdit.title);
|
||||||
form.setValue("description", taskEdit.description);
|
form.setValue("description", taskEdit.description);
|
||||||
form.setValue("date", taskEdit.date);
|
form.setValue("date", taskEdit.date);
|
||||||
}
|
}
|
||||||
}, [taskEdit]);
|
}, [taskEdit]);
|
||||||
|
|
||||||
|
const getOptions = () => {
|
||||||
|
if (selectType === "doctor") return doctor || [];
|
||||||
|
if (selectType === "pharm") return pharm;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isLoading = () => {
|
||||||
|
if (selectType === "doctor") return isDoctorsLoading;
|
||||||
|
if (selectType === "pharm") return isPharmLoading;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeLabel = (type: "district" | "object" | "doctor" | "pharm") => {
|
||||||
|
if (type === "district") return "Tuman";
|
||||||
|
if (type === "object") return "Obyekt";
|
||||||
|
if (type === "doctor") return "Shifokor";
|
||||||
|
if (type === "pharm") return "Dorixona";
|
||||||
|
};
|
||||||
|
|
||||||
function onSubmit(values: z.infer<typeof plansForm>) {
|
function onSubmit(values: z.infer<typeof plansForm>) {
|
||||||
if (taskEdit) {
|
if (taskEdit) {
|
||||||
edit({
|
edit({
|
||||||
body: {
|
body: {
|
||||||
title: values.title,
|
...(values.title && {
|
||||||
|
title: values.title,
|
||||||
|
}),
|
||||||
|
date: values.date ? formatDate.format(values.date, "YYYY-MM-DD") : "",
|
||||||
description: values.description,
|
description: values.description,
|
||||||
date: formatDate.format(values.date!, "YYYY-MM-DD"),
|
doctor_id: doctorId ? doctorId.id : null,
|
||||||
is_done: taskEdit.is_done,
|
pharmacy_id: pharmId ? pharmId.id : null,
|
||||||
|
extra_location: {
|
||||||
|
latitude: doctorId
|
||||||
|
? doctorId.latitude
|
||||||
|
: pharmId
|
||||||
|
? pharmId.latitude
|
||||||
|
: 43.123,
|
||||||
|
longitude: doctorId
|
||||||
|
? doctorId.longitude
|
||||||
|
: pharmId
|
||||||
|
? pharmId.longitude
|
||||||
|
: 63.123,
|
||||||
|
},
|
||||||
|
latitude: doctorId
|
||||||
|
? doctorId.latitude
|
||||||
|
: pharmId
|
||||||
|
? pharmId.latitude
|
||||||
|
: 43.123,
|
||||||
|
longitude: doctorId
|
||||||
|
? doctorId.longitude
|
||||||
|
: pharmId
|
||||||
|
? pharmId.longitude
|
||||||
|
: 63.123,
|
||||||
},
|
},
|
||||||
id: taskEdit.id,
|
id: taskEdit.id,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
added({
|
added({
|
||||||
title: values.title,
|
...(values.title && {
|
||||||
|
title: values.title,
|
||||||
|
}),
|
||||||
|
date: values.date ? formatDate.format(values.date, "YYYY-MM-DD") : "",
|
||||||
description: values.description,
|
description: values.description,
|
||||||
date: formatDate.format(values.date!, "YYYY-MM-DD"),
|
doctor_id: doctorId ? doctorId.id : null,
|
||||||
is_done: false,
|
pharmacy_id: pharmId ? pharmId.id : null,
|
||||||
|
extra_location: {
|
||||||
|
latitude: doctorId
|
||||||
|
? doctorId.latitude
|
||||||
|
: pharmId
|
||||||
|
? pharmId.latitude
|
||||||
|
: 43.123,
|
||||||
|
longitude: doctorId
|
||||||
|
? doctorId.longitude
|
||||||
|
: pharmId
|
||||||
|
? pharmId.longitude
|
||||||
|
: 63.123,
|
||||||
|
},
|
||||||
|
latitude: doctorId
|
||||||
|
? doctorId.latitude
|
||||||
|
: pharmId
|
||||||
|
? pharmId.latitude
|
||||||
|
: 43.123,
|
||||||
|
longitude: doctorId
|
||||||
|
? doctorId.longitude
|
||||||
|
: pharmId
|
||||||
|
? pharmId.longitude
|
||||||
|
: 63.123,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
<>
|
||||||
<DialogTrigger asChild>
|
<Dialog open={showMainModal} onOpenChange={setShowMainModal}>
|
||||||
<AddedButton onClick={() => setTaskEdit(null)} />
|
<DialogTrigger asChild>
|
||||||
</DialogTrigger>
|
<AddedButton
|
||||||
|
onClick={() => {
|
||||||
|
setShowMainModal(true);
|
||||||
|
setTaskEdit(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle className="text-2xl">
|
||||||
{taskEdit ? "Vazifani tahrirlash" : "Yangi vazifa qo‘shish"}
|
{"Rejani biriktirish"}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
<div className="space-y-3 mt-4">
|
||||||
<Form {...form}>
|
{(["doctor", "pharm"] as const).map((type) => (
|
||||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
<Button
|
||||||
<FormField
|
key={type}
|
||||||
control={form.control}
|
onClick={() => {
|
||||||
name="title"
|
setSelectType(type);
|
||||||
render={({ field }) => (
|
setShowSelectModal(true);
|
||||||
<FormItem>
|
setShowMainModal(false);
|
||||||
<FormLabel>Sarlavha</FormLabel>
|
}}
|
||||||
<FormControl>
|
className="w-full h-12 text-lg"
|
||||||
<Input placeholder="Sarlavha" {...field} />
|
variant="outline"
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="description"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Tavsif</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Tavsif" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Sana</FormLabel>
|
|
||||||
<Dialog
|
|
||||||
open={isDateDialogOpen}
|
|
||||||
onOpenChange={setIsDateDialogOpen}
|
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
{typeLabel(type)}
|
||||||
<Button variant="outline" className="w-full justify-start">
|
</Button>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
))}
|
||||||
{newTask.date
|
</div>
|
||||||
? format(newTask.date, "dd-MM-yyyy")
|
</DialogContent>
|
||||||
: "Sanani tanlang"}
|
</Dialog>
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
|
|
||||||
<DialogContent className="w-auto p-4">
|
<Dialog open={showSelectModal} onOpenChange={setShowSelectModal}>
|
||||||
<Calendar
|
<DialogContent className="sm:max-w-md h-[60%] flex flex-col justify-start overflow-y-auto">
|
||||||
mode="single"
|
<DialogHeader>
|
||||||
selected={newTask.date}
|
<DialogTitle className="text-2xl">
|
||||||
onSelect={(date) => {
|
Mavjud {selectType && typeLabel(selectType)}lar
|
||||||
if (date) {
|
</DialogTitle>
|
||||||
setNewTask({ ...newTask, date });
|
</DialogHeader>
|
||||||
form.setValue("date", date.toISOString());
|
<div className="space-y-2 flex flex-col flex-1">
|
||||||
|
{isLoading() ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-12 gap-3">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
|
||||||
|
<p className="text-muted-foreground">Yuklanmoqda...</p>
|
||||||
|
</div>
|
||||||
|
) : getOptions()?.length === 0 ? (
|
||||||
|
<div className="flex items-center justify-center py-12">
|
||||||
|
<p className="text-muted-foreground">Ma'lumot topilmadi</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
getOptions()?.map((item: DoctorListData | PharmacyListData) => {
|
||||||
|
const id = item.id;
|
||||||
|
const label =
|
||||||
|
"name" in item
|
||||||
|
? item.name
|
||||||
|
: `${item.first_name} ${item.last_name}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
key={id}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full justify-start h-auto py-3 flex items-center gap-2"
|
||||||
|
onClick={() => {
|
||||||
|
if (selectType === "doctor") {
|
||||||
|
setDoctorId(item as DoctorListData);
|
||||||
|
setPharmId(null);
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
setShowSelectModal(false);
|
||||||
|
setShowMainModal(false);
|
||||||
|
} else if (selectType === "pharm") {
|
||||||
|
setDoctorId(null);
|
||||||
|
setPharmId(item as PharmacyListData);
|
||||||
|
setIsDialogOpen(true);
|
||||||
|
setShowSelectModal(false);
|
||||||
|
setShowMainModal(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
initialFocus
|
>
|
||||||
/>
|
<MapPin className="h-4 w-4" />
|
||||||
<div className="mt-4 flex justify-end">
|
{label}
|
||||||
<Button onClick={() => setIsDateDialogOpen(false)}>
|
</Button>
|
||||||
Tanlash
|
);
|
||||||
</Button>
|
})
|
||||||
</div>
|
)}
|
||||||
</DialogContent>
|
</div>
|
||||||
</Dialog>
|
<Button
|
||||||
</FormItem>
|
onClick={() => setShowSelectModal(false)}
|
||||||
|
variant="outline"
|
||||||
|
className="w-full mt-2"
|
||||||
|
>
|
||||||
|
Bekor qilish
|
||||||
|
</Button>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
<Button
|
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||||
type="button"
|
<DialogHeader>
|
||||||
variant="outline"
|
<DialogTitle>
|
||||||
disabled={isPending || editPending}
|
{taskEdit ? "Vazifani tahrirlash" : "Yangi vazifa qo‘shish"}
|
||||||
onClick={() => setIsDialogOpen(false)}
|
</DialogTitle>
|
||||||
>
|
</DialogHeader>
|
||||||
Bekor qilish
|
|
||||||
</Button>
|
<Form {...form}>
|
||||||
<Button type="submit" disabled={isPending || editPending}>
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||||
{isPending || editPending ? (
|
<FormField
|
||||||
<Loader2 className="animate-spin" />
|
control={form.control}
|
||||||
) : taskEdit ? (
|
name="title"
|
||||||
"Saqlash"
|
render={({ field }) => (
|
||||||
) : (
|
<FormItem>
|
||||||
"Qo‘shish"
|
<FormLabel>Sarlavha</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Sarlavha" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
</Button>
|
/>
|
||||||
</div>
|
|
||||||
</form>
|
<FormField
|
||||||
</Form>
|
control={form.control}
|
||||||
</DialogContent>
|
name="description"
|
||||||
</Dialog>
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Tavsif</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Tavsif" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Sana</FormLabel>
|
||||||
|
<Dialog
|
||||||
|
open={isDateDialogOpen}
|
||||||
|
onOpenChange={setIsDateDialogOpen}
|
||||||
|
>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant="outline" className="w-full justify-start">
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{newTask.date
|
||||||
|
? formatDate.format(newTask.date, "DD-MM-YYYY")
|
||||||
|
: "Sanani tanlang"}
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
|
||||||
|
<DialogContent className="w-auto p-4">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={newTask.date}
|
||||||
|
onSelect={(date) => {
|
||||||
|
if (date) {
|
||||||
|
setNewTask({ ...newTask, date });
|
||||||
|
form.setValue("date", date.toISOString());
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
<div className="mt-4 flex justify-end">
|
||||||
|
<Button onClick={() => setIsDateDialogOpen(false)}>
|
||||||
|
Tanlash
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</FormItem>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
disabled={isPending || editPending}
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Bekor qilish
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" disabled={isPending || editPending}>
|
||||||
|
{isPending || editPending ? (
|
||||||
|
<Loader2 className="animate-spin" />
|
||||||
|
) : taskEdit ? (
|
||||||
|
"Saqlash"
|
||||||
|
) : (
|
||||||
|
"Qo‘shish"
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,13 +21,7 @@ import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import {
|
import { CalendarIcon, Loader2, Pencil, TriangleAlert } from "lucide-react";
|
||||||
CalendarIcon,
|
|
||||||
Loader2,
|
|
||||||
Pencil,
|
|
||||||
Trash,
|
|
||||||
TriangleAlert,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -96,15 +90,15 @@ export default function Plans() {
|
|||||||
setIsPlanDetailsOpen(true);
|
setIsPlanDetailsOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteTask = (task: Task) => {
|
// const handleDeleteTask = (task: Task) => {
|
||||||
setSelectedTask(task);
|
// setSelectedTask(task);
|
||||||
setDeleteDialogOpen(true);
|
// setDeleteDialogOpen(true);
|
||||||
};
|
// };
|
||||||
|
|
||||||
const grouped = groupByDate(data?.data.data || []);
|
const grouped = groupByDate(data?.data.data || []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-3xl font-bold text-foreground">Kunlik Reja</h1>
|
<h1 className="text-3xl font-bold text-foreground">Kunlik Reja</h1>
|
||||||
@@ -209,28 +203,26 @@ export default function Plans() {
|
|||||||
>
|
>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<h3
|
{/* <h3
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"font-semibold wrap-break-word",
|
"font-semibold wrap-break-word",
|
||||||
item.is_done ? "text-green-500" : "text-foreground",
|
item.comment ? "text-green-500" : "text-foreground",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3> */}
|
||||||
<p
|
<h3
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"text-sm wrap-break-word",
|
"font-semibold wrap-break-word",
|
||||||
item.is_done
|
item.comment ? "text-green-500" : "text-foreground",
|
||||||
? "text-green-500"
|
|
||||||
: "text-muted-foreground",
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{item.description}
|
{item.description}
|
||||||
</p>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-1 gap-2">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -240,12 +232,12 @@ export default function Plans() {
|
|||||||
setIsAddDialogOpen(true);
|
setIsAddDialogOpen(true);
|
||||||
}}
|
}}
|
||||||
className="p-4"
|
className="p-4"
|
||||||
disabled={item.is_done}
|
disabled={item.comment ? true : false}
|
||||||
>
|
>
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
<p>Tahrirlash</p>
|
<p>Tahrirlash</p>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
{/* <Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
@@ -257,7 +249,7 @@ export default function Plans() {
|
|||||||
>
|
>
|
||||||
<Trash className="h-4 w-4" />
|
<Trash className="h-4 w-4" />
|
||||||
<p>{"O'chirish"}</p>
|
<p>{"O'chirish"}</p>
|
||||||
</Button>
|
</Button> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -33,4 +33,9 @@ export const order_api = {
|
|||||||
const res = await httpClient.patch(`${ORDER}${id}/update/`, body);
|
const res = await httpClient.patch(`${ORDER}${id}/update/`, body);
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async download_pdf(id: number) {
|
||||||
|
const res = await httpClient.get(`${ORDER}${id}/send_pdf/`);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,14 @@ import { formatPrice } from "@/shared/lib/formatPrice";
|
|||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
|
import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Building2, User } from "lucide-react";
|
import { Building2, Loader2, User } from "lucide-react";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export function DetailViewPage() {
|
export function DetailViewPage() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
|
const [pdfId, setPdfId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
queryKey: ["order_list"],
|
queryKey: ["order_list"],
|
||||||
@@ -27,14 +30,37 @@ export function DetailViewPage() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: pdf,
|
||||||
|
isLoading,
|
||||||
|
isError,
|
||||||
|
error,
|
||||||
|
} = useQuery({
|
||||||
|
queryKey: ["download_pdf", pdfId],
|
||||||
|
queryFn: () => order_api.download_pdf(Number(pdfId!)),
|
||||||
|
enabled: !!pdfId,
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isError) {
|
||||||
|
toast.error(error.message || "Faylni yuklab olishda xatolik yuz berdi", {
|
||||||
|
richColors: true,
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [isError]);
|
||||||
|
|
||||||
const item = data && data.find((h) => h.id === Number(id));
|
const item = data && data.find((h) => h.id === Number(id));
|
||||||
|
|
||||||
const handleDownloadPDF = async () => {};
|
const handleDownloadPDF = (id: number) => {
|
||||||
|
setPdfId(id);
|
||||||
|
console.log(pdf);
|
||||||
|
};
|
||||||
|
|
||||||
if (!item) return <div>{"Ma'lumot topilmadi"}</div>;
|
if (!item) return <div>{"Ma'lumot topilmadi"}</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/specification">
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
<div className="bg-white rounded-lg shadow-lg overflow-hidden">
|
||||||
@@ -160,11 +186,11 @@ export function DetailViewPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleDownloadPDF}
|
onClick={() => handleDownloadPDF(item.id)}
|
||||||
className="w-full h-14 mt-5 text-md bg-green-600 hover:bg-green-700 text-white"
|
className="w-full h-14 mt-5 text-md bg-green-600 hover:bg-green-700 text-white"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
Yuklab olish
|
{isLoading ? <Loader2 className="animate-spin" /> : "Yuklab olish"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</DashboardLayout>
|
</DashboardLayout>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export function HistoryListPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="min-h-screen flex justify-center items-center">
|
<div className="min-h-screen flex justify-center items-center">
|
||||||
<p className="text-gray-500 text-lg">Yuklanmoqda...</p>
|
<p className="text-gray-500 text-lg">Yuklanmoqda...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,7 +32,7 @@ export function HistoryListPage() {
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="min-h-screen flex flex-col justify-center items-center">
|
<div className="min-h-screen flex flex-col justify-center items-center">
|
||||||
<p className="text-red-600 text-lg mb-2">Xatolik yuz berdi</p>
|
<p className="text-red-600 text-lg mb-2">Xatolik yuz berdi</p>
|
||||||
<Button onClick={() => window.location.reload()}>
|
<Button onClick={() => window.location.reload()}>
|
||||||
@@ -44,7 +44,7 @@ export function HistoryListPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex justify-between items-center mb-6">
|
||||||
@@ -125,9 +125,9 @@ export function HistoryListPage() {
|
|||||||
</div>
|
</div>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<p className="text-center text-gray-500 py-10">
|
<div className="fixed h-[70vh] flex justify-center items-center w-full">
|
||||||
{"Hozircha ma'lumot yo‘q."}
|
<p className="text-gray-500">{"Hozircha ma'lumot yo‘q."}</p>
|
||||||
</p>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export function SpecificationPage() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [selectedPharmacy, setSelectedPharmacy] = useState(
|
const [selectedPharmacy, setSelectedPharmacy] = useState(
|
||||||
data ? data[0].id : "",
|
data && data.length > 0 ? data[0].id : "",
|
||||||
);
|
);
|
||||||
|
|
||||||
// const [selectedPharm, setSelectedPharm] = useState(
|
// const [selectedPharm, setSelectedPharm] = useState(
|
||||||
@@ -104,7 +104,7 @@ export function SpecificationPage() {
|
|||||||
if (product) {
|
if (product) {
|
||||||
setMedicines(product?.map((m) => ({ ...m, quantity: 0 })));
|
setMedicines(product?.map((m) => ({ ...m, quantity: 0 })));
|
||||||
}
|
}
|
||||||
if (data) {
|
if (data && data.length > 0) {
|
||||||
setSelectedPharmacy(data[0].id);
|
setSelectedPharmacy(data[0].id);
|
||||||
}
|
}
|
||||||
// if (pharmacy) {
|
// if (pharmacy) {
|
||||||
@@ -165,7 +165,7 @@ export function SpecificationPage() {
|
|||||||
const hasSelectedMedicines = medicines.some((m) => m.quantity > 0);
|
const hasSelectedMedicines = medicines.some((m) => m.quantity > 0);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/specification">
|
||||||
<div className="min-h-screen">
|
<div className="min-h-screen">
|
||||||
<div className="max-w-6xl mx-auto">
|
<div className="max-w-6xl mx-auto">
|
||||||
<h1 className="text-4xl font-bold text-foreground">
|
<h1 className="text-4xl font-bold text-foreground">
|
||||||
|
|||||||
21
src/features/support/lib/api.ts
Normal file
21
src/features/support/lib/api.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { SupportListRes } from "@/features/support/lib/data";
|
||||||
|
import httpClient from "@/shared/config/api/httpClient";
|
||||||
|
import { SUPPORT } from "@/shared/config/api/URLs";
|
||||||
|
import type { AxiosResponse } from "axios";
|
||||||
|
|
||||||
|
export const support_api = {
|
||||||
|
async list(): Promise<AxiosResponse<SupportListRes>> {
|
||||||
|
const res = await httpClient.get(`${SUPPORT}list/`);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async send(body: {
|
||||||
|
district_id?: number;
|
||||||
|
problem: string;
|
||||||
|
date: string;
|
||||||
|
type: "PROBLEM" | "HELP";
|
||||||
|
}) {
|
||||||
|
const res = await httpClient.post(`${SUPPORT}send/`, body);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
};
|
||||||
32
src/features/support/lib/column.tsx
Normal file
32
src/features/support/lib/column.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { SupportListData } from "@/features/support/lib/data";
|
||||||
|
import type { ColumnDef } from "@tanstack/react-table";
|
||||||
|
|
||||||
|
export const columnsSupport = (): ColumnDef<SupportListData>[] => [
|
||||||
|
{
|
||||||
|
accessorKey: "id",
|
||||||
|
header: () => <div className="text-center">№</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return <div className="text-center font-medium">{row.index + 1}</div>;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "name",
|
||||||
|
header: () => <div className="text-center">Xabar tavsifi</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="text-center font-medium">{row.original.problem}</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "districtName",
|
||||||
|
header: () => <div className="text-center">Tuman</div>,
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="text-center font-medium">
|
||||||
|
{row.original.district ? row.original.district.name : "-"}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
80
src/features/support/lib/data-table.tsx
Normal file
80
src/features/support/lib/data-table.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
flexRender,
|
||||||
|
getCoreRowModel,
|
||||||
|
useReactTable,
|
||||||
|
type ColumnDef,
|
||||||
|
} from "@tanstack/react-table";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "@/shared/ui/table";
|
||||||
|
|
||||||
|
interface DataTableProps<ObjectAllData, TValue> {
|
||||||
|
columns: ColumnDef<ObjectAllData, TValue>[];
|
||||||
|
data: ObjectAllData[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DataTableSupport<ObjectAllData, TValue>({
|
||||||
|
columns,
|
||||||
|
data,
|
||||||
|
}: DataTableProps<ObjectAllData, TValue>) {
|
||||||
|
const table = useReactTable({
|
||||||
|
data,
|
||||||
|
columns,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="overflow-hidden rounded-md border">
|
||||||
|
<Table className="">
|
||||||
|
<TableHeader>
|
||||||
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
|
<TableRow key={headerGroup.id}>
|
||||||
|
{headerGroup.headers.map((header) => {
|
||||||
|
return (
|
||||||
|
<TableHead key={header.id} className="border-r">
|
||||||
|
{header.isPlaceholder
|
||||||
|
? null
|
||||||
|
: flexRender(
|
||||||
|
header.column.columnDef.header,
|
||||||
|
header.getContext(),
|
||||||
|
)}
|
||||||
|
</TableHead>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{table.getRowModel().rows?.length ? (
|
||||||
|
table.getRowModel().rows.map((row) => (
|
||||||
|
<TableRow
|
||||||
|
key={row.id}
|
||||||
|
data-state={row.getIsSelected() && "selected"}
|
||||||
|
>
|
||||||
|
{row.getVisibleCells().map((cell) => (
|
||||||
|
<TableCell key={cell.id} className="border-r">
|
||||||
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<TableRow>
|
||||||
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
|
Yordam so'rovlari mavjud emas
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/features/support/lib/data.ts
Normal file
23
src/features/support/lib/data.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
export interface SupportListRes {
|
||||||
|
status_code: number;
|
||||||
|
status: string;
|
||||||
|
message: string;
|
||||||
|
data: {
|
||||||
|
count: number;
|
||||||
|
next: null | string;
|
||||||
|
previous: null | string;
|
||||||
|
results: SupportListData[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SupportListData {
|
||||||
|
id: number;
|
||||||
|
problem: string;
|
||||||
|
date: string;
|
||||||
|
type: string;
|
||||||
|
district: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
217
src/features/support/ui/SendSupport.tsx
Normal file
217
src/features/support/ui/SendSupport.tsx
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
import { district_api } from "@/features/district/lib/api";
|
||||||
|
import { support_api } from "@/features/support/lib/api";
|
||||||
|
import formatDate from "@/shared/lib/formatDate";
|
||||||
|
import { Button } from "@/shared/ui/button";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
import { Textarea } from "@/shared/ui/textarea";
|
||||||
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import type { AxiosError } from "axios";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
|
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
showMainModal: boolean;
|
||||||
|
setShowMainModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SendSupport = ({ setShowMainModal, showMainModal }: Props) => {
|
||||||
|
const [supportType, setSupportType] = useState<"PROBLEM" | "HELP">("HELP");
|
||||||
|
const [districtModal, setDistrictModal] = useState<boolean>(false);
|
||||||
|
const [districtType, setDistrictType] = useState<number | null>(null);
|
||||||
|
const [commentModal, setCommentModal] = useState<boolean>(false);
|
||||||
|
const [comment, setComment] = useState<string>("");
|
||||||
|
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ["my_disctrict"],
|
||||||
|
queryFn: () => district_api.getDiscrict(),
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryClinent = useQueryClient();
|
||||||
|
|
||||||
|
const { mutate, isPending } = useMutation({
|
||||||
|
mutationFn: (body: {
|
||||||
|
district_id?: number;
|
||||||
|
problem: string;
|
||||||
|
date: string;
|
||||||
|
type: "PROBLEM" | "HELP";
|
||||||
|
}) => support_api.send(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Sizning so'rovingiz yuborildi");
|
||||||
|
queryClinent.refetchQueries({ queryKey: ["support_list"] });
|
||||||
|
setCommentModal(false);
|
||||||
|
setDistrictModal(false);
|
||||||
|
setShowMainModal(false);
|
||||||
|
setComment("");
|
||||||
|
setDistrictType(null);
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
const data = error.response?.data as { message?: string };
|
||||||
|
const errorData = error.response?.data as {
|
||||||
|
messages?: {
|
||||||
|
token_class: string;
|
||||||
|
token_type: string;
|
||||||
|
message: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
const errorName = error.response?.data as {
|
||||||
|
data?: {
|
||||||
|
name: string[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const message =
|
||||||
|
Array.isArray(errorName.data?.name) && errorName.data.name.length
|
||||||
|
? errorName.data.name[0]
|
||||||
|
: data?.message ||
|
||||||
|
(Array.isArray(errorData?.messages) && errorData.messages.length
|
||||||
|
? errorData.messages[0].message
|
||||||
|
: undefined) ||
|
||||||
|
"Xatolik yuz berdi";
|
||||||
|
|
||||||
|
toast.error(message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const sendSupport = () => {
|
||||||
|
let body;
|
||||||
|
|
||||||
|
if (districtType === null) {
|
||||||
|
body = {
|
||||||
|
date: formatDate.format(new Date(), "YYYY-MM-DD"),
|
||||||
|
problem: comment,
|
||||||
|
type: supportType,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
body = {
|
||||||
|
date: formatDate.format(new Date(), "YYYY-MM-DD"),
|
||||||
|
district_id: districtType,
|
||||||
|
problem: comment,
|
||||||
|
type: supportType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
mutate(body);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={showMainModal} onOpenChange={setShowMainModal}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl">
|
||||||
|
Sizga qanday yordam kerak
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3 mt-4">
|
||||||
|
<Button
|
||||||
|
className="w-full h-12 text-lg"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSupportType("HELP");
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
setDistrictModal(true);
|
||||||
|
} else {
|
||||||
|
setCommentModal(true);
|
||||||
|
}
|
||||||
|
setShowMainModal(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Yordam so'rash
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full h-12 text-lg"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setSupportType("PROBLEM");
|
||||||
|
setShowMainModal(false);
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
setDistrictModal(true);
|
||||||
|
} else {
|
||||||
|
setCommentModal(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Muammoni hal qilish
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={districtModal} onOpenChange={setDistrictModal}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl">Tummani tanlang</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3 mt-4">
|
||||||
|
{data &&
|
||||||
|
data.length > 0 &&
|
||||||
|
data.map((e) => (
|
||||||
|
<Button
|
||||||
|
key={e.id}
|
||||||
|
className="w-full h-12 text-lg"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setDistrictType(e.id);
|
||||||
|
setDistrictModal(false);
|
||||||
|
setCommentModal(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{e.name}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="flex justify-end items-end mt-5">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setDistrictType(null);
|
||||||
|
setDistrictModal(false);
|
||||||
|
setCommentModal(true);
|
||||||
|
}}
|
||||||
|
className="w-fit h-12"
|
||||||
|
>
|
||||||
|
O'tkazib yuborish
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={commentModal} onOpenChange={setCommentModal}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl">
|
||||||
|
Yordam uchun izoh qoldiring
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div>
|
||||||
|
<Textarea
|
||||||
|
className="min-h-32 max-h-52"
|
||||||
|
placeholder="Izoh"
|
||||||
|
value={comment}
|
||||||
|
onChange={(e) => setComment(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DialogFooter className="flex justify-end items-end mt-5">
|
||||||
|
<Button
|
||||||
|
onClick={() => sendSupport()}
|
||||||
|
disabled={isPending}
|
||||||
|
className="w-fit h-12"
|
||||||
|
>
|
||||||
|
{isPending ? <Loader2 className="animate-spin" /> : "Jo'natish"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SendSupport;
|
||||||
71
src/features/support/ui/SupportList.tsx
Normal file
71
src/features/support/ui/SupportList.tsx
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { support_api } from "@/features/support/lib/api";
|
||||||
|
import { columnsSupport } from "@/features/support/lib/column";
|
||||||
|
import { DataTableSupport } from "@/features/support/lib/data-table";
|
||||||
|
import SendSupport from "@/features/support/ui/SendSupport";
|
||||||
|
import AddedButton from "@/shared/ui/added-button";
|
||||||
|
import { Skeleton } from "@/shared/ui/skeleton";
|
||||||
|
import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
const SupportList = () => {
|
||||||
|
const [showMainModal, setShowMainModal] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const { data, isLoading, isError } = useQuery({
|
||||||
|
queryKey: ["support_list"],
|
||||||
|
queryFn: () => support_api.list(),
|
||||||
|
select(data) {
|
||||||
|
return data.data.data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns = columnsSupport();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout link="/">
|
||||||
|
<AddedButton onClick={() => setShowMainModal(true)} />
|
||||||
|
<div className="space-y-6">
|
||||||
|
<h1 className="text-3xl font-bold">Yordam so'rovlari</h1>
|
||||||
|
|
||||||
|
{isLoading && (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<span className="text-gray-500">Yuklanmoqda...</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isError && (
|
||||||
|
<div className="flex justify-center items-center h-64">
|
||||||
|
<span className="text-red-500">
|
||||||
|
Xatolik yuz berdi. Iltimos, qayta urinib ko‘ring.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{[1, 2, 3].map((i) => (
|
||||||
|
<Skeleton key={i} className="h-12 w-full rounded-md" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : isError ? (
|
||||||
|
<p className="text-red-500">
|
||||||
|
So'rovlar yuklanmadi. Qayta urinib ko‘ring.
|
||||||
|
</p>
|
||||||
|
) : data ? (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<DataTableSupport columns={columns} data={data.results} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-gray-500">Yordam so'rovlari mavjud emas</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SendSupport
|
||||||
|
showMainModal={showMainModal}
|
||||||
|
setShowMainModal={setShowMainModal}
|
||||||
|
/>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SupportList;
|
||||||
@@ -65,7 +65,7 @@ export function DataTable({ columns, data }: DataTableProps<TourItemData>) {
|
|||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||||
No results.
|
Bu oy uchun tur plan mavjud emas
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -135,8 +135,11 @@ export default function TourPlan() {
|
|||||||
});
|
});
|
||||||
}, [data, year, month]);
|
}, [data, year, month]);
|
||||||
|
|
||||||
|
const currentYearSelect = new Date().getFullYear();
|
||||||
|
const years = Array.from({ length: 11 }, (_, i) => currentYearSelect - 5 + i);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout link="/">
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h1 className="text-3xl font-bold">Tur Plan</h1>
|
<h1 className="text-3xl font-bold">Tur Plan</h1>
|
||||||
|
|
||||||
@@ -147,13 +150,14 @@ export default function TourPlan() {
|
|||||||
<SelectTrigger className="w-fit h-10">
|
<SelectTrigger className="w-fit h-10">
|
||||||
<SelectValue placeholder="Yil" />
|
<SelectValue placeholder="Yil" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectItem value="2025">2025</SelectItem>
|
{years.map((y) => (
|
||||||
<SelectItem value="2024">2024</SelectItem>
|
<SelectItem key={y} value={String(y)}>
|
||||||
<SelectItem value="2023">2023</SelectItem>
|
{y}
|
||||||
<SelectItem value="2022">2022</SelectItem>
|
</SelectItem>
|
||||||
<SelectItem value="2021">2021</SelectItem>
|
))}
|
||||||
</SelectGroup>
|
</SelectGroup>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@@ -9,6 +9,7 @@ interface Window {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
sendData?: (data: string) => void;
|
sendData?: (data: string) => void;
|
||||||
|
openLink?: (url: string) => void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/pages/distributed-product/DistributedProduct.tsx
Normal file
12
src/pages/distributed-product/DistributedProduct.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import DistributedList from "@/features/distributed/ui/DistributedList";
|
||||||
|
import TokenLayout from "@/token-layaout";
|
||||||
|
|
||||||
|
const DistributedProduct = () => {
|
||||||
|
return (
|
||||||
|
<TokenLayout>
|
||||||
|
<DistributedList />
|
||||||
|
</TokenLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DistributedProduct;
|
||||||
12
src/pages/support/index.tsx
Normal file
12
src/pages/support/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import SupportList from "@/features/support/ui/SupportList";
|
||||||
|
import TokenLayout from "@/token-layaout";
|
||||||
|
|
||||||
|
const Support = () => {
|
||||||
|
return (
|
||||||
|
<TokenLayout>
|
||||||
|
<SupportList />
|
||||||
|
</TokenLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Support;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import Home from "@/features/home/ui/Home";
|
import Home from "@/features/home/ui/Home";
|
||||||
|
import DistributedProduct from "@/pages/distributed-product/DistributedProduct";
|
||||||
import DistrictPage from "@/pages/district/page";
|
import DistrictPage from "@/pages/district/page";
|
||||||
import Location from "@/pages/location/page";
|
import Location from "@/pages/location/page";
|
||||||
import ObjectAdded from "@/pages/object/added/page";
|
import ObjectAdded from "@/pages/object/added/page";
|
||||||
@@ -14,6 +15,7 @@ import Plan from "@/pages/plan/page";
|
|||||||
import SpecificationAdded from "@/pages/specification/added/page";
|
import SpecificationAdded from "@/pages/specification/added/page";
|
||||||
import SpecificationDetail from "@/pages/specification/history/[id]/page";
|
import SpecificationDetail from "@/pages/specification/history/[id]/page";
|
||||||
import Specification from "@/pages/specification/page";
|
import Specification from "@/pages/specification/page";
|
||||||
|
import Support from "@/pages/support";
|
||||||
import TourPlanPage from "@/pages/tour-plan/page";
|
import TourPlanPage from "@/pages/tour-plan/page";
|
||||||
import { TypePlan } from "@/pages/type-plan/page";
|
import { TypePlan } from "@/pages/type-plan/page";
|
||||||
import TokenLayout from "@/token-layaout";
|
import TokenLayout from "@/token-layaout";
|
||||||
@@ -170,6 +172,22 @@ const routesConfig: RouteObject = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/support",
|
||||||
|
element: <Support />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: "/distributed-product",
|
||||||
|
element: <DistributedProduct />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
export default routesConfig;
|
export default routesConfig;
|
||||||
|
|||||||
@@ -1,24 +1,31 @@
|
|||||||
const BASE_URL =
|
const BASE_URL =
|
||||||
process.env.NEXT_PUBLIC_API_URL || "https://api.meridynpharma.com";
|
process.env.NEXT_PUBLIC_API_URL || "https://api.meridynpharma.com";
|
||||||
|
|
||||||
const CREATE_USER = "/api/v1/accounts/user/create";
|
const API_V = "/api/v1";
|
||||||
const LOGIN_USER = "/api/v1/authentication/login/";
|
|
||||||
const REGIONS = "/api/v1/shared/region/list/";
|
const CREATE_USER = `${API_V}/accounts/user/create`;
|
||||||
const PLANS = "/api/v1/shared/plan/";
|
const LOGIN_USER = `${API_V}/authentication/login/`;
|
||||||
const DISCTRICT = "/api/v1/shared/disctrict/";
|
const REGIONS = `${API_V}/shared/region/list/`;
|
||||||
const OBJECT = "/api/v1/shared/place/";
|
const PLANS = `${API_V}/shared/plan/`;
|
||||||
const DOCTOR = "/api/v1/shared/doctor/";
|
const DISCTRICT = `${API_V}/shared/disctrict/`;
|
||||||
const PHARMACY = "/api/v1/shared/pharmacy/";
|
const OBJECT = `${API_V}/shared/place/`;
|
||||||
const LOCATION = "/api/v1/shared/location/";
|
const DOCTOR = `${API_V}/shared/doctor/`;
|
||||||
const TOUR_PLAN = "/api/v1/shared/tour_plan/";
|
const PHARMACY = `${API_V}/shared/pharmacy/`;
|
||||||
const FACTORY = "/api/v1/shared/factory/list/";
|
const LOCATION = `${API_V}/shared/location/`;
|
||||||
const PRODUCT = "/api/v1/orders/product/list/";
|
const TOUR_PLAN = `${API_V}/shared/tour_plan/`;
|
||||||
const ORDER = "/api/v1/orders/order/";
|
const FACTORY = `${API_V}/shared/factory/list/`;
|
||||||
|
const PRODUCT = `${API_V}/orders/product/list/`;
|
||||||
|
const ORDER = `${API_V}/orders/order/`;
|
||||||
|
const SUPPORT = `${API_V}/shared/support/`;
|
||||||
|
const DISTRIBUTED_LIST = `${API_V}/shared/distributed_product/list/`;
|
||||||
|
const DISTRIBUTED_CREATE = `${API_V}/orders/distributed_product/create/`;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
BASE_URL,
|
BASE_URL,
|
||||||
CREATE_USER,
|
CREATE_USER,
|
||||||
DISCTRICT,
|
DISCTRICT,
|
||||||
|
DISTRIBUTED_CREATE,
|
||||||
|
DISTRIBUTED_LIST,
|
||||||
DOCTOR,
|
DOCTOR,
|
||||||
FACTORY,
|
FACTORY,
|
||||||
LOCATION,
|
LOCATION,
|
||||||
@@ -29,5 +36,6 @@ export {
|
|||||||
PLANS,
|
PLANS,
|
||||||
PRODUCT,
|
PRODUCT,
|
||||||
REGIONS,
|
REGIONS,
|
||||||
|
SUPPORT,
|
||||||
TOUR_PLAN,
|
TOUR_PLAN,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type State = {
|
|||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
id?: number;
|
||||||
} | null;
|
} | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ type Actions = {
|
|||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
id?: number;
|
||||||
} | null,
|
} | null,
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
@@ -37,6 +39,7 @@ export const userInfoStore = create<State & Actions>((set) => ({
|
|||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
id?: number;
|
||||||
} | null,
|
} | null,
|
||||||
) => set(() => ({ loginUser: login })),
|
) => set(() => ({ loginUser: login })),
|
||||||
user: {
|
user: {
|
||||||
@@ -44,6 +47,7 @@ export const userInfoStore = create<State & Actions>((set) => ({
|
|||||||
last_name: "",
|
last_name: "",
|
||||||
user_id: "",
|
user_id: "",
|
||||||
active: false,
|
active: false,
|
||||||
|
id: null,
|
||||||
},
|
},
|
||||||
addedUser: (user) => set(() => ({ user })),
|
addedUser: (user) => set(() => ({ user })),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const rippleVariants = {
|
|||||||
|
|
||||||
const AddedButton: React.FC<AddedButtonProps> = ({ onClick }) => {
|
const AddedButton: React.FC<AddedButtonProps> = ({ onClick }) => {
|
||||||
return (
|
return (
|
||||||
<div className="fixed bottom-8 right-8 flex items-center justify-center w-16 h-16">
|
<div className="fixed bottom-8 right-8 flex items-center justify-center w-16 h-16 z-50">
|
||||||
{[0.5, 1, 1.5].map((delay, i) => (
|
{[0.5, 1, 1.5].map((delay, i) => (
|
||||||
<motion.span
|
<motion.span
|
||||||
key={i}
|
key={i}
|
||||||
|
|||||||
184
src/shared/ui/command.tsx
Normal file
184
src/shared/ui/command.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import * as React from "react";
|
||||||
|
import { Command as CommandPrimitive } from "cmdk";
|
||||||
|
import { SearchIcon } from "lucide-react";
|
||||||
|
|
||||||
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/shared/ui/dialog";
|
||||||
|
|
||||||
|
function Command({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive
|
||||||
|
data-slot="command"
|
||||||
|
className={cn(
|
||||||
|
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandDialog({
|
||||||
|
title = "Command Palette",
|
||||||
|
description = "Search for a command to run...",
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
showCloseButton = true,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof Dialog> & {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
className?: string;
|
||||||
|
showCloseButton?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Dialog {...props}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>{title}</DialogTitle>
|
||||||
|
<DialogDescription>{description}</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className={cn("overflow-hidden p-0", className)}
|
||||||
|
showCloseButton={showCloseButton}
|
||||||
|
>
|
||||||
|
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||||
|
{children}
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandInput({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-slot="command-input-wrapper"
|
||||||
|
className="flex h-9 items-center gap-2 border-b px-3"
|
||||||
|
>
|
||||||
|
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||||
|
<CommandPrimitive.Input
|
||||||
|
data-slot="command-input"
|
||||||
|
className={cn(
|
||||||
|
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.List
|
||||||
|
data-slot="command-list"
|
||||||
|
className={cn(
|
||||||
|
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandEmpty({
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Empty
|
||||||
|
data-slot="command-empty"
|
||||||
|
className="py-6 text-center text-sm"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandGroup({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Group
|
||||||
|
data-slot="command-group"
|
||||||
|
className={cn(
|
||||||
|
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandSeparator({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Separator
|
||||||
|
data-slot="command-separator"
|
||||||
|
className={cn("bg-border -mx-1 h-px", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandItem({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||||
|
return (
|
||||||
|
<CommandPrimitive.Item
|
||||||
|
data-slot="command-item"
|
||||||
|
className={cn(
|
||||||
|
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function CommandShortcut({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<"span">) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
data-slot="command-shortcut"
|
||||||
|
className={cn(
|
||||||
|
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
Command,
|
||||||
|
CommandDialog,
|
||||||
|
CommandInput,
|
||||||
|
CommandList,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandItem,
|
||||||
|
CommandShortcut,
|
||||||
|
CommandSeparator,
|
||||||
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import * as React from "react";
|
|
||||||
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
import * as SheetPrimitive from "@radix-ui/react-dialog";
|
||||||
import { XIcon } from "lucide-react";
|
import { XIcon } from "lucide-react";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
|
|
||||||
@@ -47,10 +47,12 @@ function SheetOverlay({
|
|||||||
function SheetContent({
|
function SheetContent({
|
||||||
className,
|
className,
|
||||||
children,
|
children,
|
||||||
|
closedButton,
|
||||||
side = "right",
|
side = "right",
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
}: React.ComponentProps<typeof SheetPrimitive.Content> & {
|
||||||
side?: "top" | "right" | "bottom" | "left";
|
side?: "top" | "right" | "bottom" | "left";
|
||||||
|
closedButton?: boolean;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<SheetPortal>
|
<SheetPortal>
|
||||||
@@ -72,10 +74,12 @@ function SheetContent({
|
|||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
{closedButton && (
|
||||||
<XIcon className="size-4" />
|
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||||
<span className="sr-only">Close</span>
|
<XIcon className="size-4" />
|
||||||
</SheetPrimitive.Close>
|
<span className="sr-only">Close</span>
|
||||||
|
</SheetPrimitive.Close>
|
||||||
|
)}
|
||||||
</SheetPrimitive.Content>
|
</SheetPrimitive.Content>
|
||||||
</SheetPortal>
|
</SheetPortal>
|
||||||
);
|
);
|
||||||
@@ -129,11 +133,11 @@ function SheetDescription({
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetTrigger,
|
|
||||||
SheetClose,
|
SheetClose,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
SheetHeader,
|
|
||||||
SheetFooter,
|
|
||||||
SheetTitle,
|
|
||||||
SheetDescription,
|
SheetDescription,
|
||||||
|
SheetFooter,
|
||||||
|
SheetHeader,
|
||||||
|
SheetTitle,
|
||||||
|
SheetTrigger,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import LoginForm from "@/features/auth/ui/login";
|
|||||||
import { userInfoStore } from "@/shared/hooks/user-info";
|
import { userInfoStore } from "@/shared/hooks/user-info";
|
||||||
import { getToken, removeToken, saveToken } from "@/shared/lib/cookie";
|
import { getToken, removeToken, saveToken } from "@/shared/lib/cookie";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useEffect } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
const TokenLayout = ({ children }: { children: React.ReactNode }) => {
|
const TokenLayout = ({ children }: { children: React.ReactNode }) => {
|
||||||
const { addedUser, loginUser, setLoginUser } = userInfoStore();
|
const { addedUser, loginUser, setLoginUser } = userInfoStore();
|
||||||
const token = getToken();
|
const gettoken = getToken();
|
||||||
|
const [token, setToken] = useState<string | null>(null);
|
||||||
|
|
||||||
const { mutate: login, isPending } = useMutation({
|
const { mutate: login, isPending } = useMutation({
|
||||||
mutationFn: (body: { telegram_id: string }) => auth_api.login(body),
|
mutationFn: (body: { telegram_id: string }) => auth_api.login(body),
|
||||||
@@ -19,6 +20,7 @@ const TokenLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
active: user.is_active,
|
active: user.is_active,
|
||||||
first_name: user.first_name,
|
first_name: user.first_name,
|
||||||
last_name: user.last_name,
|
last_name: user.last_name,
|
||||||
|
id: user.id,
|
||||||
});
|
});
|
||||||
if (user.token) saveToken(user.token);
|
if (user.token) saveToken(user.token);
|
||||||
},
|
},
|
||||||
@@ -44,6 +46,48 @@ const TokenLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
}
|
}
|
||||||
}, [addedUser, login]);
|
}, [addedUser, login]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loginUser === null) return;
|
||||||
|
|
||||||
|
const socket = new WebSocket(
|
||||||
|
`wss://api.meridynpharma.com/ws/user_activation/${loginUser.id}/`,
|
||||||
|
);
|
||||||
|
|
||||||
|
socket.onopen = () => {
|
||||||
|
console.log("WebSocket connected ✅");
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.token) {
|
||||||
|
saveToken(data.token);
|
||||||
|
setToken(data.token);
|
||||||
|
|
||||||
|
// User holatini yangilash
|
||||||
|
setLoginUser({
|
||||||
|
...loginUser,
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("WebSocket message parse error:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onerror = (err) => {
|
||||||
|
console.error("WebSocket error:", err);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.onclose = () => {
|
||||||
|
console.log("WebSocket closed");
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket.close();
|
||||||
|
};
|
||||||
|
}, [loginUser]);
|
||||||
|
|
||||||
if (isPending && loginUser === null) {
|
if (isPending && loginUser === null) {
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900">
|
<div className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900">
|
||||||
@@ -66,7 +110,7 @@ const TokenLayout = ({ children }: { children: React.ReactNode }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <>{token ? children : <LoginForm />}</>;
|
return <>{token || gettoken ? children : <LoginForm onLogin={login} />}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TokenLayout;
|
export default TokenLayout;
|
||||||
|
|||||||
@@ -1,22 +1,30 @@
|
|||||||
import Logo from "@/assets/logo.png";
|
import Logo from "@/assets/logo.png";
|
||||||
|
import { location_api, type SendLocation } from "@/features/home/lib/api";
|
||||||
|
|
||||||
import { cn } from "@/shared/lib/utils";
|
import { cn } from "@/shared/lib/utils";
|
||||||
import { Button } from "@/shared/ui/button";
|
import { Button } from "@/shared/ui/button";
|
||||||
import { Sheet, SheetContent } from "@/shared/ui/sheet";
|
import { Sheet, SheetClose, SheetContent } from "@/shared/ui/sheet";
|
||||||
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
import type { AxiosError } from "axios";
|
||||||
import {
|
import {
|
||||||
Building,
|
Banknote,
|
||||||
Building2,
|
|
||||||
Calendar,
|
Calendar,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
FileText,
|
FileText,
|
||||||
MapPin,
|
Home,
|
||||||
|
List,
|
||||||
|
Loader2,
|
||||||
|
MapPinCheck,
|
||||||
|
MapPinHouse,
|
||||||
|
MapPinned,
|
||||||
Menu,
|
Menu,
|
||||||
Navigation,
|
Pill,
|
||||||
Truck,
|
|
||||||
User,
|
User,
|
||||||
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import { Link, useLocation, useNavigate, useParams } from "react-router-dom";
|
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface NavItem {
|
interface NavItem {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -25,24 +33,69 @@ interface NavItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const navItems: NavItem[] = [
|
const navItems: NavItem[] = [
|
||||||
{ title: "Reja", href: "/plan", icon: Calendar },
|
{ title: "Asosiy sahifa", href: "/", icon: Home },
|
||||||
{ title: "Tuman", href: "/district", icon: MapPin },
|
{ title: "Lokatsiya jo'natish", href: "/location", icon: MapPinCheck },
|
||||||
{ title: "Obyekt", href: "/object", icon: Building2 },
|
|
||||||
{ title: "Shifokor", href: "/physician", icon: User },
|
|
||||||
{ title: "Dorixona", href: "/pharmacy", icon: Building },
|
|
||||||
{ title: "Tur Plan", href: "/type-plan", icon: Truck },
|
|
||||||
{ title: "Lokatsiya", href: "/location", icon: Navigation },
|
|
||||||
{ title: "Spetsifikatsiya", href: "/specification", icon: FileText },
|
{ title: "Spetsifikatsiya", href: "/specification", icon: FileText },
|
||||||
|
{ title: "Tur Plan", href: "/tour-plan", icon: List },
|
||||||
|
{ title: "Reja", href: "/plan", icon: Calendar },
|
||||||
|
{ title: "Tuman", href: "/district", icon: MapPinned },
|
||||||
|
{ title: "Obyekt", href: "/object", icon: MapPinHouse },
|
||||||
|
{ title: "Shifokor", href: "/physician", icon: User },
|
||||||
|
{ title: "Dorixona", href: "/pharmacy", icon: Pill },
|
||||||
|
{ title: "To'lovlar", href: "/type-plan", icon: Banknote },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function DashboardLayout({ children }: { children: ReactNode }) {
|
export function DashboardLayout({
|
||||||
|
children,
|
||||||
|
link,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
link: string;
|
||||||
|
}) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const pathname = location.pathname;
|
const pathname = location.pathname;
|
||||||
const router = useNavigate();
|
const router = useNavigate();
|
||||||
const params = useParams();
|
|
||||||
const locale = params.locale as string;
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||||
|
|
||||||
|
const [locationLoad, setLocationLoad] = useState(false);
|
||||||
|
|
||||||
|
const { mutate: sendLocation } = useMutation({
|
||||||
|
mutationFn: (body: SendLocation) => location_api.send_loaction(body),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Lokatsiya jo'natildi");
|
||||||
|
setLocationLoad(false);
|
||||||
|
router("/location");
|
||||||
|
},
|
||||||
|
onError: (error: AxiosError) => {
|
||||||
|
const data = error.response?.data as { message?: string };
|
||||||
|
toast.error(data?.message || "Xatolik yuz berdi");
|
||||||
|
setLocationLoad(false);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSidebarLocationClick = () => {
|
||||||
|
setLocationLoad(true);
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(pos) => {
|
||||||
|
sendLocation({
|
||||||
|
latitude: pos.coords.latitude,
|
||||||
|
longitude: pos.coords.longitude,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
toast.error("Lokatsiya olishda xatolik");
|
||||||
|
console.error(err);
|
||||||
|
setLocationLoad(false);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 15000,
|
||||||
|
maximumAge: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-background">
|
<div className="min-h-screen bg-background">
|
||||||
{/* Sidebar - Desktop */}
|
{/* Sidebar - Desktop */}
|
||||||
@@ -55,21 +108,46 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
<ul role="list" className="flex flex-1 flex-col gap-y-1">
|
<ul role="list" className="flex flex-1 flex-col gap-y-1">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = pathname?.includes(item.href);
|
const isActive =
|
||||||
|
item.href === "/"
|
||||||
|
? pathname === "/"
|
||||||
|
: pathname.startsWith(item.href);
|
||||||
|
|
||||||
|
const isLocationItem = item.href === "/location";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={item.href}>
|
<li key={item.href}>
|
||||||
<Link
|
{isLocationItem ? (
|
||||||
to={`/${locale}${item.href}`}
|
<button
|
||||||
className={cn(
|
onClick={handleSidebarLocationClick}
|
||||||
"group flex gap-x-3 p-3 text-sm font-semibold leading-6 transition-colors",
|
className={cn(
|
||||||
isActive
|
"group flex gap-x-3 p-3 text-left w-full text-sm font-semibold leading-6 transition-colors",
|
||||||
? "bg-accent-gradient-soft text-primary rounded-xl-soft shadow-sm"
|
isActive
|
||||||
: "text-sidebar-foreground hover:bg-accent-gradient-soft hover:text-primary rounded-lg",
|
? "bg-accent-gradient-soft text-primary rounded-xl-soft shadow-sm"
|
||||||
)}
|
: "text-sidebar-foreground hover:bg-accent-gradient-soft hover:text-primary rounded-lg",
|
||||||
>
|
)}
|
||||||
<Icon className="h-5 w-5 shrink-0" />
|
>
|
||||||
{item.title}
|
{locationLoad ? (
|
||||||
</Link>
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Icon className="h-5 w-5 shrink-0" />
|
||||||
|
)}
|
||||||
|
{item.title}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to={item.href}
|
||||||
|
className={cn(
|
||||||
|
"group flex gap-x-3 p-3 text-sm font-semibold leading-6 transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-accent-gradient-soft text-primary rounded-xl-soft shadow-sm"
|
||||||
|
: "text-sidebar-foreground hover:bg-accent-gradient-soft hover:text-primary rounded-lg",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5 shrink-0" />
|
||||||
|
{item.title}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -82,7 +160,7 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
<div className="sticky top-0 z-40 flex justify-between h-16 shrink-0 items-center gap-x-4 border-b border-border bg-background px-1 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8 lg:pl-72">
|
<div className="sticky top-0 z-40 flex justify-between h-16 shrink-0 items-center gap-x-4 border-b border-border bg-background px-1 shadow-sm sm:gap-x-6 sm:px-6 lg:px-8 lg:pl-72">
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
onClick={() => router(-1)}
|
onClick={() => router(link)}
|
||||||
className="group flex items-center gap-2 px-2 py-5 ml-2 text-foreground hover:text-primary hover:bg-accent/50 transition-all duration-200 rounded-lg"
|
className="group flex items-center gap-2 px-2 py-5 ml-2 text-foreground hover:text-primary hover:bg-accent/50 transition-all duration-200 rounded-lg"
|
||||||
>
|
>
|
||||||
<ChevronLeft className="size-7 transition-transform text-white group-hover:-translate-x-1" />
|
<ChevronLeft className="size-7 transition-transform text-white group-hover:-translate-x-1" />
|
||||||
@@ -105,30 +183,59 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
|
|||||||
|
|
||||||
{/* Mobile Sidebar */}
|
{/* Mobile Sidebar */}
|
||||||
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
<Sheet open={sidebarOpen} onOpenChange={setSidebarOpen}>
|
||||||
<SheetContent side="left" className="w-64 p-0">
|
<SheetContent side="left" className="w-64 p-0" closedButton={false}>
|
||||||
<div className="flex h-16 shrink-0 items-center border-b border-sidebar-border">
|
<div className="flex justify-between h-16 px-2 shrink-0 items-center border-b border-sidebar-border">
|
||||||
<img src={Logo} alt="logo" className="w-32 h-12 ml-2" />
|
<img src={Logo} alt="logo" className="w-32 h-10 ml-2" />
|
||||||
|
<SheetClose asChild>
|
||||||
|
<Button size={"icon"} variant={"ghost"}>
|
||||||
|
<X className="size-5 text-gray-600" />
|
||||||
|
</Button>
|
||||||
|
</SheetClose>
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex flex-1 flex-col p-6">
|
<nav className="flex flex-1 flex-col px-2">
|
||||||
<ul role="list" className="flex flex-1 flex-col gap-y-1">
|
<ul role="list" className="flex flex-1 flex-col">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const Icon = item.icon;
|
const Icon = item.icon;
|
||||||
const isActive = pathname?.includes(item.href);
|
const isActive =
|
||||||
|
item.href === "/"
|
||||||
|
? pathname === "/"
|
||||||
|
: pathname.startsWith(item.href);
|
||||||
|
|
||||||
|
const isLocationItem = item.href === "/location";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={item.href}>
|
<li key={item.href}>
|
||||||
<Link
|
{isLocationItem ? (
|
||||||
to={`/$${item.href}`}
|
<button
|
||||||
onClick={() => setSidebarOpen(false)}
|
onClick={handleSidebarLocationClick}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex gap-x-3 p-3 text-sm font-semibold leading-6 transition-colors",
|
"group flex gap-x-3 p-3 text-left w-full text-sm font-semibold leading-6 transition-colors",
|
||||||
isActive
|
isActive
|
||||||
? "bg-accent-gradient-soft text-primary rounded-xl-soft shadow-sm"
|
? "bg-accent-gradient-soft text-primary rounded-xl-soft shadow-sm"
|
||||||
: "text-sidebar-foreground hover:bg-accent-gradient-soft hover:text-primary rounded-lg",
|
: "text-sidebar-foreground hover:bg-accent-gradient-soft hover:text-primary rounded-lg",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon className="h-5 w-5 shrink-0" />
|
{locationLoad ? (
|
||||||
{item.title}
|
<Loader2 className="h-5 w-5 animate-spin" />
|
||||||
</Link>
|
) : (
|
||||||
|
<Icon className="h-5 w-5 shrink-0" />
|
||||||
|
)}
|
||||||
|
{item.title}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
to={item.href}
|
||||||
|
className={cn(
|
||||||
|
"group flex gap-x-3 p-3 text-sm font-semibold leading-6 transition-colors",
|
||||||
|
isActive
|
||||||
|
? "bg-accent-gradient-soft text-primary rounded-xl-soft shadow-sm"
|
||||||
|
: "text-sidebar-foreground hover:bg-accent-gradient-soft hover:text-primary rounded-lg",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5 shrink-0" />
|
||||||
|
{item.title}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -14,9 +14,10 @@ export default defineConfig({
|
|||||||
"process.env": {},
|
"process.env": {},
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
|
port: 5174,
|
||||||
host: true, // barcha hostlarga ruxsat
|
host: true, // barcha hostlarga ruxsat
|
||||||
allowedHosts: [
|
allowedHosts: [
|
||||||
"explaining-spoke-component-awareness.trycloudflare.com", // ngrok host qo'shildi
|
"spin-ronald-officers-reasonably.trycloudflare.com", // ngrok host qo'shildi
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user