diff --git a/.env b/.env index 6d7cbca..ebaf5ff 100644 --- a/.env +++ b/.env @@ -1 +1,2 @@ -VITE_API_URL=https://jsonplaceholder.typicode.com \ No newline at end of file +VITE_API_URL=https://api.meridynpharma.com +VITE_SOCKET_URL=wss://api.meridynpharma.com \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 1e415ff..b36b4c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,10 +24,12 @@ "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", "i18next": "^25.5.2", "i18next-browser-languagedetector": "^8.2.0", + "js-cookie": "^3.0.5", "lucide-react": "^0.544.0", "next-themes": "^0.4.6", "react": "^19.1.1", @@ -37,6 +39,7 @@ "react-hook-form": "^7.66.1", "react-i18next": "^15.7.3", "react-router-dom": "^7.9.6", + "socket.io-client": "^4.8.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.13", @@ -44,6 +47,7 @@ }, "devDependencies": { "@eslint/js": "^9.25.0", + "@types/js-cookie": "^3.0.6", "@types/node": "^22.15.21", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", @@ -2332,6 +2336,12 @@ "win32" ] }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", @@ -2656,6 +2666,13 @@ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "dev": true }, + "node_modules/@types/js-cookie": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz", + "integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3259,6 +3276,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", @@ -3434,6 +3467,45 @@ "integrity": "sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==", "dev": true }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", @@ -4224,6 +4296,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/js-cookie": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", + "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4725,8 +4806,7 @@ "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/nano-spawn": { "version": "1.0.3", @@ -5379,6 +5459,68 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -5947,6 +6089,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 6008684..cd2237d 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,12 @@ "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", "i18next": "^25.5.2", "i18next-browser-languagedetector": "^8.2.0", + "js-cookie": "^3.0.5", "lucide-react": "^0.544.0", "next-themes": "^0.4.6", "react": "^19.1.1", @@ -41,6 +43,7 @@ "react-hook-form": "^7.66.1", "react-i18next": "^15.7.3", "react-router-dom": "^7.9.6", + "socket.io-client": "^4.8.1", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.13", @@ -48,6 +51,7 @@ }, "devDependencies": { "@eslint/js": "^9.25.0", + "@types/js-cookie": "^3.0.6", "@types/node": "^22.15.21", "@types/react": "^19.1.2", "@types/react-dom": "^19.1.2", diff --git a/src/App.tsx b/src/App.tsx index 87d7c56..21ab4f2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,7 @@ const App = () => { return ( - + ); }; diff --git a/src/LoginLayout.tsx b/src/LoginLayout.tsx new file mode 100644 index 0000000..6df5dcb --- /dev/null +++ b/src/LoginLayout.tsx @@ -0,0 +1,20 @@ +import { getToken } from "@/shared/lib/cookie"; +import { useEffect, type ReactNode } from "react"; +import { useNavigate } from "react-router-dom"; + +const LoginLayout = ({ children }: { children: ReactNode }) => { + const token = getToken(); + const navigate = useNavigate(); + + useEffect(() => { + if (!token) { + navigate("/"); + } else if (token) { + navigate("/dashboard"); + } + }, [token, navigate]); + + return children; +}; + +export default LoginLayout; diff --git a/src/features/auth/lib/api.ts b/src/features/auth/lib/api.ts new file mode 100644 index 0000000..904fd92 --- /dev/null +++ b/src/features/auth/lib/api.ts @@ -0,0 +1,22 @@ +import httpClient from "@/shared/config/api/httpClient"; +import { LOGIN } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +interface LoginRes { + status_code: number; + status: string; + message: string; + data: { + token: string; + }; +} + +export const auth_pai = { + async login(body: { + username: string; + password: string; + }): Promise> { + const res = await httpClient.post(LOGIN, body); + return res; + }, +}; diff --git a/src/features/auth/ui/AuthLogin.tsx b/src/features/auth/ui/AuthLogin.tsx new file mode 100644 index 0000000..7eadd3f --- /dev/null +++ b/src/features/auth/ui/AuthLogin.tsx @@ -0,0 +1,160 @@ +import { auth_pai } from "@/features/auth/lib/api"; +import { saveToken } from "@/shared/lib/cookie"; +import { Button } from "@/shared/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/shared/ui/card"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/shared/ui/form"; +import { Input } from "@/shared/ui/input"; +import { Label } from "@/shared/ui/label"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Eye, EyeOff, Loader2 } from "lucide-react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; +import { z } from "zod"; + +const AuthLogin = () => { + const [showPassword, setShowPassword] = useState(false); + const navigate = useNavigate(); + + const { mutate, isPending } = useMutation({ + mutationFn: (body: { username: string; password: string }) => + auth_pai.login(body), + onSuccess: (res) => { + toast.success(res.data.message, { + richColors: true, + position: "top-center", + }); + + saveToken(res.data.data.token); + navigate("dashboard"); + }, + onError: (err: AxiosError) => { + console.log(err); + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + const FormSchema = z.object({ + email: z.string().min(1, "Login kiriting"), + password: z.string().min(1, "Parolni kiriting"), + }); + + const form = useForm>({ + resolver: zodResolver(FormSchema), + defaultValues: { + email: "", + password: "", + }, + }); + + const handleSubmit = async (values: z.infer) => { + mutate({ + password: values.password, + username: values.email, + }); + }; + return ( +
+ + + + Admin panelga kirish + + + Login va parolingizni kiriting. + + + + +
+ + ( + + + + + + + + )} + /> + + ( + + + +
+ + +
+
+ +
+ )} + /> + + + + +
+
+
+ ); +}; + +export default AuthLogin; diff --git a/src/features/districts/lib/api.ts b/src/features/districts/lib/api.ts new file mode 100644 index 0000000..0b79c81 --- /dev/null +++ b/src/features/districts/lib/api.ts @@ -0,0 +1,42 @@ +import type { DistrictListRes } from "@/features/districts/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { DISTRICT } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const discrit_api = { + async list(params: { + limit?: number; + offset?: number; + name?: string; + }): Promise> { + const res = await httpClient.get(`${DISTRICT}list/`, { params }); + return res; + }, + + async create(body: { + name: string; + user_id: number; + }): Promise> { + const res = await httpClient.post(`${DISTRICT}create/`, body); + return res; + }, + + async update({ + body, + id, + }: { + id: number; + body: { + name: string; + user: number; + }; + }): Promise> { + const res = await httpClient.patch(`${DISTRICT}${id}/update/`, body); + return res; + }, + + async delete(id: number): Promise> { + const res = await httpClient.delete(`${DISTRICT}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/districts/lib/data.ts b/src/features/districts/lib/data.ts index edf7c5e..29203a6 100644 --- a/src/features/districts/lib/data.ts +++ b/src/features/districts/lib/data.ts @@ -23,3 +23,26 @@ export const fakeDistrict: District[] = [ user: FakeUserList[2], }, ]; + +export interface DistrictListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: string | null; + previous: string | null; + results: DistrictListData[]; + }; +} + +export interface DistrictListData { + id: number; + name: string; + user: { + id: number; + first_name: string; + last_name: string; + }; + created_at: string; +} diff --git a/src/features/districts/ui/AddDistrict.tsx b/src/features/districts/ui/AddDistrict.tsx index 2a73b9e..da36eaf 100644 --- a/src/features/districts/ui/AddDistrict.tsx +++ b/src/features/districts/ui/AddDistrict.tsx @@ -1,7 +1,17 @@ -import type { District } from "@/features/districts/lib/data"; +import { discrit_api } from "@/features/districts/lib/api"; +import type { DistrictListData } from "@/features/districts/lib/data"; import { addDistrict } from "@/features/districts/lib/form"; -import { FakeUserList } from "@/features/users/lib/data"; +import { user_api } from "@/features/users/lib/api"; +import { cn } from "@/shared/lib/utils"; import { Button } from "@/shared/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/shared/ui/command"; import { Form, FormControl, @@ -11,33 +21,77 @@ import { } from "@/shared/ui/form"; import { Input } from "@/shared/ui/input"; import { Label } from "@/shared/ui/label"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Loader2 } from "lucide-react"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Check, ChevronsUpDown, Loader2 } from "lucide-react"; import { useState, type Dispatch, type SetStateAction } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import z from "zod"; type FormValues = z.infer; interface Props { - initialValues: District | null; - setDistricts: Dispatch>; + initialValues: DistrictListData | null; setDialogOpen: Dispatch>; } -export default function AddDistrict({ - initialValues, - setDistricts, - setDialogOpen, -}: Props) { - const [load, setLoad] = useState(false); +export default function AddDistrict({ initialValues, setDialogOpen }: Props) { + const [openUser, setOpenUser] = useState(false); + const [userSearch, setUserSearch] = useState(""); + const queryClient = useQueryClient(); + + const { data: user, isLoading: isUserLoading } = useQuery({ + queryKey: ["user_list", userSearch], + queryFn: () => user_api.list({ search: userSearch }), + select(data) { + return data.data.data.results; + }, + }); + + const { mutate, isPending } = useMutation({ + mutationFn: (body: { name: string; user_id: number }) => + discrit_api.create(body), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["discrit_list"] }); + toast.success(`Tuman qo'shildi`); + setDialogOpen(false); + }, + onError: (err: AxiosError) => { + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + const { mutate: update, isPending: updatePending } = useMutation({ + mutationFn: ({ + body, + id, + }: { + id: number; + body: { name: string; user: number }; + }) => discrit_api.update({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["discrit_list"] }); + toast.success(`Tuman qo'shildi`); + setDialogOpen(false); + }, + onError: (err: AxiosError) => { + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + const form = useForm({ resolver: zodResolver(addDistrict), defaultValues: { @@ -47,41 +101,16 @@ export default function AddDistrict({ }); function onSubmit(values: FormValues) { - const selectedUser = FakeUserList.find( - (u) => u.id === Number(values.userId), - ); - - if (!selectedUser) return; - setLoad(true); if (initialValues) { - setTimeout(() => { - setDistricts((prev) => - prev.map((d) => - d.id === initialValues.id - ? { - ...d, - name: values.name, - user: selectedUser, - } - : d, - ), - ); - setDialogOpen(false); - setLoad(false); - }, 2000); + update({ + id: initialValues.id, + body: { + name: values.name, + user: Number(values.userId), + }, + }); } else { - setTimeout(() => { - setDistricts((prev) => [ - ...prev, - { - id: prev.length ? prev[prev.length - 1].id + 1 : 1, - name: values.name, - user: selectedUser, - }, - ]); - setDialogOpen(false); - setLoad(false); - }, 2000); + mutate({ name: values.name, user_id: Number(values.userId) }); } } @@ -109,36 +138,100 @@ export default function AddDistrict({ ( - - - - - - - - )} + render={({ field }) => { + const selectedUser = user?.find( + (u) => String(u.id) === field.value, + ); + return ( + + + + + + + + + + + + + + + + {isUserLoading ? ( +
+ +
+ ) : user && user.length > 0 ? ( + + {user.map((u) => ( + { + field.onChange(String(u.id)); + setOpenUser(false); + }} + > + + {u.first_name} {u.last_name} {u.region.name} + + ))} + + ) : ( + Foydalanuvchi topilmadi + )} +
+
+
+
+ + +
+ ); + }} /> - {/* SUBMIT */} - diff --git a/src/features/districts/ui/DeleteDiscrit.tsx b/src/features/districts/ui/DeleteDiscrit.tsx new file mode 100644 index 0000000..40ab633 --- /dev/null +++ b/src/features/districts/ui/DeleteDiscrit.tsx @@ -0,0 +1,90 @@ +import { discrit_api } from "@/features/districts/lib/api"; +import type { DistrictListData } from "@/features/districts/lib/data"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Loader2, Trash, X } from "lucide-react"; +import { type Dispatch, type SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + opneDelete: boolean; + setOpenDelete: Dispatch>; + setDiscritDelete: Dispatch>; + discrit: DistrictListData | null; +} + +const DeleteDiscrit = ({ + opneDelete, + setOpenDelete, + setDiscritDelete, + discrit, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate: deleteDiscrict, isPending } = useMutation({ + mutationFn: (id: number) => discrit_api.delete(id), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["discrit_list"] }); + toast.success(`Tuman o'chirildi`); + setOpenDelete(false); + setDiscritDelete(null); + }, + onError: (err: AxiosError) => { + console.log(err); + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + return ( + + + + Tumanni o'chirish + + Siz rostan ham {discrit?.user.first_name} {discrit?.user.last_name}{" "} + ga tegishli {discrit?.name} tumanini o'chirmoqchimisiz + + + + + + + + + ); +}; + +export default DeleteDiscrit; diff --git a/src/features/districts/ui/DistrictsList.tsx b/src/features/districts/ui/DistrictsList.tsx index cea660c..9235743 100644 --- a/src/features/districts/ui/DistrictsList.tsx +++ b/src/features/districts/ui/DistrictsList.tsx @@ -1,194 +1,83 @@ -import { fakeDistrict, type District } from "@/features/districts/lib/data"; -import AddDistrict from "@/features/districts/ui/AddDistrict"; -import { Button } from "@/shared/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/shared/ui/dialog"; -import { Input } from "@/shared/ui/input"; +import { discrit_api } from "@/features/districts/lib/api"; +import { type DistrictListData } from "@/features/districts/lib/data"; +import DeleteDiscrit from "@/features/districts/ui/DeleteDiscrit"; +import Filter from "@/features/districts/ui/Filter"; +import PaginationDistrict from "@/features/districts/ui/PaginationDistrict"; +import TableDistrict from "@/features/districts/ui/TableDistrict"; +import { useQuery } from "@tanstack/react-query"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/shared/ui/table"; -import clsx from "clsx"; - -import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react"; import { useState } from "react"; const DistrictsList = () => { const [currentPage, setCurrentPage] = useState(1); - const totalPages = 5; - - const [districts, setDistricts] = useState(fakeDistrict); - const [search, setSearch] = useState(""); + const [editingDistrict, setEditingDistrict] = + useState(null); - const [userSearch, setUserSearch] = useState(""); + const { data, isLoading, isError } = useQuery({ + queryKey: ["discrit_list", currentPage, search], + queryFn: () => + discrit_api.list({ + limit: 20, + offset: (currentPage - 1) * 20, + name: search, + }), + select(data) { + return data.data.data; + }, + }); - const [editing, setEditing] = useState(null); + const totalPages = data ? Math.ceil(data.count / 20) : 1; const [dialogOpen, setDialogOpen] = useState(false); - const filtered = districts.filter((d) => { - return ( - d.name.toLowerCase().includes(search.toLowerCase()) && - `${d.user.firstName} ${d.user.lastName}` - .toLowerCase() - .includes(userSearch.toLowerCase()) - ); - }); + const [disricDelete, setDiscritDelete] = useState( + null, + ); + const [opneDelete, setOpenDelete] = useState(false); - function deleteDistrict(id: number) { - setDistricts((prev) => prev.filter((d) => d.id !== id)); - } + const handleDelete = (user: DistrictListData) => { + setDiscritDelete(user); + setOpenDelete(true); + }; return (

Tumanlar ro‘yxati

-
- setSearch(e.target.value)} - className="max-w-sm h-12" - /> - - setUserSearch(e.target.value)} - className="max-w-sm h-12" - /> - - - - - - - - - {editing ? "Tumanni tahrirlash" : "Yangi tuman qo‘shish"} - - - - - - -
+
-
- - - - ID - Tuman nomi - Kim qo‘shgan - Harakatlar - - + - - {filtered.map((d) => ( - - {d.id} - {d.name} - - {d.user.firstName} {d.user.lastName} - - - + - - - - ))} - - {filtered.length === 0 && ( - - - Hech qanday tuman topilmadi - - - )} - -
-
- -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+
); }; diff --git a/src/features/districts/ui/Filter.tsx b/src/features/districts/ui/Filter.tsx new file mode 100644 index 0000000..7d4de4d --- /dev/null +++ b/src/features/districts/ui/Filter.tsx @@ -0,0 +1,65 @@ +import type { DistrictListData } from "@/features/districts/lib/data"; +import AddDistrict from "@/features/districts/ui/AddDistrict"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/shared/ui/dialog"; +import { Input } from "@/shared/ui/input"; +import { Plus } from "lucide-react"; +import { type Dispatch, type SetStateAction } from "react"; + +interface Props { + search: string; + setSearch: Dispatch>; + dialogOpen: boolean; + setDialogOpen: Dispatch>; + editing: DistrictListData | null; + setEditing: Dispatch>; +} + +const Filter = ({ + search, + setSearch, + dialogOpen, + setDialogOpen, + setEditing, + editing, +}: Props) => { + return ( +
+ setSearch(e.target.value)} + className="max-w-sm h-12" + /> + + + + + + + + + + {editing ? "Tumanni tahrirlash" : "Yangi tuman qo‘shish"} + + + + + + +
+ ); +}; + +export default Filter; diff --git a/src/features/districts/ui/PaginationDistrict.tsx b/src/features/districts/ui/PaginationDistrict.tsx new file mode 100644 index 0000000..4eb5c34 --- /dev/null +++ b/src/features/districts/ui/PaginationDistrict.tsx @@ -0,0 +1,57 @@ +import { Button } from "@/shared/ui/button"; +import clsx from "clsx"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import { type Dispatch, type SetStateAction } from "react"; + +interface Props { + currentPage: number; + setCurrentPage: Dispatch>; + totalPages: number; +} + +const PaginationDistrict = ({ + currentPage, + setCurrentPage, + totalPages, +}: Props) => { + return ( +
+ + {Array.from({ length: totalPages }, (_, i) => ( + + ))} + +
+ ); +}; + +export default PaginationDistrict; diff --git a/src/features/districts/ui/TableDistrict.tsx b/src/features/districts/ui/TableDistrict.tsx new file mode 100644 index 0000000..b4498ff --- /dev/null +++ b/src/features/districts/ui/TableDistrict.tsx @@ -0,0 +1,109 @@ +import type { DistrictListData } from "@/features/districts/lib/data"; +import { Button } from "@/shared/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui/table"; +import { Edit, Loader2, Trash } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + data: DistrictListData[] | []; + isLoading: boolean; + isError: boolean; + handleDelete: (user: DistrictListData) => void; + setDialogOpen: Dispatch>; + setEditingDistrict: Dispatch>; + currentPage: number; +} + +const TableDistrict = ({ + data, + isError, + isLoading, + handleDelete, + setDialogOpen, + setEditingDistrict, + currentPage, +}: Props) => { + return ( +
+ {isLoading && ( +
+ + + +
+ )} + + {isError && ( +
+ + Ma'lumotlarni olishda xatolik yuz berdi. + +
+ )} + + {!isError && !isLoading && ( + + + + ID + Tuman nomi + Kim qo‘shgan + Harakatlar + + + + + {data.map((d, index) => ( + + {index + 1 + (currentPage - 1) * 20} + {d.name} + + {d.user.first_name} {d.user.last_name} + + + + + + + + ))} + + {data.length === 0 && ( + + + Hech qanday tuman topilmadi + + + )} + +
+ )} +
+ ); +}; + +export default TableDistrict; diff --git a/src/features/region/lib/api.ts b/src/features/region/lib/api.ts new file mode 100644 index 0000000..66b5063 --- /dev/null +++ b/src/features/region/lib/api.ts @@ -0,0 +1,14 @@ +import type { RegionListRes } from "@/features/region/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { REGIONS } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const region_api = { + async list(params: { + limit?: number; + offset?: number; + name?: string; + }): Promise> { + return await httpClient.get(`${REGIONS}list/`, { params }); + }, +}; diff --git a/src/features/region/lib/data.ts b/src/features/region/lib/data.ts index 3f2f0a7..73e493d 100644 --- a/src/features/region/lib/data.ts +++ b/src/features/region/lib/data.ts @@ -17,3 +17,16 @@ export const fakeRegionList: RegionType[] = [ name: "Andijon", }, ]; + +export interface RegionListRes { + status_code: number; + status: string; + message: string; + data: RegionListResData[]; +} + +export interface RegionListResData { + id: number; + name: string; + created_at: string; +} diff --git a/src/features/users/lib/api.ts b/src/features/users/lib/api.ts new file mode 100644 index 0000000..31c411a --- /dev/null +++ b/src/features/users/lib/api.ts @@ -0,0 +1,41 @@ +import type { + UserCreateReq, + UserListRes, + UserUpdateReq, +} from "@/features/users/lib/data"; +import httpClient from "@/shared/config/api/httpClient"; +import { USER } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +export const user_api = { + async list(params: { + limit?: number; + offset?: number; + search?: string; + is_active?: boolean | string; + region_id?: number; + }): Promise> { + const res = await httpClient.get(`${USER}list/`, { params }); + return res; + }, + + async update({ body, id }: { id: number; body: UserUpdateReq }) { + const res = await httpClient.patch(`${USER}${id}/update/`, body); + return res; + }, + + async create(body: UserCreateReq) { + const res = await httpClient.post(`${USER}create/`, body); + return res; + }, + + async active(id: number) { + const res = await httpClient.post(`${USER}${id}/activate/`); + return res; + }, + + async delete({ id }: { id: number }) { + const res = await httpClient.delete(`${USER}${id}/delete/`); + return res; + }, +}; diff --git a/src/features/users/lib/data.ts b/src/features/users/lib/data.ts index 2e4853f..2906f34 100644 --- a/src/features/users/lib/data.ts +++ b/src/features/users/lib/data.ts @@ -50,3 +50,41 @@ export const FakeUserList: User[] = [ isActive: true, }, ]; + +export interface UserListRes { + status_code: number; + status: string; + message: string; + data: { + count: number; + next: string | null; + previous: null | string; + results: UserListData[]; + }; +} + +export interface UserListData { + id: number; + first_name: string; + last_name: string; + region: { + id: number; + name: string; + }; + is_active: boolean; + created_at: string; +} + +export interface UserUpdateReq { + first_name: string; + last_name: string; + region: number; + is_active: boolean; +} + +export interface UserCreateReq { + first_name: string; + last_name: string; + region_id: number; + is_active: boolean; +} diff --git a/src/features/users/ui/AddUsers.tsx b/src/features/users/ui/AddUsers.tsx index 5e086cf..f26c689 100644 --- a/src/features/users/ui/AddUsers.tsx +++ b/src/features/users/ui/AddUsers.tsx @@ -1,4 +1,9 @@ -import type { User } from "@/features/users/lib/data"; +import { user_api } from "@/features/users/lib/api"; +import type { + UserCreateReq, + UserListData, + UserUpdateReq, +} from "@/features/users/lib/data"; import { AddedUser } from "@/features/users/lib/form"; import { Button } from "@/shared/ui/button"; import { @@ -18,60 +23,83 @@ import { SelectValue, } from "@/shared/ui/select"; import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; import { Loader2 } from "lucide-react"; -import { useState } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; import type z from "zod"; interface UserFormProps { - initialData?: User | null; - setUsers: React.Dispatch>; + initialData: UserListData | null; setDialogOpen: React.Dispatch>; } -const AddUsers = ({ initialData, setUsers, setDialogOpen }: UserFormProps) => { - const [load, setLoad] = useState(false); +const AddUsers = ({ initialData, setDialogOpen }: UserFormProps) => { const form = useForm>({ resolver: zodResolver(AddedUser), defaultValues: { - firstName: initialData?.firstName || "", - lastName: initialData?.lastName || "", - region: initialData?.region || "", - isActive: initialData ? String(initialData.isActive) : "true", + firstName: initialData?.first_name || "", + lastName: initialData?.last_name || "", + region: initialData?.region.name || "", + isActive: initialData ? String(initialData.is_active) : "true", + }, + }); + const queryClient = useQueryClient(); + + const { mutate: update, isPending } = useMutation({ + mutationFn: ({ body, id }: { id: number; body: UserUpdateReq }) => + user_api.update({ body, id }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["user_list"] }); + toast.success(`Foydalanuvchi tahrirlandi`); + setDialogOpen(false); + }, + onError: (err: AxiosError) => { + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + const { mutate: create, isPending: createPending } = useMutation({ + mutationFn: (body: UserCreateReq) => user_api.create(body), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["user_list"] }); + toast.success(`Foydalanuvchi qo'shildi`); + setDialogOpen(false); + }, + onError: (err: AxiosError) => { + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); }, }); function onSubmit(values: z.infer) { - setLoad(true); if (initialData) { - setTimeout(() => { - setUsers((prev) => - prev.map((user) => - user.id === initialData.id - ? { - ...user, - ...values, - isActive: values.isActive === "true" ? true : false, - } - : user, - ), - ); - setLoad(false); - setDialogOpen(false); - }, 2000); - } else { - setTimeout(() => { - setUsers((prev) => [ - ...prev, - { - id: prev.length ? prev[prev.length - 1].id + 1 : 1, - ...values, - isActive: values.isActive === "true" ? true : false, - }, - ]); - setLoad(false); - setDialogOpen(false); - }, 2000); + update({ + body: { + first_name: values.firstName, + is_active: values.isActive === "true" ? true : false, + last_name: values.lastName, + region: Number(values.region), + }, + id: initialData.id, + }); + } else if (initialData === null) { + create({ + first_name: values.firstName, + is_active: values.isActive === "true" ? true : false, + last_name: values.lastName, + region_id: Number(values.region), + }); } } @@ -126,9 +154,9 @@ const AddUsers = ({ initialData, setUsers, setDialogOpen }: UserFormProps) => { - Toshkent - Samarqand - Bekobod + Toshkent + Samarqand + Bekobod @@ -166,7 +194,7 @@ const AddUsers = ({ initialData, setUsers, setDialogOpen }: UserFormProps) => { type="submit" className="w-full h-12 text-lg rounded-lg bg-blue-600 hover:bg-blue-600 cursor-pointer" > - {load ? ( + {isPending || createPending ? ( ) : initialData ? ( "Saqlash" diff --git a/src/features/users/ui/DeleteUser.tsx b/src/features/users/ui/DeleteUser.tsx new file mode 100644 index 0000000..50fca25 --- /dev/null +++ b/src/features/users/ui/DeleteUser.tsx @@ -0,0 +1,90 @@ +import { user_api } from "@/features/users/lib/api"; +import type { UserListData } from "@/features/users/lib/data"; +import { Button } from "@/shared/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/shared/ui/dialog"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError } from "axios"; +import { Loader2, Trash, X } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + opneDelete: boolean; + setOpenDelete: Dispatch>; + setUserDelete: Dispatch>; + userDelete: UserListData | null; +} + +const DeleteUser = ({ + opneDelete, + setOpenDelete, + userDelete, + setUserDelete, +}: Props) => { + const queryClient = useQueryClient(); + + const { mutate: deleteUser, isPending } = useMutation({ + mutationFn: ({ id }: { id: number }) => user_api.delete({ id }), + + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["user_list"] }); + toast.success(`Foydalanuvchi o'chirildi`); + setOpenDelete(false); + setUserDelete(null); + }, + onError: (err: AxiosError) => { + console.log(err); + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + return ( + + + + Foydalanuvchini o'chrish + + Siz rostan ham {userDelete?.first_name} {userDelete?.last_name}{" "} + nomli foydalanuvchini o'chimoqchimiszi + + + + + + + + + ); +}; + +export default DeleteUser; diff --git a/src/features/users/ui/Filter.tsx b/src/features/users/ui/Filter.tsx new file mode 100644 index 0000000..08c8d78 --- /dev/null +++ b/src/features/users/ui/Filter.tsx @@ -0,0 +1,202 @@ +import { region_api } from "@/features/region/lib/api"; +import type { RegionListResData } from "@/features/region/lib/data"; +import type { UserListData } from "@/features/users/lib/data"; +import AddUsers from "@/features/users/ui/AddUsers"; +import { cn } from "@/shared/lib/utils"; +import { Button } from "@/shared/ui/button"; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "@/shared/ui/command"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/shared/ui/dialog"; +import { Input } from "@/shared/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/ui/select"; +import { useQuery } from "@tanstack/react-query"; +import { Check, ChevronsUpDown, Loader2, Plus } from "lucide-react"; +import { useState, type Dispatch, type SetStateAction } from "react"; + +interface Props { + searchTerm: string; + setSearchTerm: Dispatch>; + statusFilter: "all" | "true" | "false"; + setStatusFilter: Dispatch>; + regionValue: RegionListResData | null; + setRegionValue: Dispatch>; + dialogOpen: boolean; + setDialogOpen: Dispatch>; + editingUser: UserListData | null; + setEditingUser: Dispatch>; +} + +const Filter = ({ + searchTerm, + setSearchTerm, + statusFilter, + setStatusFilter, + regionValue, + setRegionValue, + dialogOpen, + setDialogOpen, + editingUser, + setEditingUser, +}: Props) => { + const [openRegion, setOpenRegion] = useState(false); + const [regionSearch, setRegionSearch] = useState(""); + + const { data: regions, isLoading } = useQuery({ + queryKey: ["region_list", regionSearch], + queryFn: () => region_api.list({ name: regionSearch }), + select: (res) => res.data.data, + }); + + return ( +
+ {/* Search input */} + setSearchTerm(e.target.value)} + /> + + + + + + + + + + + + {isLoading ? ( +
+ +
+ ) : regions && regions.length > 0 ? ( + + { + setRegionValue(null); + setRegionSearch(""); + setOpenRegion(false); + }} + > + + Barchasi + + {regions.map((r) => ( + { + setRegionValue(r); + setRegionSearch(""); + setOpenRegion(false); + }} + > + + {r.name} + + ))} + + ) : ( + Hudud topilmadi + )} +
+
+
+
+ + + + + + + + + {editingUser + ? "Foydalanuvchini tahrirlash" + : "Foydalanuvchi qo'shish"} + + + + + + +
+ ); +}; + +export default Filter; diff --git a/src/features/users/ui/Pagination.tsx b/src/features/users/ui/Pagination.tsx new file mode 100644 index 0000000..0618c8a --- /dev/null +++ b/src/features/users/ui/Pagination.tsx @@ -0,0 +1,53 @@ +import { Button } from "@/shared/ui/button"; +import clsx from "clsx"; +import { ChevronLeft, ChevronRight } from "lucide-react"; +import type { Dispatch, SetStateAction } from "react"; + +interface Props { + currentPage: number; + setCurrentPage: Dispatch>; + totalPages: number; +} + +const Pagination = ({ currentPage, setCurrentPage, totalPages }: Props) => { + return ( +
+ + {Array.from({ length: totalPages }, (_, i) => ( + + ))} + +
+ ); +}; + +export default Pagination; diff --git a/src/features/users/ui/UserTable.tsx b/src/features/users/ui/UserTable.tsx new file mode 100644 index 0000000..bc9b7c6 --- /dev/null +++ b/src/features/users/ui/UserTable.tsx @@ -0,0 +1,202 @@ +import { user_api } from "@/features/users/lib/api"; +import type { UserListData, UserListRes } from "@/features/users/lib/data"; +import { Button } from "@/shared/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/shared/ui/table"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { AxiosError, AxiosResponse } from "axios"; +import clsx from "clsx"; +import { Edit, Loader2, Trash } from "lucide-react"; +import { useState, type Dispatch, type SetStateAction } from "react"; +import { toast } from "sonner"; + +interface Props { + data: AxiosResponse | undefined; + isLoading: boolean; + isError: boolean; + setDialogOpen: Dispatch>; + setEditingUser: Dispatch>; + handleDelete: (user: UserListData) => void; + currentPage: number; +} + +const UserTable = ({ + data, + isLoading, + isError, + setDialogOpen, + handleDelete, + setEditingUser, + currentPage, +}: Props) => { + const queryClient = useQueryClient(); + + const [pendingUserId, setPendingUserId] = useState(null); + + const { mutate: active } = useMutation({ + mutationFn: (id: number) => user_api.active(id), + onMutate: (id) => { + setPendingUserId(id); + }, + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["user_list"] }); + toast.success(`Foydalanuvchi aktivlashdi`); + setPendingUserId(null); + }, + onError: (err: AxiosError) => { + const errMessage = err.response?.data as { message: string }; + const messageText = errMessage.message; + setPendingUserId(null); + toast.error(messageText || "Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + const handleActivate = (userId: number) => { + active(userId); + }; + + return ( +
+ {isLoading && ( +
+ + + +
+ )} + + {isError && ( +
+ + Ma'lumotlarni olishda xatolik yuz berdi. + +
+ )} + {!isLoading && !isError && ( + + + + ID + Ismi + Familiyasi + Hududi + Holati + Harakatlar + + + + {data && data.data.data.results.length > 0 ? ( + data.data.data.results.map((user, index) => ( + + {index + 1 + (currentPage - 1) * 20} + {user.first_name} + {user.last_name} + {user.region.name} + + {/* */} + + + + + + + + )) + ) : ( + + + Foydalanuvchilar topilmadi. + + + )} + +
+ )} +
+ ); +}; + +export default UserTable; diff --git a/src/features/users/ui/UsersList.tsx b/src/features/users/ui/UsersList.tsx index e0ef7ca..13b05c2 100644 --- a/src/features/users/ui/UsersList.tsx +++ b/src/features/users/ui/UsersList.tsx @@ -1,292 +1,107 @@ import SidebarLayout from "@/SidebarLayout"; -import { FakeUserList, type User } from "@/features/users/lib/data"; -import AddUsers from "@/features/users/ui/AddUsers"; -import { Button } from "@/shared/ui/button"; -import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/shared/ui/dialog"; -import { Input } from "@/shared/ui/input"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from "@/shared/ui/table"; -import clsx from "clsx"; -import { ChevronLeft, ChevronRight, Edit, Plus, Trash } from "lucide-react"; -import { useMemo, useState } from "react"; +import type { RegionListResData } from "@/features/region/lib/data"; +import { user_api } from "@/features/users/lib/api"; +import type { UserListData } from "@/features/users/lib/data"; +import DeleteUser from "@/features/users/ui/DeleteUser"; +import Filter from "@/features/users/ui/Filter"; +import Pagination from "@/features/users/ui/Pagination"; +import UserTable from "@/features/users/ui/UserTable"; + +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; const UsersList = () => { - const [users, setUsers] = useState(FakeUserList); - - const [editingUser, setEditingUser] = useState(null); - const [dialogOpen, setDialogOpen] = useState(false); - + const [editingUser, setEditingUser] = useState(null); const [searchTerm, setSearchTerm] = useState(""); - const [statusFilter, setStatusFilter] = useState< - "all" | "active" | "inactive" - >("all"); - const [regionFilter, setRegionFilter] = useState("all"); - - // Pagination state + const [regionValue, setRegionValue] = useState( + null, + ); + const limit = 20; const [currentPage, setCurrentPage] = useState(1); - const pageSize = 5; - - const handleDelete = (id: number) => { - setUsers(users.filter((u) => u.id !== id)); - }; - - // Filter & search users - const filteredUsers = useMemo(() => { - return users.filter((user) => { - const matchesSearch = - user.firstName.toLowerCase().includes(searchTerm.toLowerCase()) || - user.lastName.toLowerCase().includes(searchTerm.toLowerCase()); - - const matchesStatus = - statusFilter === "all" || - (statusFilter === "active" && user.isActive) || - (statusFilter === "inactive" && !user.isActive); - - const matchesRegion = - regionFilter === "all" || user.region === regionFilter; - - return matchesSearch && matchesStatus && matchesRegion; - }); - }, [users, searchTerm, statusFilter, regionFilter]); - - const totalPages = Math.ceil(filteredUsers.length / pageSize); - const paginatedUsers = filteredUsers.slice( - (currentPage - 1) * pageSize, - currentPage * pageSize, + const [statusFilter, setStatusFilter] = useState<"all" | "true" | "false">( + "all", ); - // Hududlarni filter qilish uchun - const regions = Array.from(new Set(users.map((u) => u.region))); + const [userDelete, setUserDelete] = useState(null); + const [opneDelete, setOpenDelete] = useState(false); + + const { data, isLoading, isError } = useQuery({ + queryKey: ["user_list", currentPage, statusFilter, regionValue, searchTerm], + queryFn: () => { + const params: { + limit?: number; + offset?: number; + search?: string; + is_active?: boolean | string; + region_id?: number; + } = { + limit, + offset: (currentPage - 1) * limit, + search: searchTerm, + }; + + if (regionValue !== null) { + params.region_id = Number(regionValue.id); + } + + if (statusFilter !== "all") { + params.is_active = statusFilter; + } + + return user_api.list(params); + }, + }); + + const [dialogOpen, setDialogOpen] = useState(false); + + const handleDelete = (user: UserListData) => { + setUserDelete(user); + setOpenDelete(true); + }; + + const totalPages = data ? Math.ceil(data.data.data.count / limit) : 1; return (
- {/* Top Controls */}

Foydalanuvchilar ro'yxati

-
- setSearchTerm(e.target.value)} - /> - - - - - - - - - - - - - {editingUser - ? "Foydalanuvchini tahrirlash" - : "Foydalanuvchi qo'shish"} - - - - - - -
+
-
- - - - ID - Ismi - Familiyasi - Hududi - Holati - Harakatlar - - - - {paginatedUsers.length > 0 ? ( - paginatedUsers.map((user) => ( - - {user.id} - {user.firstName} - {user.lastName} - {user.region} - - - - - - - - - )) - ) : ( - - - Foydalanuvchilar topilmadi. - - - )} - -
-
+ + - {/* Pagination */} -
- - {Array.from({ length: totalPages }, (_, i) => ( - - ))} - -
+
); diff --git a/src/index.css b/src/index.css index dc288e7..f4c1e9b 100644 --- a/src/index.css +++ b/src/index.css @@ -1,5 +1,5 @@ -@import 'tailwindcss'; -@import 'tw-animate-css'; +@import "tailwindcss"; +@import "tw-animate-css"; @custom-variant dark (&:is(.dark *)); diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index bd9e670..3444fab 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,166 +1,10 @@ -import { Button } from "@/shared/ui/button"; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from "@/shared/ui/card"; -import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/shared/ui/form"; -import { Input } from "@/shared/ui/input"; -import { Label } from "@/shared/ui/label"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Eye, EyeOff, Loader2 } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import { useNavigate } from "react-router-dom"; -import { toast } from "sonner"; -import { z } from "zod"; +import AuthLogin from "@/features/auth/ui/AuthLogin"; +import LoginLayout from "@/LoginLayout"; export default function AdminLoginPage() { - const [showPassword, setShowPassword] = useState(false); - const navigate = useNavigate(); - - const FAKE_ADMIN = { - email: "admin", - password: "12345", - }; - - const FormSchema = z.object({ - email: z.string().min(1, "Login kiriting"), - password: z.string().min(1, "Parolni kiriting"), - }); - - const form = useForm>({ - resolver: zodResolver(FormSchema), - defaultValues: { - email: "", - password: "", - }, - }); - - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [success, setSuccess] = useState(false); - - useEffect(() => { - if (success) { - toast.success("Muvaffaqiyatli kirdingiz!", { - richColors: true, - position: "top-center", - }); - } else if (error) { - toast.error(error, { - richColors: true, - position: "top-center", - }); - } - }, [success, error]); - - const handleSubmit = async (values: z.infer) => { - setLoading(true); - setError(null); - setSuccess(false); - - setTimeout(() => { - if ( - values.email === FAKE_ADMIN.email && - values.password === FAKE_ADMIN.password - ) { - setSuccess(true); - navigate("/dashboard"); - } else { - setError("Login yoki parol noto‘g‘ri!"); - } - setLoading(false); - }, 800); - }; - return ( -
- - - - Admin panelga kirish - - - Login va parolingizni kiriting. - - - - -
- - ( - - - - - - - - )} - /> - - ( - - - -
- - -
-
- -
- )} - /> - - - - -
-
-
+ + + ); } diff --git a/src/providers/routing/AppRoutes.tsx b/src/providers/routing/AppRoutes.tsx index 4201c4a..ba60694 100644 --- a/src/providers/routing/AppRoutes.tsx +++ b/src/providers/routing/AppRoutes.tsx @@ -1,3 +1,4 @@ +import LoginLayout from "@/LoginLayout"; import Districts from "@/pages/Districts"; import Doctors from "@/pages/Doctors"; import UsersPage from "@/pages/Home"; @@ -23,7 +24,11 @@ const AppRouter = () => { }, { path: "/dashboard", - element: , + element: ( + + + + ), }, { path: "/dashboard/plans", diff --git a/src/shared/config/api/URLs.ts b/src/shared/config/api/URLs.ts index 5ff9d1c..65a9cf6 100644 --- a/src/shared/config/api/URLs.ts +++ b/src/shared/config/api/URLs.ts @@ -1,6 +1,10 @@ const BASE_URL = - import.meta.env.VITE_API_URL || 'https://jsonplaceholder.typicode.com'; + import.meta.env.VITE_API_URL || "https://api.meridynpharma.com"; -const ENDP_POSTS = '/posts/'; +const LOGIN = "/api/v1/authentication/admin_login/"; +const USER = "/api/v1/admin/user/"; +const REGION = "/api/v1/admin/district/"; +const REGIONS = "/api/v1/admin/region/"; +const DISTRICT = "/api/v1/admin/district/"; -export { BASE_URL, ENDP_POSTS }; +export { BASE_URL, DISTRICT, LOGIN, REGION, REGIONS, USER }; diff --git a/src/shared/config/api/httpClient.ts b/src/shared/config/api/httpClient.ts index 07d479f..dad5eb1 100644 --- a/src/shared/config/api/httpClient.ts +++ b/src/shared/config/api/httpClient.ts @@ -1,6 +1,7 @@ -import axios from 'axios'; -import { BASE_URL } from './URLs'; -import i18n from '@/shared/config/i18n'; +import i18n from "@/shared/config/i18n"; +import { getToken } from "@/shared/lib/cookie"; +import axios from "axios"; +import { BASE_URL } from "./URLs"; const httpClient = axios.create({ baseURL: BASE_URL, @@ -13,11 +14,11 @@ httpClient.interceptors.request.use( // Language configs const language = i18n.language; - config.headers['Accept-Language'] = language; - // const accessToken = localStorage.getItem('accessToken'); - // if (accessToken) { - // config.headers['Authorization'] = `Bearer ${accessToken}`; - // } + config.headers["Accept-Language"] = language; + const accessToken = getToken(); + if (accessToken) { + config.headers["Authorization"] = `Bearer ${accessToken}`; + } return config; }, @@ -27,7 +28,7 @@ httpClient.interceptors.request.use( httpClient.interceptors.response.use( (response) => response, (error) => { - console.error('API error:', error); + console.error("API error:", error); return Promise.reject(error); }, ); diff --git a/src/shared/config/api/test/test.request.ts b/src/shared/config/api/test/test.request.ts deleted file mode 100644 index a8d9bcb..0000000 --- a/src/shared/config/api/test/test.request.ts +++ /dev/null @@ -1,14 +0,0 @@ -import httpClient from '@/shared/config/api/httpClient'; -import type { TestApiType } from '@/shared/config/api/test/test.model'; -import type { ReqWithPagination } from '@/shared/config/api/types'; -import { ENDP_POSTS } from '@/shared/config/api/URLs'; -import type { AxiosResponse } from 'axios'; - -const getPosts = async ( - pagination?: ReqWithPagination, -): Promise> => { - const response = await httpClient.get(ENDP_POSTS, { params: pagination }); - return response; -}; - -export { getPosts }; diff --git a/src/shared/lib/cookie.ts b/src/shared/lib/cookie.ts new file mode 100644 index 0000000..3d244ae --- /dev/null +++ b/src/shared/lib/cookie.ts @@ -0,0 +1,15 @@ +import cookie from "js-cookie"; + +const token = "access_meridyn_admin"; + +export const saveToken = (value: string) => { + cookie.set(token, value); +}; + +export const getToken = () => { + return cookie.get(token); +}; + +export const removeToken = () => { + cookie.remove(token); +}; diff --git a/src/shared/ui/command.tsx b/src/shared/ui/command.tsx new file mode 100644 index 0000000..fd75261 --- /dev/null +++ b/src/shared/ui/command.tsx @@ -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) { + return ( + + ); +} + +function CommandDialog({ + title = "Command Palette", + description = "Search for a command to run...", + children, + className, + showCloseButton = true, + ...props +}: React.ComponentProps & { + title?: string; + description?: string; + className?: string; + showCloseButton?: boolean; +}) { + return ( + + + {title} + {description} + + + + {children} + + + + ); +} + +function CommandInput({ + className, + ...props +}: React.ComponentProps) { + return ( +
+ + +
+ ); +} + +function CommandList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandEmpty({ + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +function CommandShortcut({ + className, + ...props +}: React.ComponentProps<"span">) { + return ( + + ); +} + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/src/widgets/sidebar-layout/index.tsx b/src/widgets/sidebar-layout/index.tsx index 8a544bc..32eba19 100644 --- a/src/widgets/sidebar-layout/index.tsx +++ b/src/widgets/sidebar-layout/index.tsx @@ -16,6 +16,7 @@ import { } from "lucide-react"; import { Link, useLocation, useNavigate } from "react-router-dom"; +import { removeToken } from "@/shared/lib/cookie"; import { Button } from "@/shared/ui/button"; import { Sidebar, @@ -147,7 +148,10 @@ export function AppSidebar() {