Compare commits

..

10 Commits

Author SHA1 Message Date
Samandar Turgunboyev
9d84fc38f3 initial commit 2025-12-09 17:02:13 +05:00
Samandar Turgunboyev
db6bfa7e40 update new api reques and response 2025-12-05 17:47:11 +05:00
Samandar Turgunboyev
21725762c6 sheet closed button 2025-12-03 11:15:17 +05:00
Samandar Turgunboyev
ba419b8de4 back page update 2025-12-03 11:10:27 +05:00
Samandar Turgunboyev
2fb567d93f back page update 2025-12-03 11:00:57 +05:00
Samandar Turgunboyev
d4788c7cb2 ui edit 2025-12-01 13:23:40 +05:00
Samandar Turgunboyev
5c4e1327be websocket bug fiz 2025-11-29 10:34:56 +05:00
Samandar Turgunboyev
6cf0a4200b added socket 2025-11-28 19:18:27 +05:00
Samandar Turgunboyev
d224cbeb38 added socket 2025-11-28 19:17:21 +05:00
Samandar Turgunboyev
85c0eee6dd order price update 2025-11-28 17:36:54 +05:00
58 changed files with 2169 additions and 490 deletions

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
legacy-peer-deps=true

17
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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 };

View 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;
},
};

View 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>
);
},
},
];

View 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>
);
}

View 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;
}

View 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 qoshish</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>
);
}

View 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;

View 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 koring.
</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;

View File

@@ -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} /> Ochirish <Trash size={16} /> Ochirish
</Button> </Button> */}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
); );

View File

@@ -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>
)} )}

View File

@@ -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 royxati</h1> <h1 className="text-3xl font-bold">Tumanlar royxati</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>

View File

@@ -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>
); );

View File

@@ -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>
)} )}

View File

@@ -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) => (

View File

@@ -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}

View File

@@ -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>

View File

@@ -4,16 +4,19 @@ 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,
Layers,
List, List,
MapPin, Loader2,
Syringe, MapPinCheck,
MapPinHouse,
MapPinned,
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 +34,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,
}, },
@@ -56,12 +59,14 @@ const navItems: NavItem[] = [
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 +82,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 +98,7 @@ export default function Home() {
}); });
const handleLocationClick = () => { const handleLocationClick = () => {
setLocationLoad(true);
navigator.geolocation.getCurrentPosition( navigator.geolocation.getCurrentPosition(
(pos) => { (pos) => {
mutate({ mutate({
@@ -125,41 +132,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 +224,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 +232,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 +256,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 +271,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>
); );

View File

@@ -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>
)} )}

View File

@@ -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>
)} )}

View File

@@ -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}>

View File

@@ -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 royxati</h1> <h1 className="text-3xl font-bold">Obyektlar royxati</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 koring.
</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>

View File

@@ -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">

View File

@@ -229,7 +229,7 @@ const CreatePharmacy = () => {
}; };
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 +357,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 +368,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) => (

View File

@@ -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>
)} )}

View File

@@ -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">

View File

@@ -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 && (

View File

@@ -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,6 +110,12 @@ const PlanTour = () => {
</div> </div>
)} )}
{!isLoading && !isError && data && data.length === 0 ? (
<div className="h-[80vh] flex justify-center items-center w-[90%] fixed">
<p>Hech qanday to'lovlar yo'q</p>
</div>
) : (
<>
<Card className="mb-4 border-0 p-0 mt-5"> <Card className="mb-4 border-0 p-0 mt-5">
<CardHeader className="bg-blue-500 p-3 text-white"> <CardHeader className="bg-blue-500 p-3 text-white">
<CardTitle className="flex items-center gap-2 justify-center"> <CardTitle className="flex items-center gap-2 justify-center">
@@ -143,8 +149,9 @@ const PlanTour = () => {
</CardContent> </CardContent>
</Card> </Card>
{/* 🔥 Oylik narxlar */}
<PlanPrice selectedMonth={selectedMonth} pharmacies={pharmacies} /> <PlanPrice selectedMonth={selectedMonth} pharmacies={pharmacies} />
</>
)}
</div> </div>
</DashboardLayout> </DashboardLayout>
); );

View File

@@ -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;
}, },

View File

@@ -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;
} }

View File

@@ -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,11 +88,13 @@ 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">
{!commentOpen && (
<>
{/* Status */} {/* Status */}
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div> <div>
@@ -87,10 +102,10 @@ export function PlanDetailsDialog({
<p <p
className={clsx( className={clsx(
"font-semibold", "font-semibold",
task.is_done ? "text-green-500" : "text-foreground", task.comment ? "text-green-500" : "text-foreground",
)} )}
> >
{task.is_done ? "Tugallangan ✓" : "Bajarilmagan"} {task.comment ? "Tugallangan ✓" : "Bajarilmagan"}
</p> </p>
</div> </div>
</div> </div>
@@ -100,8 +115,8 @@ export function PlanDetailsDialog({
<p className="text-sm text-muted-foreground">Sarlavha</p> <p className="text-sm text-muted-foreground">Sarlavha</p>
<p <p
className={clsx( className={clsx(
"text-lg font-semibold break-words", "text-lg font-semibold",
task.is_done ? "text-green-500" : "text-foreground", task.comment ? "text-green-500" : "text-foreground",
)} )}
> >
{task.title} {task.title}
@@ -113,8 +128,8 @@ export function PlanDetailsDialog({
<p className="text-sm text-muted-foreground">Tavsifi</p> <p className="text-sm text-muted-foreground">Tavsifi</p>
<p <p
className={clsx( className={clsx(
"text-base break-words leading-relaxed", "text-base leading-relaxed",
task.is_done ? "text-green-500" : "text-foreground", task.comment ? "text-green-500" : "text-foreground",
)} )}
> >
{task.description} {task.description}
@@ -128,21 +143,50 @@ export function PlanDetailsDialog({
{format(task.date, "dd.MM.yyyy")} {format(task.date, "dd.MM.yyyy")}
</p> </p>
</div> </div>
</>
)}
{commentOpen && (
<div>
<Textarea
className="min-h-32 max-h-52"
placeholder="Reja qanday bajarildi yozing"
value={commentText}
onChange={(e) => setCommnetText(e.target.value)}
/>
</div>
)}
{/* Close Button */} {/* Close Button */}
<div className="flex justify-end gap-2 pt-4"> <div className="flex justify-end gap-2 pt-4">
{commentOpen ? (
<Button <Button
onClick={() => { disabled={
mutate(task.id); (task.comment && task.comment.length > 0) || isPending
}} }
disabled={task.is_done || isPending} onClick={() =>
mutate({ body: { comment: commentText }, id: task.id })
}
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" className="w-fit p-5 rounded-lg"
> >
Bajarildi Bajarildi
</Button> </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"
> >

View File

@@ -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";
@@ -38,15 +41,6 @@ const plansForm = z.object({
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,11 +56,17 @@ 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),
@@ -77,6 +77,20 @@ export const AddPlans = ({
}, },
}); });
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: () => {
@@ -158,33 +172,193 @@ export const AddPlans = ({
} }
}, [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, 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,
title: values.title,
}, },
id: taskEdit.id, id: taskEdit.id,
}); });
} else { } else {
added({ added({
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,
title: values.title,
}); });
} }
} }
return ( return (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <>
<Dialog open={showMainModal} onOpenChange={setShowMainModal}>
<DialogTrigger asChild> <DialogTrigger asChild>
<AddedButton onClick={() => setTaskEdit(null)} /> <AddedButton
onClick={() => {
setShowMainModal(true);
setTaskEdit(null);
}}
/>
</DialogTrigger> </DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="text-2xl">
{"Rejani biriktirish"}
</DialogTitle>
</DialogHeader>
<div className="space-y-3 mt-4">
{(["doctor", "pharm"] as const).map((type) => (
<Button
key={type}
onClick={() => {
setSelectType(type);
setShowSelectModal(true);
setShowMainModal(false);
}}
className="w-full h-12 text-lg"
variant="outline"
>
{typeLabel(type)}
</Button>
))}
</div>
</DialogContent>
</Dialog>
<Dialog open={showSelectModal} onOpenChange={setShowSelectModal}>
<DialogContent className="sm:max-w-md h-[60%] flex flex-col justify-start overflow-y-auto">
<DialogHeader>
<DialogTitle className="text-2xl">
Mavjud {selectType && typeLabel(selectType)}lar
</DialogTitle>
</DialogHeader>
<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);
}
}}
>
<MapPin className="h-4 w-4" />
{label}
</Button>
);
})
)}
</div>
<Button
onClick={() => setShowSelectModal(false)}
variant="outline"
className="w-full mt-2"
>
Bekor qilish
</Button>
</DialogContent>
</Dialog>
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogContent className="max-h-[90vh] overflow-y-auto"> <DialogContent className="max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@@ -231,7 +405,7 @@ export const AddPlans = ({
<Button variant="outline" className="w-full justify-start"> <Button variant="outline" className="w-full justify-start">
<CalendarIcon className="mr-2 h-4 w-4" /> <CalendarIcon className="mr-2 h-4 w-4" />
{newTask.date {newTask.date
? format(newTask.date, "dd-MM-yyyy") ? formatDate.format(newTask.date, "DD-MM-YYYY")
: "Sanani tanlang"} : "Sanani tanlang"}
</Button> </Button>
</DialogTrigger> </DialogTrigger>
@@ -280,5 +454,6 @@ export const AddPlans = ({
</Form> </Form>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
</>
); );
}; };

View File

@@ -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>
@@ -212,7 +206,7 @@ export default function Plans() {
<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}
@@ -220,7 +214,7 @@ export default function Plans() {
<p <p
className={clsx( className={clsx(
"text-sm wrap-break-word", "text-sm wrap-break-word",
item.is_done item.comment
? "text-green-500" ? "text-green-500"
: "text-muted-foreground", : "text-muted-foreground",
)} )}
@@ -230,7 +224,7 @@ export default function Plans() {
</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 +234,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 +251,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>
))} ))}

View File

@@ -34,7 +34,7 @@ export function DetailViewPage() {
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">

View File

@@ -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 yoq."} <p className="text-gray-500">{"Hozircha ma'lumot yoq."}</p>
</p> </div>
)} )}
</div> </div>
</div> </div>

View File

@@ -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">

View 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;
},
};

View 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>
);
},
},
];

View 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>
);
}

View 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;
}

View 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;

View 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 koring.
</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 koring.
</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;

View File

@@ -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>
)} )}

View File

@@ -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
View File

@@ -9,6 +9,7 @@ interface Window {
}; };
}; };
sendData?: (data: string) => void; sendData?: (data: string) => void;
openLink?: (url: string) => void;
}; };
}; };
} }

View 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;

View 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;

View File

@@ -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;

View File

@@ -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,
}; };

View File

@@ -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 })),
})); }));

View File

@@ -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
View 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,
};

View File

@@ -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}
{closedButton && (
<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"> <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">
<XIcon className="size-4" /> <XIcon className="size-4" />
<span className="sr-only">Close</span> <span className="sr-only">Close</span>
</SheetPrimitive.Close> </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,
}; };

View File

@@ -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;

View File

@@ -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,11 +108,35 @@ 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}>
{isLocationItem ? (
<button
onClick={handleSidebarLocationClick}
className={cn(
"group flex gap-x-3 p-3 text-left w-full 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",
)}
>
{locationLoad ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Icon className="h-5 w-5 shrink-0" />
)}
{item.title}
</button>
) : (
<Link <Link
to={`/${locale}${item.href}`} to={item.href}
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-sm font-semibold leading-6 transition-colors",
isActive isActive
@@ -70,6 +147,7 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
<Icon className="h-5 w-5 shrink-0" /> <Icon className="h-5 w-5 shrink-0" />
{item.title} {item.title}
</Link> </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,20 +183,48 @@ 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}>
{isLocationItem ? (
<button
onClick={handleSidebarLocationClick}
className={cn(
"group flex gap-x-3 p-3 text-left w-full 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",
)}
>
{locationLoad ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<Icon className="h-5 w-5 shrink-0" />
)}
{item.title}
</button>
) : (
<Link <Link
to={`/$${item.href}`} to={item.href}
onClick={() => setSidebarOpen(false)}
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-sm font-semibold leading-6 transition-colors",
isActive isActive
@@ -129,6 +235,7 @@ export function DashboardLayout({ children }: { children: ReactNode }) {
<Icon className="h-5 w-5 shrink-0" /> <Icon className="h-5 w-5 shrink-0" />
{item.title} {item.title}
</Link> </Link>
)}
</li> </li>
); );
})} })}

View File

@@ -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 "hampshire-blake-womens-ref.trycloudflare.com", // ngrok host qo'shildi
], ],
}, },
}); });