From 05b752daf271a017ffecb6bf3b55ff29655d0c1e Mon Sep 17 00:00:00 2001 From: Samandar Turgunboyev Date: Sat, 25 Oct 2025 18:42:01 +0500 Subject: [PATCH] api ulandi --- .env | 2 +- .gitignore | 2 + .husky/pre-commit | 1 - .husky/pre-push | 1 - package-lock.json | 308 +++- package.json | 13 +- src/App.tsx | 52 +- src/ProtectedRoute.tsx | 36 + src/main.tsx | 2 + src/pages/agencies/AgencyDetail.tsx | 240 --- src/pages/agencies/lib/api.ts | 58 + src/pages/agencies/lib/type.ts | 58 + src/pages/agencies/{ => ui}/Agencies.tsx | 317 ++-- src/pages/agencies/ui/AgencyDetail.tsx | 398 +++++ src/pages/agencies/ui/EditAgecy.tsx | 273 +++ src/pages/auth/lib/form.ts | 8 + src/pages/auth/ui/Login.tsx | 181 ++ src/pages/bookings/ui/Bookings.tsx | 58 +- src/pages/employees/lib/api.ts | 74 + src/pages/employees/lib/type.ts | 35 + src/pages/employees/ui/EditEmployees.tsx | 349 ++++ src/pages/employees/ui/Employees.tsx | 523 +++--- src/pages/faq/ui/Faq.tsx | 43 +- src/pages/faq/ui/FaqCategory.tsx | 6 +- src/pages/finance/ui/Finance.tsx | 91 +- src/pages/finance/ui/FinanceDetailTour.tsx | 12 - src/pages/finance/ui/FinanceDetailUsers.tsx | 136 +- src/pages/news/lib/api.ts | 87 +- src/pages/news/lib/data.ts | 90 +- src/pages/news/lib/form.ts | 43 +- src/pages/news/lib/type.ts | 67 +- src/pages/news/ui/AddNews.tsx | 10 +- src/pages/news/ui/News.tsx | 89 +- src/pages/news/ui/NewsCategory.tsx | 277 +++- src/pages/news/ui/StepOne.tsx | 188 ++- src/pages/news/ui/StepTwo.tsx | 341 ++-- src/pages/tours/lib/api.ts | 425 +++++ src/pages/tours/lib/column.tsx | 307 ++++ src/pages/tours/lib/form.ts | 188 +++ src/pages/tours/lib/store.ts | 36 + src/pages/tours/lib/type.ts | 302 ++++ src/pages/tours/ui/BadgeTable.tsx | 353 ++++ src/pages/tours/ui/CreateEditTour.tsx | 23 +- src/pages/tours/ui/FeaturesTable.tsx | 351 ++++ src/pages/tours/ui/FeaturesTableType.tsx | 342 ++++ src/pages/tours/ui/MealTable.tsx | 324 ++++ src/pages/tours/ui/StepOne.tsx | 1464 ++++++++++++----- src/pages/tours/ui/StepTwo.tsx | 635 +++++-- src/pages/tours/ui/TarifTable.tsx | 320 ++++ src/pages/tours/ui/TicketsImagesModel.tsx | 99 +- src/pages/tours/ui/TourDetail.tsx | 71 +- src/pages/tours/ui/Tours.tsx | 195 ++- src/pages/tours/ui/ToursSetting.tsx | 641 +++----- src/pages/tours/ui/TransportTable.tsx | 334 ++++ src/pages/users/Edit.tsx | 220 --- src/pages/users/User.tsx | 398 ----- src/pages/users/UserDetail.tsx | 689 -------- src/pages/users/lib/api.ts | 64 + src/pages/users/lib/type.ts | 99 ++ src/pages/users/{ => ui}/Create.tsx | 42 +- src/pages/users/ui/Edit.tsx | 263 +++ src/pages/users/ui/User.tsx | 350 ++++ src/pages/users/ui/UserDetail.tsx | 558 +++++++ src/shared/config/api/URLs.ts | 41 +- src/shared/config/api/auth/api.ts | 22 + src/shared/config/api/auth/auth.model.ts | 32 + src/shared/config/api/httpClient.ts | 97 +- src/shared/config/api/test/test.request.ts | 13 - .../config/i18n/locales/ru/translation.json | 327 +++- .../config/i18n/locales/uz/translation.json | 327 +++- src/shared/hooks/user.ts | 42 + src/shared/lib/authCookies.ts | 57 + src/shared/lib/formatPrice.ts | 2 +- src/shared/lib/onlyNumber.ts | 6 + src/shared/ui/avatar.tsx | 51 + src/shared/ui/dialog.tsx | 2 +- src/shared/ui/form.tsx | 14 +- src/shared/ui/iocnSelect.tsx | 10 +- src/shared/ui/radio-group.tsx | 43 + src/shared/ui/sonner.tsx | 38 + src/widgets/lang-toggle/ui/lang-toggle.tsx | 5 + .../real-pagination/ui/RealPagination.tsx | 109 ++ src/widgets/sidebar/ui/Sidebar.tsx | 99 +- vite.config.ts | 4 +- 84 files changed, 11179 insertions(+), 3724 deletions(-) delete mode 100644 .husky/pre-commit delete mode 100644 .husky/pre-push create mode 100644 src/ProtectedRoute.tsx delete mode 100644 src/pages/agencies/AgencyDetail.tsx create mode 100644 src/pages/agencies/lib/api.ts create mode 100644 src/pages/agencies/lib/type.ts rename src/pages/agencies/{ => ui}/Agencies.tsx (52%) create mode 100644 src/pages/agencies/ui/AgencyDetail.tsx create mode 100644 src/pages/agencies/ui/EditAgecy.tsx create mode 100644 src/pages/auth/lib/form.ts create mode 100644 src/pages/auth/ui/Login.tsx create mode 100644 src/pages/employees/lib/api.ts create mode 100644 src/pages/employees/lib/type.ts create mode 100644 src/pages/employees/ui/EditEmployees.tsx create mode 100644 src/pages/tours/lib/api.ts create mode 100644 src/pages/tours/lib/column.tsx create mode 100644 src/pages/tours/lib/form.ts create mode 100644 src/pages/tours/lib/store.ts create mode 100644 src/pages/tours/lib/type.ts create mode 100644 src/pages/tours/ui/BadgeTable.tsx create mode 100644 src/pages/tours/ui/FeaturesTable.tsx create mode 100644 src/pages/tours/ui/FeaturesTableType.tsx create mode 100644 src/pages/tours/ui/MealTable.tsx create mode 100644 src/pages/tours/ui/TarifTable.tsx create mode 100644 src/pages/tours/ui/TransportTable.tsx delete mode 100644 src/pages/users/Edit.tsx delete mode 100644 src/pages/users/User.tsx delete mode 100644 src/pages/users/UserDetail.tsx create mode 100644 src/pages/users/lib/api.ts create mode 100644 src/pages/users/lib/type.ts rename src/pages/users/{ => ui}/Create.tsx (92%) create mode 100644 src/pages/users/ui/Edit.tsx create mode 100644 src/pages/users/ui/User.tsx create mode 100644 src/pages/users/ui/UserDetail.tsx create mode 100644 src/shared/config/api/auth/api.ts create mode 100644 src/shared/config/api/auth/auth.model.ts create mode 100644 src/shared/hooks/user.ts create mode 100644 src/shared/lib/authCookies.ts create mode 100644 src/shared/lib/onlyNumber.ts create mode 100644 src/shared/ui/avatar.tsx create mode 100644 src/shared/ui/radio-group.tsx create mode 100644 src/shared/ui/sonner.tsx create mode 100644 src/widgets/real-pagination/ui/RealPagination.tsx diff --git a/.env b/.env index 6d7cbca..5dea892 100644 --- a/.env +++ b/.env @@ -1 +1 @@ -VITE_API_URL=https://jsonplaceholder.typicode.com \ No newline at end of file +VITE_API_URL=https://simple-travel.felixits.uz/api/v1/ \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8b7e502..3ef37ae 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +.env + node_modules dist dist-ssr diff --git a/.husky/pre-commit b/.husky/pre-commit deleted file mode 100644 index d0a7784..0000000 --- a/.husky/pre-commit +++ /dev/null @@ -1 +0,0 @@ -npx lint-staged \ No newline at end of file diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100644 index 10da9ff..0000000 --- a/.husky/pre-push +++ /dev/null @@ -1 +0,0 @@ -npm run build \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index fe50c61..b7f3d0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,13 @@ "@hookform/resolvers": "^5.2.2", "@mui/material": "^7.3.4", "@pbe/react-yandex-maps": "^1.2.5", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -25,6 +27,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.7", "@tanstack/react-query": "^5.77.1", + "@tanstack/react-table": "^8.21.3", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -34,7 +37,9 @@ "framer-motion": "^12.23.24", "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", "react-day-picker": "^9.11.1", "react-dom": "^19.1.1", @@ -43,12 +48,16 @@ "react-i18next": "^15.7.3", "react-quill-new": "^3.6.0", "react-router-dom": "^7.9.4", + "react-select": "^5.10.2", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.13", - "zod": "^4.1.12" + "zod": "^4.1.12", + "zustand": "^5.0.8" }, "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", @@ -493,6 +502,7 @@ "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "aix" @@ -508,6 +518,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "android" @@ -523,6 +534,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "android" @@ -538,6 +550,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "android" @@ -553,6 +566,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -568,6 +582,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -583,6 +598,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -598,6 +614,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -613,6 +630,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -628,6 +646,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -643,6 +662,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "linux" @@ -658,6 +678,7 @@ "cpu": [ "loong64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -673,6 +694,7 @@ "cpu": [ "mips64el" ], + "dev": true, "optional": true, "os": [ "linux" @@ -688,6 +710,7 @@ "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -703,6 +726,7 @@ "cpu": [ "riscv64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -718,6 +742,7 @@ "cpu": [ "s390x" ], + "dev": true, "optional": true, "os": [ "linux" @@ -733,6 +758,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -748,6 +774,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "netbsd" @@ -763,6 +790,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "netbsd" @@ -778,6 +806,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "openbsd" @@ -793,6 +822,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "openbsd" @@ -808,6 +838,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "sunos" @@ -823,6 +854,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -838,6 +870,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "win32" @@ -853,6 +886,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -1462,6 +1496,33 @@ } } }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.10.tgz", + "integrity": "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-is-hydrated": "0.1.0", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-checkbox": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.3.tgz", @@ -1902,6 +1963,38 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz", + "integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.3", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.5", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.11", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz", @@ -2146,6 +2239,24 @@ } } }, + "node_modules/@radix-ui/react-use-is-hydrated": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz", + "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", @@ -2250,6 +2361,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "android" @@ -2262,6 +2374,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "android" @@ -2274,6 +2387,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -2286,6 +2400,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "darwin" @@ -2298,6 +2413,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -2310,6 +2426,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "freebsd" @@ -2322,6 +2439,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2334,6 +2452,7 @@ "cpu": [ "arm" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2346,6 +2465,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2358,6 +2478,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2370,6 +2491,7 @@ "cpu": [ "loong64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2382,6 +2504,7 @@ "cpu": [ "ppc64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2394,6 +2517,7 @@ "cpu": [ "riscv64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2406,6 +2530,7 @@ "cpu": [ "riscv64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2418,6 +2543,7 @@ "cpu": [ "s390x" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2430,6 +2556,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2442,6 +2569,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "linux" @@ -2454,6 +2582,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "openharmony" @@ -2466,6 +2595,7 @@ "cpu": [ "arm64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -2478,6 +2608,7 @@ "cpu": [ "ia32" ], + "dev": true, "optional": true, "os": [ "win32" @@ -2490,6 +2621,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -2502,6 +2634,7 @@ "cpu": [ "x64" ], + "dev": true, "optional": true, "os": [ "win32" @@ -2784,6 +2917,39 @@ "react": "^18 || ^19" } }, + "node_modules/@tanstack/react-table": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.21.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2828,7 +2994,15 @@ "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==" + "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", @@ -2840,7 +3014,7 @@ "version": "22.18.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", - "devOptional": true, + "dev": true, "dependencies": { "undici-types": "~6.21.0" } @@ -2861,6 +3035,7 @@ "version": "19.1.13", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", + "dev": true, "dependencies": { "csstype": "^3.0.2" } @@ -2869,7 +3044,7 @@ "version": "19.1.9", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.9.tgz", "integrity": "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==", - "devOptional": true, + "dev": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -3769,6 +3944,7 @@ "version": "0.25.4", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -4188,6 +4364,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "optional": true, "os": [ @@ -4560,6 +4737,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", @@ -5015,6 +5201,12 @@ "node": ">= 0.4" } }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5135,6 +5327,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -5154,6 +5347,16 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/next-themes": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", + "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -5332,6 +5535,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -5664,6 +5868,27 @@ "react-dom": ">=18" } }, + "node_modules/react-select": { + "version": "5.10.2", + "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz", + "integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.0", + "@emotion/cache": "^11.4.0", + "@emotion/react": "^11.8.1", + "@floating-ui/dom": "^1.0.1", + "@types/react-transition-group": "^4.4.0", + "memoize-one": "^6.0.0", + "prop-types": "^15.6.0", + "react-transition-group": "^4.3.0", + "use-isomorphic-layout-effect": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -5765,6 +5990,7 @@ "version": "4.52.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.2.tgz", "integrity": "sha512-I25/2QgoROE1vYV+NQ1En9T9UFB9Cmfm2CJ83zZOlaDpvz29wGQSZXWKw7MiNXau7wYgB/T9fVIdIuEQ+KbiiA==", + "dev": true, "dependencies": { "@types/estree": "1.0.8" }, @@ -5905,6 +6131,16 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -6057,6 +6293,7 @@ "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" @@ -6072,6 +6309,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "engines": { "node": ">=12.0.0" }, @@ -6088,6 +6326,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "engines": { "node": ">=12" }, @@ -6169,7 +6408,7 @@ "version": "5.9.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6205,7 +6444,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "devOptional": true + "dev": true }, "node_modules/update-browserslist-db": { "version": "1.1.3", @@ -6266,6 +6505,20 @@ } } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz", + "integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sidecar": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", @@ -6287,10 +6540,20 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.7.tgz", "integrity": "sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==", + "dev": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", @@ -6383,6 +6646,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, "engines": { "node": ">=12.0.0" }, @@ -6399,6 +6663,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, "engines": { "node": ">=12" }, @@ -6494,7 +6759,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", - "devOptional": true, + "dev": true, "bin": { "yaml": "bin.mjs" }, @@ -6522,6 +6787,35 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zustand": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", + "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 593f0fa..5046a62 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "vite --host", "build": "tsc -b && vite build", "lint": "eslint src --fix", "preview": "vite preview", @@ -17,11 +17,13 @@ "@hookform/resolvers": "^5.2.2", "@mui/material": "^7.3.4", "@pbe/react-yandex-maps": "^1.2.5", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", @@ -29,6 +31,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@tailwindcss/vite": "^4.1.7", "@tanstack/react-query": "^5.77.1", + "@tanstack/react-table": "^8.21.3", "axios": "^1.9.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -38,7 +41,9 @@ "framer-motion": "^12.23.24", "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", "react-day-picker": "^9.11.1", "react-dom": "^19.1.1", @@ -47,12 +52,16 @@ "react-i18next": "^15.7.3", "react-quill-new": "^3.6.0", "react-router-dom": "^7.9.4", + "react-select": "^5.10.2", + "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", "tailwindcss": "^4.1.13", - "zod": "^4.1.12" + "zod": "^4.1.12", + "zustand": "^5.0.8" }, "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 9f2dc42..b5e754a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,7 @@ -import Agencies from "@/pages/agencies/Agencies"; -import AgencyDetail from "@/pages/agencies/AgencyDetail"; +import Agencies from "@/pages/agencies/ui/Agencies"; +import AgencyDetail from "@/pages/agencies/ui/AgencyDetail"; +import EditAgecy from "@/pages/agencies/ui/EditAgecy"; +import Login from "@/pages/auth/ui/Login"; import Bookings from "@/pages/bookings/ui/Bookings"; import Employees from "@/pages/employees/ui/Employees"; import Faq from "@/pages/faq/ui/Faq"; @@ -23,31 +25,56 @@ import CreateEditTour from "@/pages/tours/ui/CreateEditTour"; import TourDetail from "@/pages/tours/ui/TourDetail"; import Tours from "@/pages/tours/ui/Tours"; import ToursSetting from "@/pages/tours/ui/ToursSetting"; -import CreateUser from "@/pages/users/Create"; -import EditUser from "@/pages/users/Edit"; -import UserList from "@/pages/users/User"; -import UserDetail from "@/pages/users/UserDetail"; +import CreateUser from "@/pages/users/ui/Create"; +import EditUser from "@/pages/users/ui/Edit"; +import UserList from "@/pages/users/ui/User"; +import UserDetail from "@/pages/users/ui/UserDetail"; import MainProvider from "@/providers/main"; import "@/shared/config/i18n"; +import useUserStore from "@/shared/hooks/user"; +import { getAuthToken } from "@/shared/lib/authCookies"; import { Sidebar } from "@/widgets/sidebar/ui/Sidebar"; -import { Navigate, Route, Routes } from "react-router-dom"; +import { useEffect } from "react"; +import { + Navigate, + Route, + Routes, + useLocation, + useNavigate, +} from "react-router-dom"; const App = () => { - const userRole = "admin"; + const { user } = useUserStore(); + const token = getAuthToken(); + const navigate = useNavigate(); + const location = useLocation(); + + const hideSidebarPaths = ["/login"]; + const shouldShowSidebar = !hideSidebarPaths.includes(location.pathname); + + useEffect(() => { + if (token && user) { + navigate("/user"); + } else if (!token && !user) { + navigate("/login"); + } + }, [token, user]); return (
- - - } /> + {shouldShowSidebar && } + + } /> } /> + } /> } /> } /> } /> } /> } /> + } /> } /> } /> } /> @@ -70,9 +97,6 @@ const App = () => { } /> } /> } /> - {/* } /> - } /> - } /> */}
diff --git a/src/ProtectedRoute.tsx b/src/ProtectedRoute.tsx new file mode 100644 index 0000000..179bee8 --- /dev/null +++ b/src/ProtectedRoute.tsx @@ -0,0 +1,36 @@ +import useUserStore from "@/shared/hooks/user"; +import { removeAuthToken, removeRefAuthToken } from "@/shared/lib/authCookies"; +import React from "react"; +import { Navigate } from "react-router-dom"; +import { toast } from "sonner"; + +const allowedRoles = [ + "superuser", + "admin", + "moderator", + "tour_admin", + "buxgalter", + "operator", +]; + +const ProtectedRoute = ({ children }: { children: React.ReactNode }) => { + const { user } = useUserStore(); + + if (!user) { + return ; + } + + if (!allowedRoles.includes(user.role)) { + toast.error("Kirishga huquq yo‘q!", { + richColors: true, + position: "top-center", + }); + removeAuthToken(); + removeRefAuthToken(); + return ; + } + + return <>{children}; +}; + +export default ProtectedRoute; diff --git a/src/main.tsx b/src/main.tsx index a721344..61622dd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,3 +1,4 @@ +import { Toaster } from "@/shared/ui/sonner.tsx"; import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; @@ -8,6 +9,7 @@ createRoot(document.getElementById("root")!).render( + , ); diff --git a/src/pages/agencies/AgencyDetail.tsx b/src/pages/agencies/AgencyDetail.tsx deleted file mode 100644 index 4457642..0000000 --- a/src/pages/agencies/AgencyDetail.tsx +++ /dev/null @@ -1,240 +0,0 @@ -"use client"; - -import { Badge } from "@/shared/ui/badge"; -import { Button } from "@/shared/ui/button"; -import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card"; -import { - ArrowLeft, - ChevronRight, - DollarSign, - Package, - Percent, - TrendingUp, -} from "lucide-react"; -import { useEffect, useState } from "react"; -import { Link, useNavigate, useParams } from "react-router-dom"; - -type Tour = { - id: number; - name: string; - description: string; - sold: number; - profit: number; - status: "faol" | "nofaol"; -}; - -type Agency = { - id: number; - name: string; - owner: string; - status: "faol" | "nofaol"; - profitPercent: number; - totalTours: number; - soldTours: number; - totalProfit: number; - tours: Tour[]; -}; - -export default function AgencyDetailPage() { - const params = useParams(); - const router = useNavigate(); - const [agency, setAgency] = useState(null); - - useEffect(() => { - // Mock data - replace with actual API call - setAgency({ - id: Number(params.id), - name: "Silk Road Travel", - owner: "Ali Karimov", - status: "faol", - profitPercent: 15, - totalTours: 12, - soldTours: 56, - totalProfit: 8900000, - tours: [ - { - id: 1, - name: "Dubai Tour", - description: "7 kunlik hashamatli sayohat", - sold: 23, - profit: 3450000, - status: "faol", - }, - { - id: 2, - name: "Bali Adventure", - description: "10 kunlik ekzotik sayohat", - sold: 33, - profit: 5450000, - status: "faol", - }, - { - id: 3, - name: "Istanbul Express", - description: "5 kunlik madaniy sayohat", - sold: 0, - profit: 0, - status: "nofaol", - }, - ], - }); - }, [params.id]); - - if (!agency) { - return ( -
-

Yuklanmoqda...

-
- ); - } - - return ( -
-
- {/* Header */} -
-
- -
-

{agency.name}

-

Egasi: {agency.owner}

-
-
- - {agency.status === "faol" ? "Faol" : "No-faol"} - -
- - {/* Stats Grid */} -
- {/* Total Tours */} - - - - Jami turlar - - - - -
- {agency.totalTours} -
-

- Qo'shilgan turlar soni -

-
-
- - {/* Sold Tours */} - - - - Sotilgan turlar - - - - -
- {agency.soldTours} -
-

Jami sotilgan turlar

-
-
- - {/* Profit Percent */} - - - - Ulush foizi - - - - -
- {agency.profitPercent}% -
-

Har bir sotuvdan

-
-
- - {/* Total Profit */} - - - - Umumiy daromad - - - - -
- {(agency.totalProfit / 1000).toLocaleString()} -
-

so'm daromad

-
-
-
- - {/* Tours List */} - - - - Qo'shilgan turlar - -

- Firma tomonidan qo'shilgan barcha turlar ro'yxati -

-
- -
- {agency.tours.map((tour) => ( - -
-
-
-
-

- {tour.name} -

-
-

- {tour.description} -

-
-
- - Sotilgan: - - {tour.sold} ta - -
-
- - Daromad: - - {(tour.profit / 1000).toLocaleString()} so'm - -
-
-
- -
-
- - ))} -
-
-
-
-
- ); -} diff --git a/src/pages/agencies/lib/api.ts b/src/pages/agencies/lib/api.ts new file mode 100644 index 0000000..5e06cbb --- /dev/null +++ b/src/pages/agencies/lib/api.ts @@ -0,0 +1,58 @@ +import type { + GetAllAgencyData, + GetDetailAgencyData, +} from "@/pages/agencies/lib/type"; +import httpClient from "@/shared/config/api/httpClient"; +import { GET_ALL_AGENCY } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +const getAllAgency = async ({ + page, + page_size, +}: { + page: number; + page_size: number; +}): Promise> => { + const response = await httpClient.get(GET_ALL_AGENCY, { + params: { page, page_size }, + }); + return response; +}; + +const getDetailAgency = async ({ + id, +}: { + id: number; +}): Promise> => { + const response = await httpClient.get(`${GET_ALL_AGENCY}${id}/`); + return response; +}; + +const deleteAgency = async ({ id }: { id: number }) => { + const response = await httpClient.delete(`${GET_ALL_AGENCY}${id}/`); + return response; +}; + +const updateAgencyStatus = async ({ + id, + body, +}: { + id: number; + body: { + status: "pending" | "approved" | "cancelled"; + custom_id?: string; + share_percentage?: number; + name?: string; + addres?: string; + email?: string; + phone?: string; + web_site?: string; + ticket_sold_count?: number; + total_income?: number; + }; +}) => { + const response = await httpClient.patch(`${GET_ALL_AGENCY}${id}/`, body); + return response; +}; + +export { deleteAgency, getAllAgency, getDetailAgency, updateAgencyStatus }; diff --git a/src/pages/agencies/lib/type.ts b/src/pages/agencies/lib/type.ts new file mode 100644 index 0000000..57e0aea --- /dev/null +++ b/src/pages/agencies/lib/type.ts @@ -0,0 +1,58 @@ +export interface GetAllAgencyData { + status: boolean; + data: { + current_page: number; + links: { + next: string; + previous: string; + }; + total_items: number; + total_pages: number; + page_size: number; + results: { + activate_tour_agency: number; + all_tickets: number; + all_tour_agency: number; + total_income: number; + list: { + id: number; + custom_id: string; + name: string; + owner_user: string; + share_percentage: number; + status: "pending" | "approved" | "cancelled"; + ticket_sold_count: number; + total_income: number; + tour_count: number; + }[]; + }; + }; +} + +export interface GetDetailAgencyData { + status: boolean; + data: { + status: "pending" | "approved" | "cancelled"; + custom_id: string; + share_percentage: number; + name: string; + addres: string; + email: string; + phone: string; + web_site: string; + owner_user: string; + tour_count: string; + ticket_sold_count: number; + total_income: number; + tickets: [ + { + id: number; + title: string; + departure: string; + destination: string; + sold_count: number; + total_income: number; + }, + ]; + }; +} diff --git a/src/pages/agencies/Agencies.tsx b/src/pages/agencies/ui/Agencies.tsx similarity index 52% rename from src/pages/agencies/Agencies.tsx rename to src/pages/agencies/ui/Agencies.tsx index d4d3c94..97133fa 100644 --- a/src/pages/agencies/Agencies.tsx +++ b/src/pages/agencies/ui/Agencies.tsx @@ -1,110 +1,91 @@ +import { deleteAgency, getAllAgency } from "@/pages/agencies/lib/api"; +import formatPrice from "@/shared/lib/formatPrice"; +import { Button } from "@/shared/ui/button"; import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/shared/ui/dialog"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { + AlertTriangle, Building2, ChevronLeft, ChevronRight, Eye, + Loader2, Package, + Pencil, + Trash2, TrendingUp, UserIcon, } from "lucide-react"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; - -type Agency = { - id: number; - name: string; - owner: string; - status: "faol" | "nofaol"; - profitPercent: number; - totalTours: number; - soldTours: number; - totalProfit: number; -}; +import { toast } from "sonner"; export default function TourAgenciesPage() { - const [agencies, setAgencies] = useState([ - { - id: 1, - name: "Silk Road Travel", - owner: "Ali Karimov", - status: "faol", - profitPercent: 15, - totalTours: 12, - soldTours: 56, - totalProfit: 8900000, - }, - { - id: 2, - name: "UzTour Plus", - owner: "Madina Qodirova", - status: "nofaol", - profitPercent: 10, - totalTours: 5, - soldTours: 0, - totalProfit: 0, - }, - { - id: 3, - name: "Orient Express", - owner: "Sardor Rahimov", - status: "faol", - profitPercent: 12, - totalTours: 8, - soldTours: 34, - totalProfit: 5600000, - }, - { - id: 4, - name: "Golden Voyage", - owner: "Dilnoza Azimova", - status: "faol", - profitPercent: 18, - totalTours: 15, - soldTours: 89, - totalProfit: 12400000, - }, - { - id: 5, - name: "SkyLine Tours", - owner: "Rustam Qobilov", - status: "nofaol", - profitPercent: 8, - totalTours: 4, - soldTours: 0, - totalProfit: 0, - }, - { - id: 6, - name: "Desert Adventures", - owner: "Kamola Saidova", - status: "faol", - profitPercent: 20, - totalTours: 11, - soldTours: 42, - totalProfit: 6700000, - }, - ]); - + const { t } = useTranslation(); const [currentPage, setCurrentPage] = useState(1); const itemsPerPage = 4; const navigate = useNavigate(); + const [isDialogOpen, setIsDialogOpen] = useState(false); - const handleStatusChange = (id: number, newStatus: "faol" | "nofaol") => { - setAgencies((prev) => - prev.map((a) => (a.id === id ? { ...a, status: newStatus } : a)), - ); + const { data, refetch, isLoading, isError } = useQuery({ + queryKey: ["all_agency", currentPage], + queryFn: () => getAllAgency({ page: currentPage, page_size: itemsPerPage }), + }); + + const { mutate } = useMutation({ + mutationFn: ({ id }: { id: number }) => { + return deleteAgency({ id }); + }, + onSuccess: () => { + refetch(); + setIsDialogOpen(false); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + + const handleDelete = async (id: number) => { + mutate({ id: id }); }; - const totalPages = Math.ceil(agencies.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const paginatedAgencies = agencies.slice( - startIndex, - startIndex + itemsPerPage, - ); + if (isLoading) { + return ( +
+ +

{t("Ma'lumotlar yuklanmoqda...")}

+
+ ); + } - const activeCount = agencies.filter((a) => a.status === "faol").length; - const totalTours = agencies.reduce((sum, a) => sum + a.totalTours, 0); - const totalRevenue = agencies.reduce((sum, a) => sum + a.totalProfit, 0); + if (isError) { + return ( +
+ +

+ {t("Ma'lumotlarni yuklashda xatolik yuz berdi.")} +

+ +
+ ); + } return (
@@ -116,40 +97,57 @@ export default function TourAgenciesPage() {

- Tur firmalari + {t("Tur firmalari")}

- Firmalarni karta ko'rinishida boshqaring va statistikani kuzating + {t( + "Tur firmalaringizni boshqaring va ularning faoliyatini kuzatib boring.", + )}

{/* Stats */} -
+
} gradient="from-blue-600 to-blue-400" shadowColor="blue" /> } gradient="from-green-600 to-emerald-400" shadowColor="green" /> } gradient="from-amber-600 to-yellow-400" shadowColor="amber" /> } gradient="from-purple-600 to-pink-400" shadowColor="purple" @@ -158,21 +156,21 @@ export default function TourAgenciesPage() { {/* Cards Grid */}
- {paginatedAgencies.map((agency) => ( + {data?.data.data.results.list.map((agency) => (
-

{agency.owner}

+

{agency.owner_user}

- {agency.status === "faol" ? "Faol" : "No-faol"} + {agency.status === "pending" + ? t("Kutilmoqda") + : agency.status === "approved" + ? t("Faol") + : agency.status === "cancelled" && t("Cancelled")}
@@ -203,64 +207,97 @@ export default function TourAgenciesPage() {

- Komissiya + {t("Komissiya")}

- {agency.profitPercent}% + {agency.share_percentage}%

- Jami tur + {t("Jami tur")}

- {agency.totalTours} + {agency.tour_count}

- Sotilgan + {t("Sotilgan tur")}

- {agency.soldTours} + {agency.ticket_sold_count}

- Daromad + {t("Daromad")}

- {(agency.totalProfit / 1_000_000).toFixed(1)}M + {agency.total_income === 0 + ? 0 + : formatPrice(agency.total_income, true)}

{/* Actions */} -
- +
+ + + + + + + + + + {t("Haqiqatan ham o‘chirmoqchimisiz?")} + + + {t( + "Haqiqatan ham bu turni o'chirib tashlamoqchimisiz? Bu amalni ortga qaytarib bo'lmaydi.", + )} + + + + + + + +
@@ -277,7 +314,7 @@ export default function TourAgenciesPage() { - {[...Array(totalPages)].map((_, i) => ( + {[...Array(data?.data.data.total_pages)].map((_, i) => ( +
+ ); + } + + const getStatusBadge = (status: string) => { + switch (status) { + case "pending": + return ( + + {t("Kutilmoqda")} + + ); + case "approved": + return ( + + {t("Faol")} + + ); + case "cancelled": + return ( + + {t("Cancelled")} + + ); + default: + return null; + } + }; + + return ( +
+
+
+
+ +
+
+

+ {data?.data.data.name} +

+

+ {data?.data.data.custom_id} +

+
+

+ {t("Egasi")}: {data?.data.data.owner_user} +

+
+
+ + {/* Status Select */} +
+ {data && getStatusBadge(data?.data.data.status)} + +
+
+ +
+ + + + {t("Jami turlar")} + + + + +
+ {data?.data.data.tour_count} +
+

+ {t("Qo'shilgan turlar soni")} +

+
+
+ + {/* Sold Tours */} + + + + {t("Sotilgan turlar soni")} + + + + +
+ {data?.data.data.ticket_sold_count} +
+

+ {t("Jami sotilgan turlar")} +

+
+
+ + {/* Profit Percent */} + + + + {t("Ulush foizi")} + + + + +
+ {data?.data.data.share_percentage}% +
+

+ {t("Har bir sotuvdan")} +

+
+
+ + {/* Total Profit */} + + + + {t("Umumiy daromad")} + + + + +
+ {data && data.data.data.total_income !== 0 + ? formatPrice(data?.data.data.total_income, true) + : 0} +
+

{t("so'm daromad")}

+
+
+
+ + + + +

{t("Umumiy ma'lumot")}

+ +
+

+ {t("Agentlik haqida batafsil ma'lumot")} +

+
+ +
+ {/* Address */} +
+

{t("Manzil")}

+

+ {data?.data.data.addres || t("Ma'lumot yo'q")} +

+
+ + {/* Email */} +
+

{t("Email")}

+

+ {data?.data.data.email || t("Ma'lumot yo'q")} +

+
+ +
+

{t("Telefon raqami")}

+

+ {data + ? formatPhone(data?.data.data.phone) + : t("Ma'lumot yo'q")} +

+
+ +
+

{t("Veb-sayt")}

+ {data?.data.data.web_site ? ( + + {data.data.data.web_site} + + ) : ( +

+ {t("Ma'lumot yo'q")} +

+ )} +
+ + {/* Custom ID */} +
+

{t("ID raqami")}

+

+ {data?.data.data.custom_id} +

+
+ + {/* Share Percentage */} +
+

{t("Komissiya")}

+

+ {data?.data.data.share_percentage}% +

+
+
+
+
+ + + + {t("Qo'shilgan turlar")} + +

+ {t("Firma tomonidan qo'shilgan barcha turlar ro'yxati")} +

+
+ +
+ {data?.data.data.tickets.map((tour) => ( + +
+
+
+
+

+ {tour.title} +

+
+

+ {tour.destination} +

+
+
+ + + {t("Sotilgan")}: + + + {tour.total_income} + +
+
+ + + {t("Daromad")}: + + + {tour.total_income === 0 + ? 0 + : formatPrice(tour.total_income, true)} + +
+
+
+ +
+
+ + ))} +
+
+
+
+
+ ); +} diff --git a/src/pages/agencies/ui/EditAgecy.tsx b/src/pages/agencies/ui/EditAgecy.tsx new file mode 100644 index 0000000..9dabf41 --- /dev/null +++ b/src/pages/agencies/ui/EditAgecy.tsx @@ -0,0 +1,273 @@ +"use client"; + +import { getDetailAgency, updateAgencyStatus } from "@/pages/agencies/lib/api"; +import formatPhone from "@/shared/lib/formatPhone"; +import { Button } from "@/shared/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/shared/ui/card"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/shared/ui/form"; +import { Input } from "@/shared/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/ui/select"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Loader2 } from "lucide-react"; +import { useEffect } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useParams } from "react-router-dom"; +import { toast } from "sonner"; +import { z } from "zod"; + +const formSchema = z.object({ + status: z.enum(["pending", "approved", "cancelled"]), + share_percentage: z + .number({ message: "Share percentage raqam bo‘lishi kerak" }) + .min(0) + .max(100), + name: z.string().min(1, "Nom kiritish shart"), + addres: z.string().min(1, "Manzil kiritish shart"), + email: z.string().email("Email noto‘g‘ri"), + phone: z.string().min(3, "Telefon raqami noto‘g‘ri"), + web_site: z.string().url("URL noto‘g‘ri"), +}); + +type FormData = z.infer; + +const EditAgency = () => { + const params = useParams(); + const { t } = useTranslation(); + const router = useNavigate(); + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + status: "pending", + share_percentage: 0, + name: "", + addres: "", + email: "", + phone: "+998", + web_site: "", + }, + }); + const queryClient = useQueryClient(); + + const { data, isPending } = useQuery({ + queryKey: ["detail_agency", params.id], + queryFn: () => getDetailAgency({ id: Number(params.id) }), + }); + + const { mutate } = useMutation({ + mutationFn: (body: { + status: "pending" | "approved" | "cancelled"; + custom_id?: string; + share_percentage?: number; + name?: string; + addres?: string; + email?: string; + phone?: string; + web_site?: string; + ticket_sold_count?: number; + total_income?: number; + }) => + updateAgencyStatus({ + id: Number(params.id), + body, + }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["detail_agency"] }); + queryClient.refetchQueries({ queryKey: ["all_agency"] }); + router(-1); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + + useEffect(() => { + if (data?.data?.data) { + const agency = data.data.data; + form.setValue("status", agency.status); + form.setValue("share_percentage", agency.share_percentage); + form.setValue("name", agency.name); + form.setValue("addres", agency.addres); + form.setValue("email", agency.email); + form.setValue("phone", agency.phone); + form.setValue("web_site", agency.web_site); + } + }, [data, form]); + + const onSubmit = (values: FormData) => { + mutate({ + status: values.status, + share_percentage: values.share_percentage, + name: values.name, + addres: values.addres, + email: values.email, + phone: values.phone, + web_site: values.web_site, + }); + }; + + return ( + + + + {t("Tahrirlash")} + + + +
+ + ( + + {t("Komissiya")} + + field.onChange(Number(e.target.value))} + /> + + + + )} + /> + ( + + {t("Nomi")} + + + + + + )} + /> + ( + + {t("Manzil")} + + + + + + )} + /> + ( + + {t("Email")} + + + + + + )} + /> + ( + + {t("Telefon raqam")} + + + + + + )} + /> + ( + + {t("Veb-sayt")} + + + + + + )} + /> + + ( + + {t("Status")} + + + + )} + /> + + + + +
+
+ ); +}; + +export default EditAgency; diff --git a/src/pages/auth/lib/form.ts b/src/pages/auth/lib/form.ts new file mode 100644 index 0000000..8391787 --- /dev/null +++ b/src/pages/auth/lib/form.ts @@ -0,0 +1,8 @@ +import z from "zod"; + +export const formSchema = z.object({ + phone: z.string().min(17, { message: "To'liq telefon raqamini kiriting" }), + password: z + .string() + .min(4, { message: "Parol kamida 4 ta belgidan iborat bo'lishi kerak" }), +}); diff --git a/src/pages/auth/ui/Login.tsx b/src/pages/auth/ui/Login.tsx new file mode 100644 index 0000000..e07e060 --- /dev/null +++ b/src/pages/auth/ui/Login.tsx @@ -0,0 +1,181 @@ +"use client"; + +import { formSchema } from "@/pages/auth/lib/form"; +import { authLogin, getMe } from "@/shared/config/api/auth/api"; +import useUserStore from "@/shared/hooks/user"; +import { + getAuthToken, + setAuthRefToken, + setAuthToken, +} from "@/shared/lib/authCookies"; +import formatPhone from "@/shared/lib/formatPhone"; +import onlyNumber from "@/shared/lib/onlyNumber"; +import { Button } from "@/shared/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/shared/ui/form"; +import { Input } from "@/shared/ui/input"; +import LangToggle from "@/widgets/lang-toggle/ui/lang-toggle"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Eye, EyeOff, LoaderCircle } from "lucide-react"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; +import { z } from "zod"; + +const Login = () => { + const [showPassword, setShowPassword] = useState(false); + const { setUser } = useUserStore(); + const queryClient = useQueryClient(); + const navigate = useNavigate(); + const token = getAuthToken(); + + const { mutate, isPending } = useMutation({ + mutationFn: ({ password, phone }: { password: string; phone: string }) => + authLogin({ + password, + phone, + }), + onSuccess: (res) => { + setAuthToken(res.data.access); + setAuthRefToken(res.data.refresh); + queryClient.invalidateQueries({ queryKey: ["auth_get_me"] }); + }, + onError: () => { + toast.error("Xatolik yuz berdi", { + richColors: true, + position: "top-center", + }); + }, + }); + + const { data } = useQuery({ + queryKey: ["auth_get_me"], + queryFn: () => getMe(), + enabled: !!token, + }); + + useEffect(() => { + if (data) { + setUser(data.data.data); + } + if (data && data.data.data.role !== "user") { + navigate("/"); + } + }, [data]); + + const { t } = useTranslation(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + password: "", + phone: "", + }, + }); + + function onSubmit(values: z.infer) { + mutate({ + password: values.password, + phone: onlyNumber(values.phone), + }); + } + + return ( +
+
+ +
+
+

+ {t("Admin Panelga Kirish")} +

+
+ + ( + + + {t("Telefon raqam")} + + + + field.onChange(formatPhone(e.target.value)) + } + maxLength={19} + className="h-[56px] text-lg rounded-xl bg-gray-700 border-gray-600 text-white placeholder-gray-400" + /> + + + + )} + /> + ( + + {t("Parol")} + +
+ + +
+
+ +
+ )} + /> + + + + + +

+ © {new Date().getFullYear()} {t("Admin Panel")} +

+
+
+ ); +}; + +export default Login; diff --git a/src/pages/bookings/ui/Bookings.tsx b/src/pages/bookings/ui/Bookings.tsx index 4e445c0..937136b 100644 --- a/src/pages/bookings/ui/Bookings.tsx +++ b/src/pages/bookings/ui/Bookings.tsx @@ -1,5 +1,6 @@ +import formatPrice from "@/shared/lib/formatPrice"; import { Eye } from "lucide-react"; -import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; type Booking = { @@ -20,8 +21,8 @@ const initialBookings: Booking[] = [ tourName: "Ichan Qala - Xiva", agentName: "Xiva Tours", destination: "Xiva", - totalAmount: 1200, - paidAmount: 1200, + totalAmount: 1200000, + paidAmount: 1200000, status: "Paid", }, { @@ -30,8 +31,8 @@ const initialBookings: Booking[] = [ tourName: "Samarqandning Qadimiy Go'zalligi", agentName: "Samarqand Travel", destination: "Samarqand", - totalAmount: 1500, - paidAmount: 800, + totalAmount: 1500000, + paidAmount: 800000, status: "Partial", }, { @@ -40,8 +41,8 @@ const initialBookings: Booking[] = [ tourName: "Tog'li Chimyon Sayohati", agentName: "Toshkent Explorer", destination: "Toshkent V.", - totalAmount: 1000, - paidAmount: 0, + totalAmount: 1000000, + paidAmount: 400000, status: "Pending", }, ]; @@ -60,19 +61,14 @@ const getStatusColor = (status: string) => { }; const BookingsPanel = () => { - const [bookings, setBookings] = useState(initialBookings); - - const handleStatusChange = (id: number, newStatus: Booking["status"]) => { - setBookings((prev) => - prev.map((b) => (b.id === id ? { ...b, status: newStatus } : b)), - ); - }; + const { t } = useTranslation(); + const bookings = initialBookings; return (

- Bronlar Paneli + {t("Bronlar Paneli")}

@@ -81,22 +77,22 @@ const BookingsPanel = () => { - User + {t("Foydalanuvchi")} - Tour (Agent) + {t("Tour (Agent)")} - Destination + {t("Destination")} - Total / Paid + {t("Total / Paid")} - Status + {t("Status")} - Ko'rish + {t("Ko'rish")} @@ -119,30 +115,22 @@ const BookingsPanel = () => { {booking.destination} - ${booking.paidAmount} / ${booking.totalAmount} + {formatPrice(booking.paidAmount, true)} /{" "} + {formatPrice(booking.totalAmount, true)} - + {booking.status} +

diff --git a/src/pages/employees/lib/api.ts b/src/pages/employees/lib/api.ts new file mode 100644 index 0000000..e71145e --- /dev/null +++ b/src/pages/employees/lib/api.ts @@ -0,0 +1,74 @@ +import type { + GetAllEmployeesData, + GetDetailEmployeesData, +} from "@/pages/employees/lib/type"; +import httpClient from "@/shared/config/api/httpClient"; +import { GET_ALL_EMPLOYEES } from "@/shared/config/api/URLs"; +import type { AxiosResponse } from "axios"; + +const getAllEmployees = async ({ + page, + page_size, +}: { + page: number; + page_size: number; +}): Promise> => { + const response = await httpClient.get(GET_ALL_EMPLOYEES, { + params: { page, page_size }, + }); + return response; +}; + +const getDetailEmployees = async ({ + id, +}: { + id: number; +}): Promise> => { + const response = await httpClient.get(`${GET_ALL_EMPLOYEES}${id}/`); + return response; +}; + +const deleteEmployees = async ({ id }: { id: number }) => { + const response = await httpClient.delete(`${GET_ALL_EMPLOYEES}${id}/`); + return response; +}; + +const editEmployees = async ({ + id, + body, +}: { + id: number; + body: { + first_name: string; + last_name: string; + phone: string; + email: string | null; + role: "buxgalter" | "operator"; + }; +}) => { + const response = await httpClient.patch(`${GET_ALL_EMPLOYEES}${id}/`, body); + return response; +}; + +const createEmployees = async ({ + body, +}: { + body: { + first_name: string; + last_name: string; + phone: string; + email: string | null; + role: "buxgalter" | "operator"; + }; +}) => { + const response = await httpClient.post(`${GET_ALL_EMPLOYEES}`, body); + return response; +}; + +export { + createEmployees, + deleteEmployees, + editEmployees, + getAllEmployees, + getDetailEmployees, +}; diff --git a/src/pages/employees/lib/type.ts b/src/pages/employees/lib/type.ts new file mode 100644 index 0000000..03ba815 --- /dev/null +++ b/src/pages/employees/lib/type.ts @@ -0,0 +1,35 @@ +export interface GetAllEmployeesData { + status: boolean; + data: { + links: { + previous: string; + next: string; + }; + total_items: number; + total_pages: number; + page_size: number; + current_page: number; + results: [ + { + id: number; + first_name: string; + last_name: string; + phone: string; + email: string; + role: "buxgalter" | "operator"; + }, + ]; + }; +} + +export interface GetDetailEmployeesData { + status: boolean; + data: { + id: number; + first_name: string; + last_name: string; + phone: string; + email: string | null; + role: "buxgalter" | "operator"; + }; +} diff --git a/src/pages/employees/ui/EditEmployees.tsx b/src/pages/employees/ui/EditEmployees.tsx new file mode 100644 index 0000000..e947e26 --- /dev/null +++ b/src/pages/employees/ui/EditEmployees.tsx @@ -0,0 +1,349 @@ +import { + createEmployees, + editEmployees, + getDetailEmployees, +} from "@/pages/employees/lib/api"; +import formatPhone from "@/shared/lib/formatPhone"; +import onlyNumber from "@/shared/lib/onlyNumber"; +import { + Form, + FormControl, + FormField, + FormItem, + FormMessage, +} from "@/shared/ui/form"; +import { Input } from "@/shared/ui/input"; +import { Label } from "@/shared/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/shared/ui/select"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { Loader, X } from "lucide-react"; +import { useEffect, type Dispatch, type SetStateAction } from "react"; +import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import z from "zod"; + +const roles = ["buxgalter", "operator"] as const; + +// ✅ Conditional schema - create uchun password majburiy, edit uchun yo'q +const createEmployeeSchema = z.object({ + firstname: z.string().min(2, "Kamida 2 ta belgidan iborat bo'lishi kerak"), + lastname: z.string().min(2, "Kamida 2 ta belgidan iborat bo'lishi kerak"), + phone: z.string().min(9, "Telefon raqam noto'g'ri"), + role: z.string().min(1, { message: "Majburiy maydon" }), + password: z + .string() + .min(6, "Parol kamida 6 ta belgidan iborat bo'lishi kerak"), +}); + +const editEmployeeSchema = z.object({ + firstname: z.string().min(2, "Kamida 2 ta belgidan iborat bo'lishi kerak"), + lastname: z.string().min(2, "Kamida 2 ta belgidan iborat bo'lishi kerak"), + phone: z.string().min(9, "Telefon raqam noto'g'ri"), + role: z.string().min(1, { message: "Majburiy maydon" }), +}); + +type CreateEmployeeFormValues = z.infer; +type EditEmployeeFormValues = z.infer; +type EmployeeFormValues = CreateEmployeeFormValues | EditEmployeeFormValues; + +const EditEmployees = ({ + modalMode, + editId, + showModal, + setEditId, + setShowModal, +}: { + modalMode: "add" | "edit"; + showModal: boolean; + setEditId: Dispatch>; + editId: number | null; + setShowModal: Dispatch>; +}) => { + const { t } = useTranslation(); + const queryClient = useQueryClient(); + + // ✅ Dinamik schema - modalMode'ga qarab + const form = useForm({ + resolver: zodResolver( + modalMode === "add" ? createEmployeeSchema : editEmployeeSchema, + ), + defaultValues: { + firstname: "", + lastname: "", + phone: "+998", + role: "", + ...(modalMode === "add" && { password: "" }), + }, + }); + + const { data } = useQuery({ + queryKey: ["detail_employees", editId], + queryFn: () => getDetailEmployees({ id: editId! }), + select(data) { + return data.data.data; + }, + enabled: !!editId, + }); + + useEffect(() => { + if (data && editId) { + form.reset({ + firstname: data.first_name, + lastname: data.last_name, + phone: data.phone, + role: data.role, + }); + } else if (!editId && showModal) { + form.reset({ + firstname: "", + lastname: "", + phone: "+998", + role: "", + ...(modalMode === "add" && { password: "" }), + }); + } + }, [data, editId, showModal, modalMode]); + + const { mutate: create, isPending } = useMutation({ + mutationFn: ({ + body, + }: { + body: { + first_name: string; + last_name: string; + phone: string; + email: string | null; + role: "buxgalter" | "operator"; + password: string; + }; + }) => createEmployees({ body }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["detail_employees"] }); + queryClient.refetchQueries({ queryKey: ["employees"] }); + setShowModal(false); + setEditId(null); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + + const { mutate: edit, isPending: editPengding } = useMutation({ + mutationFn: ({ + body, + id, + }: { + id: number; + body: { + first_name: string; + last_name: string; + phone: string; + email: string | null; + role: "buxgalter" | "operator"; + }; + }) => editEmployees({ id, body }), + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["detail_employees"] }); + queryClient.refetchQueries({ queryKey: ["employees"] }); + setShowModal(false); + setEditId(null); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); + }, + }); + + function onSubmit(values: EmployeeFormValues) { + if (modalMode === "add" && editId === null) { + create({ + body: { + email: null, + first_name: values.firstname, + last_name: values.lastname, + phone: onlyNumber(values.phone), + role: values.role as "buxgalter" | "operator", + password: (values as CreateEmployeeFormValues).password, + }, + }); + } else if (modalMode === "edit" && editId !== null) { + edit({ + id: editId, + body: { + email: null, + first_name: values.firstname, + last_name: values.lastname, + phone: onlyNumber(values.phone), + role: values.role as "buxgalter" | "operator", + }, + }); + } + } + + return ( +
+
+
+

+ {modalMode === "add" + ? t("Xodim qo'shish") + : t("Xodimni tahrirlash")} +

+ +
+ +
+ + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + ( + + + + + + + + )} + /> + + {/* ✅ Password field - faqat "add" modeda ko'rinadi */} + {modalMode === "add" && ( + ( + + + + + + + + )} + /> + )} + + ( + + + + + + )} + /> + +
+ + +
+ + +
+
+ ); +}; + +export default EditEmployees; diff --git a/src/pages/employees/ui/Employees.tsx b/src/pages/employees/ui/Employees.tsx index 2120652..de7cf4d 100644 --- a/src/pages/employees/ui/Employees.tsx +++ b/src/pages/employees/ui/Employees.tsx @@ -1,134 +1,78 @@ +"use client"; + +import { deleteEmployees, getAllEmployees } from "@/pages/employees/lib/api"; +import EditEmployees from "@/pages/employees/ui/EditEmployees"; import formatPhone from "@/shared/lib/formatPhone"; +import { Button } from "@/shared/ui/button"; import { - Form, - FormControl, - FormField, - FormItem, - FormMessage, -} from "@/shared/ui/form"; -import { Input } from "@/shared/ui/input"; -import { Label } from "@/shared/ui/label"; + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/shared/ui/dialog"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/shared/ui/select"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Edit, Phone, Plus, Trash2, X } from "lucide-react"; -import { useEffect, useState } from "react"; -import { useForm } from "react-hook-form"; -import z from "zod"; - -const roles = ["Operator", "Bugalter", "Manager"] as const; - -type Employee = { - id: number; - firstname: string; - lastname: string; - phone: string; - role: "Operator" | "Bugalter" | "Manager"; -}; - -const initialEmployees: Employee[] = [ - { - id: 1, - firstname: "Alisher", - lastname: "Karimov", - phone: "+998901234567", - role: "Operator", - }, - { - id: 2, - firstname: "Nigora", - lastname: "Rahimova", - phone: "+998912345678", - role: "Bugalter", - }, - { - id: 3, - firstname: "Nigora", - lastname: "Rahimova", - phone: "+998912345678", - role: "Manager", - }, -]; - -const employeeSchema = z.object({ - firstname: z.string().min(2, "Kamida 2 ta belgidan iborat bo'lishi kerak"), - lastname: z.string().min(2, "Kamida 2 ta belgidan iborat bo'lishi kerak"), - phone: z.string().min(9, "Telefon raqam noto'g'ri"), - role: z.enum(roles), -}); - -type EmployeeFormValues = z.infer; + ChevronLeft, + ChevronRight, + Edit, + Loader2, + Phone, + Plus, + Trash2, +} from "lucide-react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; const EmployeesManagement = () => { - const form = useForm({ - resolver: zodResolver(employeeSchema), - defaultValues: { - firstname: "", - lastname: "", - phone: "+998", - role: "Bugalter", + const { t } = useTranslation(); + const [currentPage, setCurrentPage] = useState(1); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const queryClient = useQueryClient(); + + const { data, isLoading } = useQuery({ + queryKey: ["employees", currentPage], + queryFn: () => getAllEmployees({ page: currentPage, page_size: 10 }), + select: (data) => data.data.data, + }); + + const { mutate: deleteMutate, isPending: isDeleting } = useMutation({ + mutationFn: ({ id }: { id: number }) => { + return deleteEmployees({ id }); + }, + onSuccess: () => { + queryClient.refetchQueries({ queryKey: ["employees"] }); + setIsDialogOpen(false); + toast.success(t("Xodim o'chirildi"), { + position: "top-center", + richColors: true, + }); + }, + onError: () => { + toast.error(t("Xatolik yuz berdi"), { + position: "top-center", + richColors: true, + }); }, }); - const [employees, setEmployees] = useState(initialEmployees); - const [currentPage, setCurrentPage] = useState(1); const [showModal, setShowModal] = useState(false); - const [modalMode, setModalMode] = useState("add"); - const [selectedEmployee, setSelectedEmployee] = useState( - null, - ); - - useEffect(() => { - if (selectedEmployee) { - form.setValue("firstname", selectedEmployee.firstname); - form.setValue("lastname", selectedEmployee.lastname); - form.setValue("phone", selectedEmployee.phone); - form.setValue("role", selectedEmployee.role); - } - }, [selectedEmployee, form]); - - const itemsPerPage = 6; - const totalPages = Math.ceil(employees.length / itemsPerPage); - const startIndex = (currentPage - 1) * itemsPerPage; - const currentEmployees = employees.slice( - startIndex, - startIndex + itemsPerPage, - ); + const [editId, setEditId] = useState(null); + const [modalMode, setModalMode] = useState<"add" | "edit">("add"); const handleAdd = () => { setModalMode("add"); setShowModal(true); + setEditId(null); }; - const handleEdit = (employee: Employee) => { - setSelectedEmployee(employee); + const handleEdit = (id: number) => { setShowModal(true); setModalMode("edit"); - }; - - const handleDelete = (id: number) => { - if (window.confirm("Ushbu xodimni o'chirishni xohlaysizmi?")) { - setEmployees(employees.filter((emp) => emp.id !== id)); - } - }; - - const onSubmit = (values: EmployeeFormValues) => { - if (modalMode === "add") { - const newEmp: Employee = { ...values, id: Date.now() }; - setEmployees([...employees, newEmp]); - } else if (selectedEmployee) { - setEmployees( - employees.map((emp) => - emp.id === selectedEmployee.id ? { ...emp, ...values } : emp, - ), - ); - } - setShowModal(false); + setEditId(id); }; return ( @@ -137,10 +81,10 @@ const EmployeesManagement = () => {

- Xodimlar + {t("Xodimlar")}

- Jami {employees.length} ta xodim + {t("Jami")} {data?.total_items || 0} {t("ta xodim")}

-
- {currentEmployees.map((employee) => ( -
-
-
-
-

- {employee.firstname} {employee.lastname} -

-

{employee.role}

+ {/* ✅ Loading State */} + {isLoading ? ( +
+ +

{t("Yuklanmoqda")}...

+
+ ) : data?.results && data.results.length > 0 ? ( + // ✅ Data mavjud bo'lsa + <> +
+ {data.results.map((employee) => ( +
+
+
+

+ {employee.first_name} {employee.last_name} +

+

+ {t(employee.role)} +

+
+
+ +
+
+ + + {formatPhone(employee.phone)} + +
+
+ +
+ + + + + + + + + + + {t("Foydalanuvchini o'chirish")} + + + {t("Siz")}{" "} + + {employee.first_name} {employee.last_name} + {" "} + {t("foydalanuvchini o'chirmoqchimisiz?")} +
+ {t("Ushbu amalni qaytarib bo'lmaydi")}. +
+
+ + + + +
+
-
- -
-
- - {formatPhone(employee.phone)} -
-
- -
- - -
+ ))}
- ))} -
-
- - {[...Array(totalPages)].map((_, i) => ( + {/* Pagination */} +
+ + + {[...Array(data?.total_pages || 0)].map((_, i) => ( + + ))} + + +
+ + ) : ( + // ✅ Ma'lumot yo'q bo'lsa +
+
+ +
+

+ {t("Xodimlar topilmadi")} +

+

+ {t("Birinchi xodimni qo'shing")} +

- ))} - - -
+
+ )}
{showModal && ( -
-
-
-

- {modalMode === "add" ? "Xodim qo'shish" : "Xodimni tahrirlash"} -

- -
- -
-
- - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - - - )} - /> - ( - - - - - - )} - /> -
- - -
- - -
-
-
+ )}
); diff --git a/src/pages/faq/ui/Faq.tsx b/src/pages/faq/ui/Faq.tsx index ad42d55..26006a2 100644 --- a/src/pages/faq/ui/Faq.tsx +++ b/src/pages/faq/ui/Faq.tsx @@ -40,6 +40,7 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { Pencil, PlusCircle, Trash2 } from "lucide-react"; import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import z from "zod"; type FaqType = { @@ -102,6 +103,7 @@ const faqForm = z.object({ const Faq = () => { const [faqs, setFaqs] = useState(initialFaqs); const [activeTab, setActiveTab] = useState("umumiy"); + const { t } = useTranslation(); const [openModal, setOpenModal] = useState(false); const [editFaq, setEditFaq] = useState(null); const [deleteId, setDeleteId] = useState(null); @@ -147,7 +149,9 @@ const Faq = () => {
{/* Header */}
-

FAQ (Savol va javoblar)

+

+ {t("FAQ (Savol va javoblar)")} +

@@ -176,10 +180,10 @@ const Faq = () => { # - Savol - Javob + {t("Savol")} + {t("Javob")} - Amallar + {t("Amallar")} @@ -220,7 +224,7 @@ const Faq = () => {
) : (

- Bu bo‘limda savollar yo‘q. + {t("Bu bo‘limda savollar yo‘q.")}

)} @@ -230,7 +234,7 @@ const Faq = () => { - {editFaq ? "FAQni tahrirlash" : "Yangi FAQ qo‘shish"} + {editFaq ? t("FAQni tahrirlash") : t("Yangi FAQ qo‘shish")} @@ -241,18 +245,18 @@ const Faq = () => { name="categories" render={({ field }) => ( - + @@ -288,10 +292,10 @@ const Faq = () => { name="answer" render={({ field }) => ( - +