update new api reques and response
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -37,6 +37,7 @@
|
||||
"axios": "^1.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.18",
|
||||
"framer-motion": "^12.23.24",
|
||||
@@ -4007,6 +4008,22 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"axios": "^1.9.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "^1.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"dayjs": "^1.11.18",
|
||||
"framer-motion": "^12.23.24",
|
||||
|
||||
21
src/features/distributed/lib/api.ts
Normal file
21
src/features/distributed/lib/api.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { DistributedList } from "@/features/distributed/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { DISTRIBUTED_CREATE, DISTRIBUTED_LIST } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const distributed_api = {
|
||||
async list(): Promise<AxiosResponse<DistributedList>> {
|
||||
const res = await httpClient.get(DISTRIBUTED_LIST);
|
||||
return res;
|
||||
},
|
||||
|
||||
async create(body: {
|
||||
product_id: number;
|
||||
date: string;
|
||||
employee_name: string;
|
||||
quantity: number;
|
||||
}) {
|
||||
const res = await httpClient.post(DISTRIBUTED_CREATE, body);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
75
src/features/distributed/lib/column.tsx
Normal file
75
src/features/distributed/lib/column.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { DistributedListData } from "@/features/distributed/lib/data";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { EllipsisVertical, Eye } from "lucide-react";
|
||||
|
||||
interface ColumnProps {
|
||||
handleEdit: (district: DistributedListData) => void;
|
||||
}
|
||||
|
||||
export const columnsDistributed = ({
|
||||
handleEdit,
|
||||
}: ColumnProps): ColumnDef<DistributedListData>[] => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: () => <div className="text-center">№</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center font-medium">{row.index + 1}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: () => <div className="text-center">Xaridoring ismi</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center font-medium">
|
||||
{row.original.employee_name}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "name-product",
|
||||
header: () => <div className="text-center">Mahsulot nomi</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center font-medium">{row.original.product.name}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "name-product",
|
||||
header: () => <div className="text-center">Soni</div>,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center font-medium">{row.original.quantity}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <div className="text-center">Amallar</div>,
|
||||
cell: ({ row }) => {
|
||||
const district = row.original;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="w-full h-full hover:bg-gray-100">
|
||||
<EllipsisVertical className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center" className="flex flex-col gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex items-center gap-1 px-4 py-2 w-full text-left hover:bg-green-400 hover:text-white text-white bg-green-400"
|
||||
onClick={() => handleEdit(district)}
|
||||
>
|
||||
<Eye size={16} />
|
||||
<p>Batafsil</p>
|
||||
</Button>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
80
src/features/distributed/lib/data-table.tsx
Normal file
80
src/features/distributed/lib/data-table.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
|
||||
interface DataTableProps<MyDiscrictData, TValue> {
|
||||
columns: ColumnDef<MyDiscrictData, TValue>[];
|
||||
data: MyDiscrictData[];
|
||||
}
|
||||
|
||||
export function DataTableDistributed<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table className="">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="border-r">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="border-r">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Hech narsa yo'q
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
src/features/distributed/lib/data.ts
Normal file
24
src/features/distributed/lib/data.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export interface DistributedList {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: DistributedListData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface DistributedListData {
|
||||
id: number;
|
||||
product: {
|
||||
id: number;
|
||||
name: string;
|
||||
price: number;
|
||||
};
|
||||
quantity: number;
|
||||
employee_name: string;
|
||||
created_at: string;
|
||||
date: string;
|
||||
}
|
||||
208
src/features/distributed/ui/DistributedAddModal.tsx
Normal file
208
src/features/distributed/ui/DistributedAddModal.tsx
Normal file
@@ -0,0 +1,208 @@
|
||||
import { distributed_api } from "@/features/distributed/lib/api";
|
||||
import { order_api } from "@/features/specification/lib/api";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import { Calendar } from "@/shared/ui/calendar";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/shared/ui/command";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
import { Label } from "@/shared/ui/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { ChevronDownIcon, ChevronsUpDown } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export default function DistributedAddModal({ open, setOpen }: Props) {
|
||||
const [form, setForm] = useState({
|
||||
product_id: 0,
|
||||
date: "",
|
||||
employee_name: "",
|
||||
quantity: 0,
|
||||
});
|
||||
|
||||
const client = useQueryClient();
|
||||
|
||||
const { data: product } = useQuery({
|
||||
queryKey: ["product_list"],
|
||||
queryFn: () => order_api.product_list(),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const [openProduct, setOpenProduct] = useState(false);
|
||||
const [searchProduct, setSearchProduct] = useState("");
|
||||
const [openDate, setOpenData] = useState(false);
|
||||
|
||||
const selectedProduct = product?.find((x) => x.id === form.product_id);
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: () => distributed_api.create(form),
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries({ queryKey: ["distributed_list"] });
|
||||
setOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSubmit = () => {
|
||||
createMutation.mutate();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="space-y-4 max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Yangi topshirilgan mahsulot qo‘shish</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex flex-col gap-3 space-y-3">
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>Topshirilga mahsulot</Label>
|
||||
<Popover open={openProduct} onOpenChange={setOpenProduct}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={openProduct}
|
||||
className="w-full h-11 justify-between"
|
||||
>
|
||||
{selectedProduct ? selectedProduct.name : "Mahsulot tanlang"}
|
||||
<ChevronsUpDown className="ml-2 h-4 w-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className="w-[--radix-popover-trigger-width] p-0"
|
||||
align="start"
|
||||
>
|
||||
<Command shouldFilter={false}>
|
||||
<CommandInput
|
||||
placeholder="Qidirish..."
|
||||
className="h-9"
|
||||
value={searchProduct}
|
||||
onValueChange={setSearchProduct}
|
||||
/>
|
||||
|
||||
<CommandList>
|
||||
{product && product.length > 0 ? (
|
||||
<CommandGroup>
|
||||
{product
|
||||
.filter((p) =>
|
||||
p.name
|
||||
.toLowerCase()
|
||||
.includes(searchProduct.toLowerCase()),
|
||||
)
|
||||
.map((p) => (
|
||||
<CommandItem
|
||||
key={p.id}
|
||||
value={`${p.id}`}
|
||||
onSelect={() => {
|
||||
setForm({ ...form, product_id: p.id });
|
||||
setOpenProduct(false);
|
||||
}}
|
||||
>
|
||||
{p.name}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
) : (
|
||||
<CommandEmpty>Mahsulot topilmadi</CommandEmpty>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>Mahsulot soni</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Miqdori"
|
||||
value={form.quantity}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value;
|
||||
setForm({ ...form, quantity: val === "" ? 0 : Number(val) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label>Xaridorning ismi</Label>
|
||||
<Input
|
||||
placeholder="Xodim ismi"
|
||||
value={form.employee_name}
|
||||
onChange={(e) =>
|
||||
setForm({ ...form, employee_name: e.target.value })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
<Label htmlFor="date" className="px-1">
|
||||
Topshirilgan sana
|
||||
</Label>
|
||||
|
||||
<Popover open={openDate} onOpenChange={setOpenData}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
id="date"
|
||||
className="w-full h-12 justify-between font-normal"
|
||||
>
|
||||
{form.date ? form.date : "Sanani kiriting"}
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="w-auto overflow-hidden p-0"
|
||||
align="start"
|
||||
>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={new Date(form.date)}
|
||||
captionLayout="dropdown"
|
||||
onSelect={(date) => {
|
||||
if (date) {
|
||||
setForm({
|
||||
...form,
|
||||
date: formatDate.format(date, "YYYY-MM-DD"),
|
||||
});
|
||||
setOpenData(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={createMutation.isPending}
|
||||
className="h-12"
|
||||
>
|
||||
{createMutation.isPending ? "Saqlanmoqda..." : "Saqlash"}
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
82
src/features/distributed/ui/DistributedDetail.tsx
Normal file
82
src/features/distributed/ui/DistributedDetail.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { DistributedListData } from "@/features/distributed/lib/data";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { type Dispatch, type SetStateAction } from "react";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
||||
specification: DistributedListData | null;
|
||||
}
|
||||
|
||||
const DistributedDetail = ({ open, setOpen, specification }: Props) => {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="max-w-[90%] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader className="border-b pb-4">
|
||||
<DialogTitle className="text-2xl font-bold text-gray-800">
|
||||
Tafsilot
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 mt-6">
|
||||
{/* Asosiy ma'lumotlar - Grid */}
|
||||
<div className="grid grid-cols-1">
|
||||
{/* Xaridor */}
|
||||
<div className="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
|
||||
<p className="text-sm text-blue-600 font-medium mb-1">
|
||||
Xaridorning ismi
|
||||
</p>
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
{specification?.employee_name}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Foydalanuvchi */}
|
||||
|
||||
<div className="bg-gradient-to-br mt-5 from-green-50 to-green-100 rounded-lg p-4 border border-green-200 md:col-span-2">
|
||||
<p className="text-sm text-green-600 font-medium mb-1">
|
||||
Topshirilgan sanasi
|
||||
</p>
|
||||
<p className="text-lg font-semibold text-gray-800">
|
||||
{specification?.date}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dorilar ro'yxati */}
|
||||
<div className="bg-gray-50 rounded-lg p-5 border border-gray-200">
|
||||
<h3 className="text-lg font-bold text-gray-800 mb-4 flex items-center">
|
||||
Topshirilgan dori
|
||||
</h3>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="bg-white rounded-lg p-4 border border-gray-200 hover:border-indigo-300 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<p className="font-semibold text-gray-800">
|
||||
{specification?.product.name}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-gray-600">
|
||||
<span>
|
||||
Miqdor: <strong>{specification?.quantity} ta</strong>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistributedDetail;
|
||||
66
src/features/distributed/ui/DistributedList.tsx
Normal file
66
src/features/distributed/ui/DistributedList.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { distributed_api } from "@/features/distributed/lib/api";
|
||||
import { columnsDistributed } from "@/features/distributed/lib/column";
|
||||
import type { DistributedListData } from "@/features/distributed/lib/data";
|
||||
import { DataTableDistributed } from "@/features/distributed/lib/data-table";
|
||||
import DistributedAddModal from "@/features/distributed/ui/DistributedAddModal";
|
||||
import DistributedDetail from "@/features/distributed/ui/DistributedDetail";
|
||||
import AddedButton from "@/shared/ui/added-button";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const DistributedList = () => {
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["distributed_list"],
|
||||
queryFn: () => distributed_api.list(),
|
||||
});
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [added, setAdded] = useState<boolean>(false);
|
||||
const [detail, setDetail] = useState<DistributedListData | null>(null);
|
||||
|
||||
const handleEdit = (district: DistributedListData) => {
|
||||
setDetail(district);
|
||||
setOpen(true);
|
||||
};
|
||||
|
||||
const columns = columnsDistributed({
|
||||
handleEdit,
|
||||
});
|
||||
|
||||
return (
|
||||
<DashboardLayout link="/">
|
||||
<AddedButton onClick={() => setAdded(true)} />
|
||||
<>
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Topshirilgan mahsulotlar</h1>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-12 w-full rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
) : isError ? (
|
||||
<p className="text-red-500">
|
||||
Tumanlar yuklanmadi. Qayta urinib ko‘ring.
|
||||
</p>
|
||||
) : data ? (
|
||||
<div className="overflow-x-auto">
|
||||
<DataTableDistributed
|
||||
columns={columns}
|
||||
data={data.data.data.results}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">Tumanlar mavjud emas</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
<DistributedDetail open={open} setOpen={setOpen} specification={detail} />
|
||||
<DistributedAddModal open={added} setOpen={setAdded} />
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistributedList;
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Edit, EllipsisVertical, Trash } from "lucide-react";
|
||||
import { Edit, EllipsisVertical } from "lucide-react";
|
||||
|
||||
interface ColumnProps {
|
||||
handleEdit: (district: MyDiscrictData) => void;
|
||||
@@ -15,7 +15,6 @@ interface ColumnProps {
|
||||
|
||||
export const columnsDistrict = ({
|
||||
handleEdit,
|
||||
onDeleteClick,
|
||||
}: ColumnProps): ColumnDef<MyDiscrictData>[] => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
@@ -52,13 +51,13 @@ export const columnsDistrict = ({
|
||||
>
|
||||
<Edit size={16} /> Tahrirlash
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
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"
|
||||
onClick={() => onDeleteClick(district)} // faqat signal yuboradi
|
||||
onClick={() => onDeleteClick(district)}
|
||||
>
|
||||
<Trash size={16} /> O‘chirish
|
||||
</Button>
|
||||
</Button> */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/shared/ui/dropdown-menu";
|
||||
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";
|
||||
|
||||
interface Props {
|
||||
@@ -15,10 +15,7 @@ interface Props {
|
||||
onDeleteClick: (district: DoctorListData) => void;
|
||||
}
|
||||
|
||||
export const columns = ({
|
||||
router,
|
||||
onDeleteClick,
|
||||
}: Props): ColumnDef<DoctorListData>[] => [
|
||||
export const columns = ({ router }: Props): ColumnDef<DoctorListData>[] => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: () => <div className="text-center">№</div>,
|
||||
@@ -95,14 +92,14 @@ export const columns = ({
|
||||
>
|
||||
<Edit size={16} /> Tahrirlash
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant={"destructive"}
|
||||
size={"lg"}
|
||||
className="cursor-pointer"
|
||||
onClick={() => onDeleteClick(obj)}
|
||||
>
|
||||
<Trash size={16} /> {"O'chirish"}
|
||||
</Button>
|
||||
</Button> */}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
|
||||
@@ -11,19 +11,16 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { DashboardLayout } from "@/widgets/dashboard-layout/ui/DashboardLayout";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { doctor_api } from "../lib/api";
|
||||
import { columns } from "../lib/column";
|
||||
import { DataTable } from "../lib/data-table";
|
||||
|
||||
const Doctor = () => {
|
||||
const router = useNavigate();
|
||||
const queryClinent = useQueryClient();
|
||||
// const queryClinent = useQueryClient();
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: ["doctor_list"],
|
||||
queryFn: () => doctor_api.list(),
|
||||
@@ -32,41 +29,41 @@ const Doctor = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleted, isPending: deletedPending } = useMutation({
|
||||
mutationFn: ({ id }: { id: number }) => doctor_api.delete({ id }),
|
||||
onSuccess: () => {
|
||||
router("/physician");
|
||||
queryClinent.refetchQueries({ queryKey: ["doctor_list"] });
|
||||
setSelectedDistrict(null);
|
||||
setDeleteDialog(false);
|
||||
},
|
||||
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 { mutate: deleted, isPending: deletedPending } = useMutation({
|
||||
// mutationFn: ({ id }: { id: number }) => doctor_api.delete({ id }),
|
||||
// onSuccess: () => {
|
||||
// router("/physician");
|
||||
// queryClinent.refetchQueries({ queryKey: ["doctor_list"] });
|
||||
// setSelectedDistrict(null);
|
||||
// setDeleteDialog(false);
|
||||
// },
|
||||
// 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";
|
||||
// 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);
|
||||
},
|
||||
});
|
||||
// toast.error(message);
|
||||
// },
|
||||
// });
|
||||
|
||||
const [deleteDialog, setDeleteDialog] = useState<boolean>(false);
|
||||
const [selectedDistrict, setSelectedDistrict] =
|
||||
@@ -125,7 +122,7 @@ const Doctor = () => {
|
||||
<Button variant="secondary" onClick={() => setDeleteDialog(false)}>
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
variant="destructive"
|
||||
onClick={() =>
|
||||
selectedDistrict && deleted({ id: selectedDistrict.id })
|
||||
@@ -136,7 +133,7 @@ const Doctor = () => {
|
||||
) : (
|
||||
"O'chirish"
|
||||
)}
|
||||
</Button>
|
||||
</Button> */}
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
AlertCircle,
|
||||
Banknote,
|
||||
Calendar,
|
||||
FileText,
|
||||
@@ -279,6 +280,23 @@ export default function Home() {
|
||||
</Link>
|
||||
</>
|
||||
</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>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -21,8 +21,8 @@ export const plans_api = {
|
||||
return res;
|
||||
},
|
||||
|
||||
async planIsDone(id: number) {
|
||||
const res = await httpClient.post(`${PLANS}${id}/complite/`);
|
||||
async planIsDone({ id, body }: { id: number; body: { comment: string } }) {
|
||||
const res = await httpClient.post(`${PLANS}${id}/complite/`, body);
|
||||
return res;
|
||||
},
|
||||
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
export interface CreatePlansReq {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string; // '2025-11-26' shu kabi jonatish kera apiga
|
||||
is_done: boolean;
|
||||
date: string; // "2025-12-05";
|
||||
doctor_id: number | null;
|
||||
pharmacy_id: number | null;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: {
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetMyPlansRes {
|
||||
@@ -14,7 +21,12 @@ export interface GetMyPlansRes {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
is_done: boolean;
|
||||
comment: string | null;
|
||||
doctor: null;
|
||||
pharmacy: null;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: null;
|
||||
created_at: string;
|
||||
}[];
|
||||
}
|
||||
@@ -24,6 +36,11 @@ export interface Task {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
is_done: boolean;
|
||||
comment: string | null;
|
||||
doctor: null;
|
||||
pharmacy: null;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { AxiosError } from "axios";
|
||||
import clsx from "clsx";
|
||||
import { format } from "date-fns";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { plans_api } from "../lib/api";
|
||||
|
||||
@@ -17,7 +19,12 @@ interface Task {
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
is_done: boolean;
|
||||
comment: string | null;
|
||||
doctor: null;
|
||||
pharmacy: null;
|
||||
longitude: number;
|
||||
latitude: number;
|
||||
extra_location: null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
@@ -33,11 +40,17 @@ export function PlanDetailsDialog({
|
||||
task,
|
||||
}: PlanDetailsDialogProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const [commentOpen, setCommnetOpen] = useState<boolean>(false);
|
||||
const [commentText, setCommnetText] = useState<string>("");
|
||||
|
||||
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: () => {
|
||||
toast.success("Rejangiz bajarildi bo'lib o'zgardi");
|
||||
onOpenChange(false);
|
||||
setCommnetOpen(false);
|
||||
setCommnetText("");
|
||||
queryClient.refetchQueries({ queryKey: ["my_plans"] });
|
||||
},
|
||||
onError: (error: AxiosError) => {
|
||||
@@ -75,74 +88,105 @@ export function PlanDetailsDialog({
|
||||
<DialogContent className="w-[95%] max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl font-bold">
|
||||
{"Rejani ko'rish"}
|
||||
{commentOpen ? "Reja qanday bajarildi ma'lumot" : "Rejani ko'rish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-4">
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
{!commentOpen && (
|
||||
<>
|
||||
{/* Status */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Xolati</p>
|
||||
<p
|
||||
className={clsx(
|
||||
"font-semibold",
|
||||
task.comment ? "text-green-500" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{task.comment ? "Tugallangan ✓" : "Bajarilmagan"}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Sarlavha</p>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-lg font-semibold",
|
||||
task.comment ? "text-green-500" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Tavsifi</p>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-base leading-relaxed",
|
||||
task.comment ? "text-green-500" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{task.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Sana</p>
|
||||
<p className="text-base font-medium text-foreground">
|
||||
{format(task.date, "dd.MM.yyyy")}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{commentOpen && (
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Xolati</p>
|
||||
<p
|
||||
className={clsx(
|
||||
"font-semibold",
|
||||
task.is_done ? "text-green-500" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{task.is_done ? "Tugallangan ✓" : "Bajarilmagan"}
|
||||
</p>
|
||||
<Textarea
|
||||
className="min-h-32 max-h-52"
|
||||
placeholder="Reja qanday bajarildi yozing"
|
||||
value={commentText}
|
||||
onChange={(e) => setCommnetText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Sarlavha</p>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-lg font-semibold break-words",
|
||||
task.is_done ? "text-green-500" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{task.title}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Tavsifi</p>
|
||||
<p
|
||||
className={clsx(
|
||||
"text-base break-words leading-relaxed",
|
||||
task.is_done ? "text-green-500" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{task.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Date */}
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm text-muted-foreground">Sana</p>
|
||||
<p className="text-base font-medium text-foreground">
|
||||
{format(task.date, "dd.MM.yyyy")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
)}
|
||||
{/* Close Button */}
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<Button
|
||||
onClick={() => {
|
||||
mutate(task.id);
|
||||
}}
|
||||
disabled={task.is_done || isPending}
|
||||
className="w-fit p-5 rounded-lg"
|
||||
>
|
||||
Bajarildi
|
||||
</Button>
|
||||
{commentOpen ? (
|
||||
<Button
|
||||
disabled={
|
||||
(task.comment && task.comment.length > 0) || 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"
|
||||
>
|
||||
Bajarildi
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
disabled={isPending}
|
||||
onClick={() => onOpenChange(false)}
|
||||
onClick={() => {
|
||||
onOpenChange(false);
|
||||
setCommnetOpen(false);
|
||||
setCommnetText("");
|
||||
}}
|
||||
variant="outline"
|
||||
className="p-5 rounded-lg"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"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 AddedButton from "@/shared/ui/added-button";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
@@ -22,10 +26,9 @@ import {
|
||||
} from "@/shared/ui/form";
|
||||
import { Input } from "@/shared/ui/input";
|
||||
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 { format } from "date-fns";
|
||||
import { CalendarIcon, Loader2 } from "lucide-react";
|
||||
import { CalendarIcon, Loader2, MapPin } from "lucide-react";
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -38,15 +41,6 @@ const plansForm = z.object({
|
||||
date: z.string().optional(),
|
||||
});
|
||||
|
||||
interface Task {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string;
|
||||
date: string;
|
||||
is_done: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
isDialogOpen: boolean;
|
||||
setIsDialogOpen: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -62,11 +56,17 @@ export const AddPlans = ({
|
||||
isDialogOpen,
|
||||
setIsDialogOpen,
|
||||
newTask,
|
||||
setNewTask,
|
||||
setTaskEdit,
|
||||
setNewTask,
|
||||
taskEdit,
|
||||
}: Props) => {
|
||||
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 form = useForm<z.infer<typeof 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({
|
||||
mutationFn: (body: CreatePlansReq) => plans_api.createPlans(body),
|
||||
onSuccess: () => {
|
||||
@@ -158,127 +172,288 @@ export const AddPlans = ({
|
||||
}
|
||||
}, [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>) {
|
||||
if (taskEdit) {
|
||||
edit({
|
||||
body: {
|
||||
title: values.title,
|
||||
date: values.date ? formatDate.format(values.date, "YYYY-MM-DD") : "",
|
||||
description: values.description,
|
||||
date: formatDate.format(values.date!, "YYYY-MM-DD"),
|
||||
is_done: taskEdit.is_done,
|
||||
doctor_id: doctorId ? doctorId.id : null,
|
||||
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,
|
||||
});
|
||||
} else {
|
||||
added({
|
||||
title: values.title,
|
||||
date: values.date ? formatDate.format(values.date, "YYYY-MM-DD") : "",
|
||||
description: values.description,
|
||||
date: formatDate.format(values.date!, "YYYY-MM-DD"),
|
||||
is_done: false,
|
||||
doctor_id: doctorId ? doctorId.id : null,
|
||||
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 (
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<AddedButton onClick={() => setTaskEdit(null)} />
|
||||
</DialogTrigger>
|
||||
<>
|
||||
<Dialog open={showMainModal} onOpenChange={setShowMainModal}>
|
||||
<DialogTrigger asChild>
|
||||
<AddedButton
|
||||
onClick={() => {
|
||||
setShowMainModal(true);
|
||||
setTaskEdit(null);
|
||||
}}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{taskEdit ? "Vazifani tahrirlash" : "Yangi vazifa qo‘shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sarlavha</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Sarlavha" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tavsif</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tavsif" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>Sana</FormLabel>
|
||||
<Dialog
|
||||
open={isDateDialogOpen}
|
||||
onOpenChange={setIsDateDialogOpen}
|
||||
<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"
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-full justify-start">
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{newTask.date
|
||||
? format(newTask.date, "dd-MM-yyyy")
|
||||
: "Sanani tanlang"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
{typeLabel(type)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<DialogContent className="w-auto p-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={newTask.date}
|
||||
onSelect={(date) => {
|
||||
if (date) {
|
||||
setNewTask({ ...newTask, date });
|
||||
form.setValue("date", date.toISOString());
|
||||
<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);
|
||||
}
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button onClick={() => setIsDateDialogOpen(false)}>
|
||||
Tanlash
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</FormItem>
|
||||
>
|
||||
<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>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isPending || editPending}
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending || editPending}>
|
||||
{isPending || editPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : taskEdit ? (
|
||||
"Saqlash"
|
||||
) : (
|
||||
"Qo‘shish"
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{taskEdit ? "Vazifani tahrirlash" : "Yangi vazifa qo‘shish"}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Sarlavha</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Sarlavha" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Tavsif</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Tavsif" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>Sana</FormLabel>
|
||||
<Dialog
|
||||
open={isDateDialogOpen}
|
||||
onOpenChange={setIsDateDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="w-full justify-start">
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{newTask.date
|
||||
? formatDate.format(newTask.date, "DD-MM-YYYY")
|
||||
: "Sanani tanlang"}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
||||
<DialogContent className="w-auto p-4">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={newTask.date}
|
||||
onSelect={(date) => {
|
||||
if (date) {
|
||||
setNewTask({ ...newTask, date });
|
||||
form.setValue("date", date.toISOString());
|
||||
}
|
||||
}}
|
||||
initialFocus
|
||||
/>
|
||||
<div className="mt-4 flex justify-end">
|
||||
<Button onClick={() => setIsDateDialogOpen(false)}>
|
||||
Tanlash
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</FormItem>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={isPending || editPending}
|
||||
onClick={() => setIsDialogOpen(false)}
|
||||
>
|
||||
Bekor qilish
|
||||
</Button>
|
||||
<Button type="submit" disabled={isPending || editPending}>
|
||||
{isPending || editPending ? (
|
||||
<Loader2 className="animate-spin" />
|
||||
) : taskEdit ? (
|
||||
"Saqlash"
|
||||
) : (
|
||||
"Qo‘shish"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,13 +21,7 @@ import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
CalendarIcon,
|
||||
Loader2,
|
||||
Pencil,
|
||||
Trash,
|
||||
TriangleAlert,
|
||||
} from "lucide-react";
|
||||
import { CalendarIcon, Loader2, Pencil, TriangleAlert } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -96,10 +90,10 @@ export default function Plans() {
|
||||
setIsPlanDetailsOpen(true);
|
||||
};
|
||||
|
||||
const handleDeleteTask = (task: Task) => {
|
||||
setSelectedTask(task);
|
||||
setDeleteDialogOpen(true);
|
||||
};
|
||||
// const handleDeleteTask = (task: Task) => {
|
||||
// setSelectedTask(task);
|
||||
// setDeleteDialogOpen(true);
|
||||
// };
|
||||
|
||||
const grouped = groupByDate(data?.data.data || []);
|
||||
|
||||
@@ -212,7 +206,7 @@ export default function Plans() {
|
||||
<h3
|
||||
className={clsx(
|
||||
"font-semibold wrap-break-word",
|
||||
item.is_done ? "text-green-500" : "text-foreground",
|
||||
item.comment ? "text-green-500" : "text-foreground",
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
@@ -220,7 +214,7 @@ export default function Plans() {
|
||||
<p
|
||||
className={clsx(
|
||||
"text-sm wrap-break-word",
|
||||
item.is_done
|
||||
item.comment
|
||||
? "text-green-500"
|
||||
: "text-muted-foreground",
|
||||
)}
|
||||
@@ -230,7 +224,7 @@ export default function Plans() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<div className="grid grid-cols-1 gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@@ -240,12 +234,12 @@ export default function Plans() {
|
||||
setIsAddDialogOpen(true);
|
||||
}}
|
||||
className="p-4"
|
||||
disabled={item.is_done}
|
||||
disabled={item.comment ? true : false}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
<p>Tahrirlash</p>
|
||||
</Button>
|
||||
<Button
|
||||
{/* <Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={(e) => {
|
||||
@@ -257,7 +251,7 @@ export default function Plans() {
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
<p>{"O'chirish"}</p>
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
21
src/features/support/lib/api.ts
Normal file
21
src/features/support/lib/api.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import type { SupportListRes } from "@/features/support/lib/data";
|
||||
import httpClient from "@/shared/config/api/httpClient";
|
||||
import { SUPPORT } from "@/shared/config/api/URLs";
|
||||
import type { AxiosResponse } from "axios";
|
||||
|
||||
export const support_api = {
|
||||
async list(): Promise<AxiosResponse<SupportListRes>> {
|
||||
const res = await httpClient.get(`${SUPPORT}list/`);
|
||||
return res;
|
||||
},
|
||||
|
||||
async send(body: {
|
||||
district_id?: number;
|
||||
problem: string;
|
||||
date: string;
|
||||
type: "PROBLEM" | "HELP";
|
||||
}) {
|
||||
const res = await httpClient.post(`${SUPPORT}send/`, body);
|
||||
return res;
|
||||
},
|
||||
};
|
||||
32
src/features/support/lib/column.tsx
Normal file
32
src/features/support/lib/column.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { SupportListData } from "@/features/support/lib/data";
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
export const columnsSupport = (): ColumnDef<SupportListData>[] => [
|
||||
{
|
||||
accessorKey: "id",
|
||||
header: () => <div className="text-center">№</div>,
|
||||
cell: ({ row }) => {
|
||||
return <div className="text-center font-medium">{row.index + 1}</div>;
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: () => <div className="text-center">Xabar tavsifi</div>,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center font-medium">{row.original.problem}</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "districtName",
|
||||
header: () => <div className="text-center">Tuman</div>,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center font-medium">
|
||||
{row.original.district ? row.original.district.name : "-"}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
80
src/features/support/lib/data-table.tsx
Normal file
80
src/features/support/lib/data-table.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
type ColumnDef,
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/shared/ui/table";
|
||||
|
||||
interface DataTableProps<ObjectAllData, TValue> {
|
||||
columns: ColumnDef<ObjectAllData, TValue>[];
|
||||
data: ObjectAllData[];
|
||||
}
|
||||
|
||||
export function DataTableSupport<ObjectAllData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
}: DataTableProps<ObjectAllData, TValue>) {
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table className="">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id} className="border-r">
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="border-r">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Yordam so'rovlari mavjud emas
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
23
src/features/support/lib/data.ts
Normal file
23
src/features/support/lib/data.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export interface SupportListRes {
|
||||
status_code: number;
|
||||
status: string;
|
||||
message: string;
|
||||
data: {
|
||||
count: number;
|
||||
next: null | string;
|
||||
previous: null | string;
|
||||
results: SupportListData[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface SupportListData {
|
||||
id: number;
|
||||
problem: string;
|
||||
date: string;
|
||||
type: string;
|
||||
district: {
|
||||
id: number;
|
||||
name: string;
|
||||
} | null;
|
||||
created_at: string;
|
||||
}
|
||||
217
src/features/support/ui/SendSupport.tsx
Normal file
217
src/features/support/ui/SendSupport.tsx
Normal file
@@ -0,0 +1,217 @@
|
||||
import { district_api } from "@/features/district/lib/api";
|
||||
import { support_api } from "@/features/support/lib/api";
|
||||
import formatDate from "@/shared/lib/formatDate";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { Textarea } from "@/shared/ui/textarea";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { AxiosError } from "axios";
|
||||
import { Loader2 } from "lucide-react";
|
||||
import { useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface Props {
|
||||
showMainModal: boolean;
|
||||
setShowMainModal: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
const SendSupport = ({ setShowMainModal, showMainModal }: Props) => {
|
||||
const [supportType, setSupportType] = useState<"PROBLEM" | "HELP">("HELP");
|
||||
const [districtModal, setDistrictModal] = useState<boolean>(false);
|
||||
const [districtType, setDistrictType] = useState<number | null>(null);
|
||||
const [commentModal, setCommentModal] = useState<boolean>(false);
|
||||
const [comment, setComment] = useState<string>("");
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ["my_disctrict"],
|
||||
queryFn: () => district_api.getDiscrict(),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const queryClinent = useQueryClient();
|
||||
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: (body: {
|
||||
district_id?: number;
|
||||
problem: string;
|
||||
date: string;
|
||||
type: "PROBLEM" | "HELP";
|
||||
}) => support_api.send(body),
|
||||
onSuccess: () => {
|
||||
toast.success("Sizning so'rovingiz yuborildi");
|
||||
queryClinent.refetchQueries({ queryKey: ["support_list"] });
|
||||
setCommentModal(false);
|
||||
setDistrictModal(false);
|
||||
setShowMainModal(false);
|
||||
setComment("");
|
||||
setDistrictType(null);
|
||||
},
|
||||
onError: (error: AxiosError) => {
|
||||
const data = error.response?.data as { message?: string };
|
||||
const errorData = error.response?.data as {
|
||||
messages?: {
|
||||
token_class: string;
|
||||
token_type: string;
|
||||
message: string;
|
||||
}[];
|
||||
};
|
||||
const errorName = error.response?.data as {
|
||||
data?: {
|
||||
name: string[];
|
||||
};
|
||||
};
|
||||
|
||||
const message =
|
||||
Array.isArray(errorName.data?.name) && errorName.data.name.length
|
||||
? errorName.data.name[0]
|
||||
: data?.message ||
|
||||
(Array.isArray(errorData?.messages) && errorData.messages.length
|
||||
? errorData.messages[0].message
|
||||
: undefined) ||
|
||||
"Xatolik yuz berdi";
|
||||
|
||||
toast.error(message);
|
||||
},
|
||||
});
|
||||
|
||||
const sendSupport = () => {
|
||||
let body;
|
||||
|
||||
if (districtType === null) {
|
||||
body = {
|
||||
date: formatDate.format(new Date(), "YYYY-MM-DD"),
|
||||
problem: comment,
|
||||
type: supportType,
|
||||
};
|
||||
} else {
|
||||
body = {
|
||||
date: formatDate.format(new Date(), "YYYY-MM-DD"),
|
||||
district_id: districtType,
|
||||
problem: comment,
|
||||
type: supportType,
|
||||
};
|
||||
}
|
||||
mutate(body);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={showMainModal} onOpenChange={setShowMainModal}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">
|
||||
Sizga qanday yordam kerak
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 mt-4">
|
||||
<Button
|
||||
className="w-full h-12 text-lg"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSupportType("HELP");
|
||||
if (data && data.length > 0) {
|
||||
setDistrictModal(true);
|
||||
} else {
|
||||
setCommentModal(true);
|
||||
}
|
||||
setShowMainModal(false);
|
||||
}}
|
||||
>
|
||||
Yordam so'rash
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full h-12 text-lg"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setSupportType("PROBLEM");
|
||||
setShowMainModal(false);
|
||||
if (data && data.length > 0) {
|
||||
setDistrictModal(true);
|
||||
} else {
|
||||
setCommentModal(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Muammoni hal qilish
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={districtModal} onOpenChange={setDistrictModal}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">Tummani tanlang</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-3 mt-4">
|
||||
{data &&
|
||||
data.length > 0 &&
|
||||
data.map((e) => (
|
||||
<Button
|
||||
key={e.id}
|
||||
className="w-full h-12 text-lg"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setDistrictType(e.id);
|
||||
setDistrictModal(false);
|
||||
setCommentModal(true);
|
||||
}}
|
||||
>
|
||||
{e.name}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
<DialogFooter className="flex justify-end items-end mt-5">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setDistrictType(null);
|
||||
setDistrictModal(false);
|
||||
setCommentModal(true);
|
||||
}}
|
||||
className="w-fit h-12"
|
||||
>
|
||||
O'tkazib yuborish
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={commentModal} onOpenChange={setCommentModal}>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-2xl">
|
||||
Yordam uchun izoh qoldiring
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<Textarea
|
||||
className="min-h-32 max-h-52"
|
||||
placeholder="Izoh"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<DialogFooter className="flex justify-end items-end mt-5">
|
||||
<Button
|
||||
onClick={() => sendSupport()}
|
||||
disabled={isPending}
|
||||
className="w-fit h-12"
|
||||
>
|
||||
{isPending ? <Loader2 className="animate-spin" /> : "Jo'natish"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SendSupport;
|
||||
71
src/features/support/ui/SupportList.tsx
Normal file
71
src/features/support/ui/SupportList.tsx
Normal file
@@ -0,0 +1,71 @@
|
||||
import { support_api } from "@/features/support/lib/api";
|
||||
import { columnsSupport } from "@/features/support/lib/column";
|
||||
import { DataTableSupport } from "@/features/support/lib/data-table";
|
||||
import SendSupport from "@/features/support/ui/SendSupport";
|
||||
import AddedButton from "@/shared/ui/added-button";
|
||||
import { Skeleton } from "@/shared/ui/skeleton";
|
||||
import { DashboardLayout } from "@/widgets/dashboard-layout/ui";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
const SupportList = () => {
|
||||
const [showMainModal, setShowMainModal] = useState<boolean>(false);
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
queryKey: ["support_list"],
|
||||
queryFn: () => support_api.list(),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const columns = columnsSupport();
|
||||
|
||||
return (
|
||||
<DashboardLayout link="/">
|
||||
<AddedButton onClick={() => setShowMainModal(true)} />
|
||||
<div className="space-y-6">
|
||||
<h1 className="text-3xl font-bold">Yordam so'rovlari</h1>
|
||||
|
||||
{isLoading && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<span className="text-gray-500">Yuklanmoqda...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isError && (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<span className="text-red-500">
|
||||
Xatolik yuz berdi. Iltimos, qayta urinib ko‘ring.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Skeleton key={i} className="h-12 w-full rounded-md" />
|
||||
))}
|
||||
</div>
|
||||
) : isError ? (
|
||||
<p className="text-red-500">
|
||||
So'rovlar yuklanmadi. Qayta urinib ko‘ring.
|
||||
</p>
|
||||
) : data ? (
|
||||
<div className="overflow-x-auto">
|
||||
<DataTableSupport columns={columns} data={data.results} />
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-gray-500">Yordam so'rovlari mavjud emas</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SendSupport
|
||||
showMainModal={showMainModal}
|
||||
setShowMainModal={setShowMainModal}
|
||||
/>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default SupportList;
|
||||
12
src/pages/distributed-product/DistributedProduct.tsx
Normal file
12
src/pages/distributed-product/DistributedProduct.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import DistributedList from "@/features/distributed/ui/DistributedList";
|
||||
import TokenLayout from "@/token-layaout";
|
||||
|
||||
const DistributedProduct = () => {
|
||||
return (
|
||||
<TokenLayout>
|
||||
<DistributedList />
|
||||
</TokenLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistributedProduct;
|
||||
12
src/pages/support/index.tsx
Normal file
12
src/pages/support/index.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import SupportList from "@/features/support/ui/SupportList";
|
||||
import TokenLayout from "@/token-layaout";
|
||||
|
||||
const Support = () => {
|
||||
return (
|
||||
<TokenLayout>
|
||||
<SupportList />
|
||||
</TokenLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Support;
|
||||
@@ -1,4 +1,5 @@
|
||||
import Home from "@/features/home/ui/Home";
|
||||
import DistributedProduct from "@/pages/distributed-product/DistributedProduct";
|
||||
import DistrictPage from "@/pages/district/page";
|
||||
import Location from "@/pages/location/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 SpecificationDetail from "@/pages/specification/history/[id]/page";
|
||||
import Specification from "@/pages/specification/page";
|
||||
import Support from "@/pages/support";
|
||||
import TourPlanPage from "@/pages/tour-plan/page";
|
||||
import { TypePlan } from "@/pages/type-plan/page";
|
||||
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;
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
const BASE_URL =
|
||||
process.env.NEXT_PUBLIC_API_URL || "https://api.meridynpharma.com";
|
||||
|
||||
const CREATE_USER = "/api/v1/accounts/user/create";
|
||||
const LOGIN_USER = "/api/v1/authentication/login/";
|
||||
const REGIONS = "/api/v1/shared/region/list/";
|
||||
const PLANS = "/api/v1/shared/plan/";
|
||||
const DISCTRICT = "/api/v1/shared/disctrict/";
|
||||
const OBJECT = "/api/v1/shared/place/";
|
||||
const DOCTOR = "/api/v1/shared/doctor/";
|
||||
const PHARMACY = "/api/v1/shared/pharmacy/";
|
||||
const LOCATION = "/api/v1/shared/location/";
|
||||
const TOUR_PLAN = "/api/v1/shared/tour_plan/";
|
||||
const FACTORY = "/api/v1/shared/factory/list/";
|
||||
const PRODUCT = "/api/v1/orders/product/list/";
|
||||
const ORDER = "/api/v1/orders/order/";
|
||||
const API_V = "/api/v1";
|
||||
|
||||
const CREATE_USER = `${API_V}/accounts/user/create`;
|
||||
const LOGIN_USER = `${API_V}/authentication/login/`;
|
||||
const REGIONS = `${API_V}/shared/region/list/`;
|
||||
const PLANS = `${API_V}/shared/plan/`;
|
||||
const DISCTRICT = `${API_V}/shared/disctrict/`;
|
||||
const OBJECT = `${API_V}/shared/place/`;
|
||||
const DOCTOR = `${API_V}/shared/doctor/`;
|
||||
const PHARMACY = `${API_V}/shared/pharmacy/`;
|
||||
const LOCATION = `${API_V}/shared/location/`;
|
||||
const TOUR_PLAN = `${API_V}/shared/tour_plan/`;
|
||||
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 {
|
||||
BASE_URL,
|
||||
CREATE_USER,
|
||||
DISCTRICT,
|
||||
DISTRIBUTED_CREATE,
|
||||
DISTRIBUTED_LIST,
|
||||
DOCTOR,
|
||||
FACTORY,
|
||||
LOCATION,
|
||||
@@ -29,5 +36,6 @@ export {
|
||||
PLANS,
|
||||
PRODUCT,
|
||||
REGIONS,
|
||||
SUPPORT,
|
||||
TOUR_PLAN,
|
||||
};
|
||||
|
||||
184
src/shared/ui/command.tsx
Normal file
184
src/shared/ui/command.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { SearchIcon } from "lucide-react";
|
||||
|
||||
import { cn } from "@/shared/lib/utils";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-9 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<SearchIcon className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -17,7 +17,7 @@ export default defineConfig({
|
||||
port: 5174,
|
||||
host: true, // barcha hostlarga ruxsat
|
||||
allowedHosts: [
|
||||
"humanitarian-monroe-theories-philadelphia.trycloudflare.com", // ngrok host qo'shildi
|
||||
"battery-behavior-forgot-recovery.trycloudflare.com", // ngrok host qo'shildi
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user