Initial commit
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
12
README.md
Normal file
12
README.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# React + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and [`typescript-eslint`](https://typescript-eslint.io) in your project.
|
||||
29
eslint.config.js
Normal file
29
eslint.config.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{js,jsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 'latest',
|
||||
ecmaFeatures: { jsx: true },
|
||||
sourceType: 'module',
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { varsIgnorePattern: '^[A-Z_]' }],
|
||||
},
|
||||
},
|
||||
])
|
||||
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
19683
package-lock.json
generated
Normal file
19683
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
46
package.json
Normal file
46
package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "cargo-admin",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview",
|
||||
"start": "set PORT=3080 && react-scripts start"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pbe/react-yandex-maps": "^1.2.5",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.85.5",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"firebase": "^12.0.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-react": "^0.536.0",
|
||||
"react": "^19.0.0-rc.1",
|
||||
"react-dom": "^19.0.0-rc.1",
|
||||
"react-leaflet": "^5.0.0-rc.2",
|
||||
"react-qr-code": "^2.0.18",
|
||||
"react-qr-reader": "^3.0.0-beta-1",
|
||||
"react-router-dom": "^7.7.1",
|
||||
"react-scripts": "^5.0.1",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.1",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react-swc": "^3.10.2",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.20",
|
||||
"globals": "^16.3.0",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
1
public/vite.svg
Normal file
1
public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
151
src/App.jsx
Normal file
151
src/App.jsx
Normal file
@@ -0,0 +1,151 @@
|
||||
// App.js
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import RoleProtectedRoute from './components/RoleProtectedRoute';
|
||||
import Sidebar from './components/Sidebar';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import Branches from './pages/Branches';
|
||||
import Clients from './pages/Clients';
|
||||
import Currency from './pages/Currency';
|
||||
import Employees from './pages/Employees';
|
||||
import Login from './pages/Login';
|
||||
import Payments from './pages/Payments';
|
||||
import Permissions from './pages/Permissions'; // Yangi sahifa
|
||||
import Reference from './pages/Reference';
|
||||
import Warhouses from './pages/Warhouses';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
function AppLayout({ children }) {
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="flex min-h-screen md:ml-[250px]">
|
||||
<Sidebar />
|
||||
<main className="flex-1 p-4 bg-gray-100">{children}</main>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<Routes>
|
||||
{/* Login */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
|
||||
{/* Asosiy sahifa Clients */}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin', 'uzb_worker', 'china_worker']}>
|
||||
<AppLayout>
|
||||
<Clients />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Clients */}
|
||||
<Route
|
||||
path="/clients"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin', 'uzb_worker', 'china_worker']}>
|
||||
<AppLayout>
|
||||
<Clients />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Employees - faqat admin */}
|
||||
<Route
|
||||
path="/employees"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin']}>
|
||||
<AppLayout>
|
||||
<Employees />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Payments */}
|
||||
<Route
|
||||
path="/payments"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin', 'uzb_worker', 'china_worker']}>
|
||||
<AppLayout>
|
||||
<Payments />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Branches - faqat admin */}
|
||||
<Route
|
||||
path="/branches"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin']}>
|
||||
<AppLayout>
|
||||
<Branches />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Currency */}
|
||||
<Route
|
||||
path="/currency"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin', 'uzb_worker', 'china_worker']}>
|
||||
<AppLayout>
|
||||
<Currency />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Permissions - faqat admin */}
|
||||
<Route
|
||||
path="/permissions"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin']}>
|
||||
<AppLayout>
|
||||
<Permissions />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/reference"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin']}>
|
||||
<AppLayout>
|
||||
<Reference />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="/warhouses"
|
||||
element={
|
||||
<RoleProtectedRoute allowedRoles={['admin']}>
|
||||
<AppLayout>
|
||||
<Warhouses />
|
||||
</AppLayout>
|
||||
</RoleProtectedRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Default redirect */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
18
src/api/axiosInstance.jsx
Normal file
18
src/api/axiosInstance.jsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import axios from 'axios';
|
||||
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE_URL,
|
||||
});
|
||||
|
||||
axiosInstance.interceptors.request.use(
|
||||
(config) => {
|
||||
const user = JSON.parse(localStorage.getItem('user'));
|
||||
if (user?.token) {
|
||||
config.headers.Authorization = `Bearer ${user.token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
export default axiosInstance;
|
||||
1
src/assets/react.svg
Normal file
1
src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
45
src/components/Error.tsx
Normal file
45
src/components/Error.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { cn } from '../lib/utils';
|
||||
|
||||
const variantStyles = {
|
||||
error: {
|
||||
container: 'border-red-200 bg-red-50',
|
||||
text: 'text-red-800',
|
||||
icon: 'text-red-600',
|
||||
},
|
||||
success: {
|
||||
container: 'border-green-200 bg-green-50',
|
||||
text: 'text-green-800',
|
||||
icon: 'text-green-600',
|
||||
},
|
||||
info: {
|
||||
container: 'border-blue-200 bg-blue-50',
|
||||
text: 'text-blue-800',
|
||||
icon: 'text-blue-600',
|
||||
},
|
||||
warning: {
|
||||
container: 'border-yellow-200 bg-yellow-50',
|
||||
text: 'text-yellow-800',
|
||||
icon: 'text-yellow-600',
|
||||
},
|
||||
};
|
||||
|
||||
const Alert = ({ variant = 'info', message, Icon }) => {
|
||||
const styles = variantStyles[variant];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
// Position & animation
|
||||
'fixed top-4 left-1/2 -translate-x-1/2 z-50',
|
||||
'animate-slideDown'
|
||||
)}
|
||||
>
|
||||
<div className={cn('flex items-center gap-2 p-3 rounded-lg border shadow-lg min-w-[280px] max-w-md', styles.container)}>
|
||||
{Icon && <Icon className={cn('h-5 w-5', styles.icon)} />}
|
||||
<p className={cn('text-sm', styles.text)}>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Alert;
|
||||
13
src/components/Header.tsx
Normal file
13
src/components/Header.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
const Header = ({ Icon, title, text }) => {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Icon className="h-8 w-8 text-[#3489e3]" />
|
||||
<h1 className="text-3xl font-bold text-slate-900">{title}</h1>
|
||||
</div>
|
||||
<p className="text-slate-600">{text}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
83
src/components/Pagination.tsx
Normal file
83
src/components/Pagination.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronLeft, ChevronRight, MoreHorizontal } from 'lucide-react';
|
||||
|
||||
interface PaginationProps {
|
||||
currentPage: number;
|
||||
totalPages: number;
|
||||
totalElements: number;
|
||||
onPageChange: (page: number) => void;
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
const Pagination = ({ currentPage, totalPages, totalElements, onPageChange, isLoading = false }: PaginationProps) => {
|
||||
const getPageNumbers = () => {
|
||||
const pages: any = [];
|
||||
const maxVisiblePages = 5;
|
||||
|
||||
if (totalPages <= maxVisiblePages) {
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
} else {
|
||||
pages.push(1);
|
||||
|
||||
if (currentPage > 3) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
const start = Math.max(2, currentPage - 1);
|
||||
const end = Math.min(totalPages - 1, currentPage + 1);
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
if (!pages.includes(i)) {
|
||||
pages.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (currentPage < totalPages - 2) {
|
||||
pages.push('...');
|
||||
}
|
||||
|
||||
if (!pages.includes(totalPages)) {
|
||||
pages.push(totalPages);
|
||||
}
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
const pageNumbers = getPageNumbers();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-4 px-4 py-3">
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => onPageChange(currentPage - 1)} disabled={currentPage === 1 || isLoading} className="flex items-center justify-center w-8 h-8 rounded border border-slate-300 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
{pageNumbers.map((page, index) => (
|
||||
<div key={index}>
|
||||
{page === '...' ? (
|
||||
<div className="flex items-center justify-center w-8 h-8">
|
||||
<MoreHorizontal className="h-4 w-4 text-slate-400" />
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => onPageChange(page as number)}
|
||||
disabled={isLoading}
|
||||
className={`flex items-center justify-center w-8 h-8 rounded border text-sm font-medium transition-colors ${currentPage === page ? 'bg-[#3489e3] text-white border-[#3489e3]' : 'border-slate-300 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed'}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button onClick={() => onPageChange(currentPage + 1)} disabled={currentPage === totalPages || isLoading} className="flex items-center justify-center w-8 h-8 rounded border border-slate-300 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors">
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Pagination;
|
||||
17
src/components/RoleProtectedRoute.jsx
Normal file
17
src/components/RoleProtectedRoute.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { Navigate } from 'react-router-dom';
|
||||
|
||||
const RoleProtectedRoute = ({ children, allowedRoles }) => {
|
||||
const { user } = useAuth();
|
||||
|
||||
if (!user) return <Navigate to="/login" replace />;
|
||||
|
||||
const userRoleLower = user.role.toLowerCase();
|
||||
const allowedRolesLower = allowedRoles.map(role => role.toLowerCase());
|
||||
|
||||
if (!allowedRolesLower.includes(userRoleLower)) return <Navigate to="/login" replace />;
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default RoleProtectedRoute;
|
||||
17
src/components/Searchs.tsx
Normal file
17
src/components/Searchs.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
const Searchs = ({ searchTerm, loading, setSearchTerm, placeholder = 'Ism, telefon yoki manzil bo‘yicha qidirish...' }) => {
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
Qidiruv
|
||||
</label>
|
||||
<div className="relative max-w-md">
|
||||
<input type="text" id="search" placeholder={placeholder} value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} className="w-full border border-slate-300 rounded-lg pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={loading} />
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Searchs;
|
||||
113
src/components/Sidebar.jsx
Normal file
113
src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,113 @@
|
||||
// Sidebar.js
|
||||
import { CreditCard, DollarSign, LogOut, Map as MapIcon, Menu, Users, Warehouse, X } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const Sidebar = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const location = useLocation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isDesktop, setIsDesktop] = useState(window.innerWidth >= 768);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
const desktop = window.innerWidth >= 768;
|
||||
setIsDesktop(desktop);
|
||||
setIsOpen(desktop);
|
||||
};
|
||||
window.addEventListener('resize', handleResize);
|
||||
handleResize();
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
// Role ni normalizatsiya qilamiz
|
||||
const role = user.role.toLowerCase().replace('-', '_');
|
||||
|
||||
// Yangi rollar taqsimoti
|
||||
// super_admin -> endi admin huquqlariga ega
|
||||
// admin -> endi china_worker va uzb_worker sifatida ishlaydi
|
||||
const isAdmin = role === 'admin'; // eski super_admin
|
||||
const isWorker = role === 'china_worker' || role === 'uzb_worker'; // eski admin
|
||||
const isChinaWorker = role === 'china_worker';
|
||||
const isUzbWorker = role === 'uzb_worker';
|
||||
|
||||
const toggleSidebar = () => setIsOpen(!isOpen);
|
||||
const closeSidebar = () => {
|
||||
if (!isDesktop) setIsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile hamburger */}
|
||||
{!isDesktop && (
|
||||
<button className="fixed top-4 right-4 z-50 p-2 rounded-md bg-[#3489e3] text-white shadow-md" onClick={toggleSidebar} aria-label="Toggle sidebar">
|
||||
{isOpen ? <X size={24} /> : <Menu size={24} />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Mobile overlay */}
|
||||
{!isDesktop && isOpen && <div onClick={closeSidebar} className="fixed inset-0 bg-black/40 z-40" />}
|
||||
|
||||
{/* Sidebar */}
|
||||
<aside
|
||||
key={location.pathname} // path o'zgarganda qayta render bo'lishi uchun
|
||||
className={`fixed top-0 left-0 bottom-0 min-w-64 bg-[#3489e3] text-white flex flex-col justify-between
|
||||
overflow-y-auto
|
||||
transform transition-transform duration-300 ease-in-out z-50
|
||||
${isOpen ? 'translate-x-0' : '-translate-x-full'}
|
||||
md:translate-x-0 md:fixed
|
||||
`}
|
||||
>
|
||||
<nav className="space-y-2 p-4">
|
||||
{isAdmin && <SidebarLink to="/" label="Mijozlar" icon={<Users size={18} />} onClick={closeSidebar} />}
|
||||
|
||||
{/* Xodimlar faqat admin (eski super_admin) */}
|
||||
{isAdmin && <SidebarLink to="/employees" label="Xodimlar ro'yhati" icon={<Users size={18} />} onClick={closeSidebar} />}
|
||||
|
||||
{/* To'lovlar */}
|
||||
<SidebarLink to="/payments" label="To'lovlar" icon={<CreditCard size={18} />} onClick={closeSidebar} />
|
||||
|
||||
{/* Filiallar faqat admin (eski super_admin) */}
|
||||
{isAdmin && <SidebarLink to="/branches" label="Filiallar" icon={<MapIcon size={18} />} onClick={closeSidebar} />}
|
||||
|
||||
{/* Valyuta admin va workerlar */}
|
||||
{isAdmin && <SidebarLink to="/currency" label="Valyuta" icon={<CreditCard size={18} />} onClick={closeSidebar} />}
|
||||
|
||||
{isAdmin && <SidebarLink to="/reference" label="Cargo narxi" icon={<DollarSign size={18} />} onClick={closeSidebar} />}
|
||||
|
||||
{isAdmin && <SidebarLink to="/warhouses" label="Omborlar" icon={<Warehouse size={18} />} onClick={closeSidebar} />}
|
||||
</nav>
|
||||
|
||||
{/* Logout */}
|
||||
<div className="p-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
logout();
|
||||
closeSidebar();
|
||||
}}
|
||||
className="flex items-center gap-2 bg-white/20 hover:bg-white/30 text-white px-3 py-2 rounded"
|
||||
>
|
||||
<LogOut size={18} /> Chiqish
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const SidebarLink = ({ to, label, icon, onClick }) => (
|
||||
<NavLink
|
||||
to={to}
|
||||
end // faqat to'liq path mos kelsa active bo'lishi uchun
|
||||
onClick={onClick}
|
||||
className={({ isActive }) => `flex items-center gap-2 px-3 py-2 rounded ${isActive ? 'bg-white text-[#3489e3]' : 'hover:bg-blue-400'}`}
|
||||
>
|
||||
{icon}
|
||||
<span>{label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
|
||||
export default Sidebar;
|
||||
85
src/components/branches/BranchesList.tsx
Normal file
85
src/components/branches/BranchesList.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Edit, Trash2 } from 'lucide-react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
const BranchesList = ({ setBranches, setAlert }) => {
|
||||
const refQuery = useQueryClient();
|
||||
const { data: branches } = useQuery({
|
||||
queryKey: ['branch_list'],
|
||||
queryFn: () => {
|
||||
return axiosInstance.get('/branches');
|
||||
},
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteBranches } = useMutation({
|
||||
mutationFn: async (id: number) => {
|
||||
await axiosInstance.delete(`/branches/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Filial muvaffaqiyatli o'chirildi." });
|
||||
refQuery.invalidateQueries({ queryKey: ['branch_list'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Filial o'chirilmadi, xatolik yuz berdi." });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto bg-white shadow rounded-lg">
|
||||
<table className="min-w-full text-left text-sm">
|
||||
<thead className="bg-slate-100 border-b border-slate-300">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-semibold">ID</th>
|
||||
<th className="px-4 py-3 font-semibold">Nomi (UZ)</th>
|
||||
<th className="px-4 py-3 font-semibold">Telefon</th>
|
||||
<th className="px-4 py-3 font-semibold">Manzil</th>
|
||||
<th className="px-4 py-3 font-semibold">Ish vaqti</th>
|
||||
<th className="px-4 py-3 font-semibold">Telegram Admin</th>
|
||||
<th className="px-4 py-3 font-semibold">Kod</th>
|
||||
<th className="px-4 py-3 font-semibold">Holati</th>
|
||||
<th className="px-4 py-3 font-semibold">Amallar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{branches &&
|
||||
branches.map((b) => {
|
||||
return (
|
||||
<tr key={b.id} className="border-b border-slate-200 hover:bg-slate-50 transition-colors">
|
||||
<td className="px-4 py-3 font-mono">{b.id}</td>
|
||||
<td className="px-4 py-3">{b.title}</td>
|
||||
<td className="px-4 py-3">{b.phone}</td>
|
||||
<td className="px-4 py-3">{b.address}</td>
|
||||
<td className="px-4 py-3">{b.workingHours}</td>
|
||||
<td className="px-4 py-3">{b.telegramAdmin}</td>
|
||||
<td className="px-4 py-3">{b.code}</td>
|
||||
<td className="px-4 py-3">{b.active ? <span className="text-green-600 font-semibold">Faol</span> : <span className="text-red-600 font-semibold">Faol emas</span>}</td>
|
||||
<td className="px-4 py-3 flex gap-2">
|
||||
<button onClick={() => setBranches(b.id)} className="flex items-center gap-1 border border-yellow-300 text-yellow-700 hover:bg-yellow-50 px-3 py-1 rounded-lg" title="Tahrirlash">
|
||||
<Edit className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Tahrirlash</span>
|
||||
</button>
|
||||
<button onClick={() => deleteBranches(b.id)} className="flex items-center gap-1 border border-red-300 text-red-700 hover:bg-red-50 px-3 py-1 rounded-lg" title="O‘chirish">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">O‘chirish</span>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{branches && branches.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={9} className="text-center py-6 text-slate-500 font-medium">
|
||||
Filiallar mavjud emas.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BranchesList;
|
||||
282
src/components/branches/CreateBranches.jsx
Normal file
282
src/components/branches/CreateBranches.jsx
Normal file
@@ -0,0 +1,282 @@
|
||||
'use client';
|
||||
|
||||
import { Map, Placemark, YMaps } from '@pbe/react-yandex-maps';
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
// Reverse geocoding (OSM orqali)
|
||||
async function getAddress(lat, lon) {
|
||||
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=json`);
|
||||
const data = await response.json();
|
||||
const address = data.address || {};
|
||||
|
||||
return {
|
||||
fullAddress: data.display_name || '',
|
||||
country: address.country || '',
|
||||
region: address.state || '',
|
||||
city: address.city || address.town || address.village || '',
|
||||
street: address.road || '',
|
||||
house: address.house_number || '',
|
||||
postalCode: address.postcode || '',
|
||||
};
|
||||
}
|
||||
|
||||
const CreateBranches = ({ branches, setAlert, setBranches }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [formData, setFormData] = useState({
|
||||
id: '',
|
||||
title: '',
|
||||
titleRu: '',
|
||||
address: '',
|
||||
addressRu: '',
|
||||
workingHours: '',
|
||||
workingHoursRu: '',
|
||||
phone: '',
|
||||
telegramAdmin: '',
|
||||
telegramChannel: '',
|
||||
code: '',
|
||||
active: true,
|
||||
});
|
||||
|
||||
const [coords, setCoords] = useState({
|
||||
latitude: 41.311081, // Toshkent
|
||||
longitude: 69.240562,
|
||||
});
|
||||
|
||||
const [locationDetails, setLocationDetails] = useState({
|
||||
fullAddress: '',
|
||||
country: '',
|
||||
region: '',
|
||||
city: '',
|
||||
street: '',
|
||||
house: '',
|
||||
postalCode: '',
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
id: '',
|
||||
title: '',
|
||||
titleRu: '',
|
||||
address: '',
|
||||
addressRu: '',
|
||||
workingHours: '',
|
||||
workingHoursRu: '',
|
||||
phone: '',
|
||||
telegramAdmin: '',
|
||||
telegramChannel: '',
|
||||
code: '',
|
||||
active: true,
|
||||
});
|
||||
setCoords({
|
||||
latitude: 41.311081,
|
||||
longitude: 69.240562,
|
||||
});
|
||||
setLocationDetails({
|
||||
fullAddress: '',
|
||||
country: '',
|
||||
region: '',
|
||||
city: '',
|
||||
street: '',
|
||||
house: '',
|
||||
postalCode: '',
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (locationDetails.fullAddress) {
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
address: locationDetails.fullAddress,
|
||||
addressRu: locationDetails.fullAddress,
|
||||
}));
|
||||
}
|
||||
}, [locationDetails]);
|
||||
|
||||
const { data: branchData, refetch } = useQuery({
|
||||
queryKey: ['one_branch', branches],
|
||||
queryFn: () => axiosInstance.get(`/branches/${branches}`).then((res) => res.data),
|
||||
enabled: !!branches,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (branches) {
|
||||
refetch();
|
||||
}
|
||||
}, [branches, refetch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (branchData) {
|
||||
setFormData({
|
||||
id: branchData.id || '',
|
||||
title: branchData.title || '',
|
||||
titleRu: branchData.titleRu || '',
|
||||
address: branchData.address || '',
|
||||
addressRu: branchData.addressRu || '',
|
||||
workingHours: branchData.workingHours || '',
|
||||
workingHoursRu: branchData.workingHoursRu || '',
|
||||
phone: branchData.phone || '',
|
||||
telegramAdmin: branchData.telegramAdmin || '',
|
||||
telegramChannel: branchData.telegramChannel || '',
|
||||
code: branchData.code || '',
|
||||
active: branchData.active ?? true,
|
||||
});
|
||||
setCoords({
|
||||
latitude: branchData.latitude || 41.311081,
|
||||
longitude: branchData.longitude || 69.240562,
|
||||
});
|
||||
}
|
||||
}, [branchData]);
|
||||
|
||||
const { mutate: createBranch } = useMutation({
|
||||
mutationFn: (body) => axiosInstance.post('/branches', body),
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Filial muvaffaqiyatli qo'shildi." });
|
||||
queryClient.invalidateQueries({ queryKey: ['branch_list'] });
|
||||
resetForm();
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({
|
||||
type: 'warning',
|
||||
message: "Filial qo'shilmadi, xatolik yuz berdi.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: editBranch } = useMutation({
|
||||
mutationFn: (body) => axiosInstance.put(`/branches/${body.id}`, body),
|
||||
onSuccess: () => {
|
||||
setBranches(null);
|
||||
setAlert({
|
||||
type: 'success',
|
||||
message: 'Filial muvaffaqiyatli yangilandi.',
|
||||
});
|
||||
queryClient.invalidateQueries({ queryKey: ['branch_list'] });
|
||||
resetForm();
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({
|
||||
type: 'warning',
|
||||
message: 'Filial yangilanmadi, xatolik yuz berdi.',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const addBranch = () => {
|
||||
const { id, ...dataWithoutId } = formData;
|
||||
createBranch({
|
||||
...dataWithoutId,
|
||||
latitude: Number(coords.latitude) || 0,
|
||||
longitude: Number(coords.longitude) || 0,
|
||||
});
|
||||
};
|
||||
|
||||
const updateBranch = (id) => {
|
||||
editBranch({
|
||||
...formData,
|
||||
id,
|
||||
latitude: Number(coords.latitude) || 0,
|
||||
longitude: Number(coords.longitude) || 0,
|
||||
});
|
||||
};
|
||||
|
||||
const handleMapClick = async (e) => {
|
||||
const lat = e.get('coords')[0];
|
||||
const lon = e.get('coords')[1];
|
||||
setCoords({ latitude: lat, longitude: lon });
|
||||
const details = await getAddress(lat, lon);
|
||||
setLocationDetails(details);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 mb-6">
|
||||
<div className="h-96 mb-10">
|
||||
<YMaps query={{ lang: 'en_RU' }}>
|
||||
<Map
|
||||
defaultState={{
|
||||
center: [coords.latitude, coords.longitude],
|
||||
zoom: 13,
|
||||
}}
|
||||
width="100%"
|
||||
height="400px"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<Placemark geometry={[coords.latitude, coords.longitude]} />
|
||||
</Map>
|
||||
</YMaps>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{Object.keys(formData)
|
||||
.filter((key) => key !== 'id')
|
||||
.map((key) => (
|
||||
<div key={key} className="flex flex-col">
|
||||
<label className="mb-1 font-semibold text-slate-700">
|
||||
{key === 'title'
|
||||
? 'Filial nomi'
|
||||
: key === 'titleRu'
|
||||
? 'Filial nomi (RU)'
|
||||
: key === 'address'
|
||||
? 'Manzil'
|
||||
: key === 'addressRu'
|
||||
? 'Manzil (RU)'
|
||||
: key === 'workingHours'
|
||||
? 'Ish vaqti'
|
||||
: key === 'workingHoursRu'
|
||||
? 'Ish vaqti (RU)'
|
||||
: key === 'phone'
|
||||
? 'Telefon'
|
||||
: key === 'telegramAdmin'
|
||||
? 'Telegram Admin'
|
||||
: key === 'telegramChannel'
|
||||
? 'Telegram Kanal'
|
||||
: key === 'code'
|
||||
? 'Kod'
|
||||
: key === 'active'
|
||||
? 'Faol'
|
||||
: key}
|
||||
</label>
|
||||
{key === 'active' ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" onClick={() => setFormData({ ...formData, active: !formData.active })} className={`w-14 h-8 flex items-center rounded-full p-1 transition-colors duration-300 ${formData.active ? 'bg-[#3489e3]' : 'bg-gray-300'}`}>
|
||||
<div className={`w-6 h-6 bg-white rounded-full shadow-md transform transition-transform duration-300 ${formData.active ? 'translate-x-6' : 'translate-x-0'}`}></div>
|
||||
</button>
|
||||
<span>{formData.active ? 'Faol' : 'Faol emas'}</span>
|
||||
</div>
|
||||
) : (
|
||||
<input
|
||||
type="text"
|
||||
className="border border-slate-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]"
|
||||
value={formData[key]}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mb-8 mt-6">
|
||||
{!branches && (
|
||||
<button onClick={addBranch} className="bg-[#3489e3] hover:bg-blue-500 text-white px-5 py-2 rounded-lg shadow">
|
||||
Yaratish
|
||||
</button>
|
||||
)}
|
||||
{branches && (
|
||||
<button onClick={() => updateBranch(formData.id)} disabled={!formData.id} className={`px-5 py-2 rounded-lg shadow text-white ${!formData.id ? 'bg-gray-400 cursor-not-allowed' : 'bg-[#3489e3] hover:bg-blue-500'}`}>
|
||||
Yangilash
|
||||
</button>
|
||||
)}
|
||||
<button onClick={resetForm} className="bg-gray-400 hover:bg-gray-500 text-white px-5 py-2 rounded-lg shadow">
|
||||
Tozalash
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateBranches;
|
||||
125
src/components/clients/ClientList.tsx
Normal file
125
src/components/clients/ClientList.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Edit, Trash2, Users } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
import Searchs from '../../components/Searchs';
|
||||
import Pagination from '../Pagination';
|
||||
|
||||
const ClientList = ({ alert, setAlert, setClients, setCargoId }) => {
|
||||
const client = useQueryClient();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const userRole = (() => {
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
return storedUser ? JSON.parse(storedUser).user.role : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['clients'],
|
||||
queryFn: () =>
|
||||
axiosInstance.get('/clients/list', {
|
||||
params: {
|
||||
page: currentPage,
|
||||
sort: 'id',
|
||||
clientName: searchTerm,
|
||||
direction: 'desc',
|
||||
},
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refetch();
|
||||
}, [searchTerm, currentPage]);
|
||||
|
||||
const handleEdit = (client, cargoid) => {
|
||||
setClients(client);
|
||||
setCargoId(cargoid);
|
||||
};
|
||||
|
||||
const { mutate: deleteClient, isPending: deleteLoad } = useMutation({
|
||||
mutationFn: (id: number) => axiosInstance.delete(`/users/delete/${id}`),
|
||||
mutationKey: ['clientsDelete'],
|
||||
onSuccess: () => {
|
||||
client.invalidateQueries({ queryKey: ['clients'] });
|
||||
setAlert({ type: 'success', message: 'Foydalanuvchi muvaffaqiyatli o‘chirildi.' });
|
||||
setClients(null);
|
||||
setCargoId();
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: 'Foydalanuvchini o‘chirishda xatolik yuz berdi.' });
|
||||
},
|
||||
});
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Searchs searchTerm={searchTerm} setSearchTerm={setSearchTerm} loading={isLoading} />
|
||||
<div className="bg-white/80 backdrop-blur-sm shadow-lg rounded-lg overflow-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200">
|
||||
<h3 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<Users className="h-5 w-5 text-[#3489e3]" /> Mijozlar ro'yxati ({data?.totalElements})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm table-auto min-w-max">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left p-4">ID</th>
|
||||
<th className="text-left p-4">To'liq ism</th>
|
||||
<th className="text-left p-4">Telefon</th>
|
||||
<th className="text-left p-4">Manzil</th>
|
||||
<th className="text-left p-4">Passport seriyasi</th>
|
||||
<th className="text-left p-4">PNFL</th>
|
||||
<th className="text-left p-4">Tug'ilgan sana</th>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && <th className="text-left p-4">Amallar</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{data &&
|
||||
data.data.map((client) => (
|
||||
<tr key={client.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4 font-mono">{client.id}</td>
|
||||
<td className="p-4">{client.fullName}</td>
|
||||
<td className="p-4">{client.phone}</td>
|
||||
<td className="p-4 max-w-3xs">{client.address}</td>
|
||||
<td className="p-4">{client.passportSeries}</td>
|
||||
<td className="p-4">{client.pinfl}</td>
|
||||
<td className="p-4">{client.dateOfBirth}</td>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && (
|
||||
<td className="p-4">
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => handleEdit(client.id, client.aviaCargoId)} className="flex items-center gap-1 border border-yellow-300 text-yellow-700 hover:bg-yellow-50 px-3 py-1 rounded-lg" disabled={isLoading}>
|
||||
<Edit className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Tahrirlash</span>
|
||||
</button>
|
||||
<button onClick={() => deleteClient(client.id)} className="flex items-center gap-1 border border-red-300 text-red-700 hover:bg-red-50 px-3 py-1 rounded-lg" disabled={deleteLoad}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">O'chirish</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end mt-5 justify-end">
|
||||
<Pagination currentPage={currentPage} totalPages={data?.totalPages || 1} totalElements={data?.totalElements || 0} onPageChange={handlePageChange} isLoading={isLoading} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClientList;
|
||||
277
src/components/clients/CreateClient.tsx
Normal file
277
src/components/clients/CreateClient.tsx
Normal file
@@ -0,0 +1,277 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { UserPlus, X } from 'lucide-react';
|
||||
import { ChangeEvent, FormEvent, useEffect, useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
import PassportCarousel from './PassportCarousel';
|
||||
|
||||
// Props type
|
||||
interface CreateClientProps {
|
||||
clients: string | null;
|
||||
setAlert: (alert: { type: 'success' | 'warning'; message: string }) => void;
|
||||
cargoId: string;
|
||||
}
|
||||
|
||||
// Form data type
|
||||
interface FormData {
|
||||
fullName: string;
|
||||
phone: string;
|
||||
address: string;
|
||||
passportSeries: string;
|
||||
pinfl: string;
|
||||
branchId?: number;
|
||||
dateOfBirth: string;
|
||||
}
|
||||
|
||||
interface PassportData {
|
||||
id: string;
|
||||
fullName: string;
|
||||
passportFrontImage: string;
|
||||
passportBackImage: string;
|
||||
passportSeries: string;
|
||||
passportPin: string;
|
||||
birthDate: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
const CreateClient = ({ clients, setAlert, cargoId }: CreateClientProps) => {
|
||||
const refQuery = useQueryClient();
|
||||
|
||||
const formatDate = (dateStr: string, format: 'yyyy-mm-dd' = 'yyyy-mm-dd') => {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
|
||||
const yyyy = date.getFullYear();
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(date.getDate()).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const [tagetIamge, setTargetImage] = useState<string | null>(null);
|
||||
const [opneModal, setOpenModal] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
fullName: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
passportSeries: '',
|
||||
pinfl: '',
|
||||
dateOfBirth: '',
|
||||
});
|
||||
|
||||
const [currentPassport, setCurrentPassport] = useState<PassportData | null>(null);
|
||||
const [active, setActive] = useState(false);
|
||||
|
||||
const userRole: string | null = (() => {
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
return storedUser ? JSON.parse(storedUser).user.role : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const { data: client, refetch: clientRef } = useQuery({
|
||||
queryKey: ['client_one'],
|
||||
queryFn: async () => axiosInstance.get('/clients/list', { params: { cargoId } }),
|
||||
select: (data) => data.data.data as { data: any[] },
|
||||
});
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['clients_one'],
|
||||
queryFn: async () => axiosInstance.get(`/clients/${clients}/passports`),
|
||||
select: (data) => data.data.data as PassportData[],
|
||||
enabled: !!clients,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (client && cargoId && client.data[0]) {
|
||||
const c = client.data[0];
|
||||
|
||||
setFormData({
|
||||
address: c.address || '',
|
||||
dateOfBirth: formatDate(c.dateOfBirth),
|
||||
fullName: c.fullName,
|
||||
passportSeries: c.passportSeries,
|
||||
phone: c.phone,
|
||||
pinfl: c.pinfl,
|
||||
});
|
||||
}
|
||||
clientRef();
|
||||
refetch();
|
||||
}, [cargoId, client, refetch, clientRef]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (currentPassport) {
|
||||
// setFormData((prev) => ({
|
||||
// ...prev,
|
||||
// fullName: currentPassport.fullName || '',
|
||||
// passportSeries: currentPassport.passportSeries || '',
|
||||
// pinfl: currentPassport.passportPin || '',
|
||||
// dateOfBirth: formatDate(currentPassport.birthDate),
|
||||
// }));
|
||||
// setActive(currentPassport.active);
|
||||
// }
|
||||
// }, [currentPassport]);
|
||||
|
||||
const { mutate: createMutate } = useMutation({
|
||||
mutationFn: async (params: any) => {
|
||||
return axiosInstance.post('/clients/create', { ...params, cargoId });
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Mijoz muvaffaqiyatli qo'shildi." });
|
||||
refQuery.invalidateQueries({ queryKey: ['clients_one', 'clients'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Mijoz qo'shilmadi, xatolik yuz berdi." });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateMutate } = useMutation({
|
||||
mutationFn: async (params: { id: string } & FormData) => {
|
||||
return axiosInstance.put(`/clients/edit/${params.id}`, params);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: 'Foydalanuvchi muvaffaqiyatli tahrirlandi.' });
|
||||
refQuery.invalidateQueries({ queryKey: ['clients_one', 'clients'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: 'Foydalanuvchi tahrirlanmadi, xatolik yuz berdi.' });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: userMutate } = useMutation({
|
||||
mutationFn: async (params: { id: string; active: boolean } & FormData) => {
|
||||
return axiosInstance.put(`/users/update/${params.id}`, {
|
||||
...params,
|
||||
role: 'CLIENT',
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: 'Foydalanuvchi muvaffaqiyatli tahrirlandi.' });
|
||||
refQuery.invalidateQueries({ queryKey: ['clients_one', 'clients'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: 'Foydalanuvchi tahrirlanmadi, xatolik yuz berdi.' });
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const onSubmit = (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (clients) {
|
||||
updateMutate({ id: clients, ...formData });
|
||||
} else {
|
||||
createMutate({
|
||||
fullName: formData.fullName,
|
||||
phone: formData.phone,
|
||||
address: formData.address || 'Nomalum',
|
||||
passport: formData.passportSeries,
|
||||
pinfl: formData.pinfl,
|
||||
dateOfBirth: formData.dateOfBirth,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleActive = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const checked = e.target.checked;
|
||||
setActive(checked);
|
||||
|
||||
if (clients) {
|
||||
userMutate({
|
||||
id: clients,
|
||||
active: checked,
|
||||
branchId: 17,
|
||||
...formData,
|
||||
});
|
||||
}
|
||||
console.log(formData);
|
||||
};
|
||||
|
||||
const handlePassportUpdate = (passportId: string, updatedData: Partial<FormData>) => {
|
||||
setAlert({ type: 'success', message: "Passport ma'lumotlari yangilandi." });
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && (
|
||||
<div className="mb-8 bg-white/80 backdrop-blur-sm shadow-lg rounded-lg p-6">
|
||||
<h2 className="flex items-center gap-2 text-lg font-semibold mb-4">
|
||||
<UserPlus className="h-5 w-5 text-[#3489e3]" />
|
||||
{clients ? 'Mijozni tahrirlash' : "Yangi mijoz qo'shish"}
|
||||
</h2>
|
||||
|
||||
{clients && data && data.length > 0 && <PassportCarousel passports={data} onUpdate={handlePassportUpdate} setTargetImage={setTargetImage} setOpenModal={setOpenModal} setCurrentPassport={setCurrentPassport} />}
|
||||
|
||||
<form className="space-y-6" onSubmit={onSubmit}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mt-10">
|
||||
{[
|
||||
{ label: "To'liq ism", name: 'fullName', type: 'text' },
|
||||
{ label: 'Telefon', name: 'phone', type: 'text' },
|
||||
{ label: 'Manzil', name: 'address', type: 'text' },
|
||||
{ label: 'Passport seriyasi', name: 'passportSeries', type: 'text' },
|
||||
{ label: 'PNFL', name: 'pinfl', type: 'text' },
|
||||
{ label: "Tug'ilgan sana", name: 'dateOfBirth', type: 'date' },
|
||||
].map((field) => (
|
||||
<div key={field.name}>
|
||||
<label className="block text-sm font-medium mb-1">{field.label}</label>
|
||||
<input type={field.type} name={field.name} value={formData[field.name as keyof FormData]} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" />
|
||||
</div>
|
||||
))}
|
||||
|
||||
{clients && currentPassport && (
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input type="checkbox" className="sr-only peer" checked={active} onChange={handleActive} />
|
||||
<div className="w-11 h-6 bg-gray-300 rounded-full peer peer-checked:bg-blue-600 peer-focus:ring-2 peer-focus:ring-blue-400 transition-colors duration-300"></div>
|
||||
<div className="absolute left-1 top-1 w-4 h-4 bg-white rounded-full transition-transform duration-300 peer-checked:translate-x-5"></div>
|
||||
</label>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-4">
|
||||
<button type="submit" className="bg-[#3489e3] hover:bg-blue-500 text-white px-6 py-2 rounded-lg disabled:opacity-60">
|
||||
{clients ? 'Yangilash' : "Qo'shish"}
|
||||
</button>
|
||||
<button type="button" className="border border-slate-300 text-slate-700 hover:bg-slate-50 px-6 py-2 rounded-lg">
|
||||
Bekor qilish
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)}
|
||||
{tagetIamge && opneModal && <ModalIamge image={tagetIamge} setTargetImage={setTargetImage} setOpenModal={setOpenModal} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ModalIamge = ({ image, setTargetImage, setOpenModal }: { image: string; setTargetImage: (img: string | null) => void; setOpenModal: (open: boolean) => void }) => {
|
||||
return (
|
||||
<div className="w-full h-screen bg-black/60 fixed top-0 bottom-0 left-0 z-50 overflow-hidden">
|
||||
<button
|
||||
onClick={() => {
|
||||
setTargetImage(null);
|
||||
setOpenModal(false);
|
||||
}}
|
||||
>
|
||||
<X className="text-white absolute right-6 size-8" />
|
||||
</button>
|
||||
<div className="w-full h-full justify-center flex items-center">
|
||||
<div className="w-[700px] aspect-[16/9]">
|
||||
<img src={image || '/placeholder.svg'} className="w-full h-full object-contain" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateClient;
|
||||
106
src/components/clients/PassportCarousel.tsx
Normal file
106
src/components/clients/PassportCarousel.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface PassportData {
|
||||
id: string;
|
||||
fullName: string;
|
||||
passportFrontImage: string;
|
||||
passportBackImage: string;
|
||||
passportSeries: string;
|
||||
passportPin: string;
|
||||
birthDate: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
interface PassportCarouselProps {
|
||||
passports: PassportData[];
|
||||
onUpdate: (id: string, data: Partial<PassportData>) => void;
|
||||
setTargetImage: (image: string) => void;
|
||||
setOpenModal: (open: boolean) => void;
|
||||
setCurrentPassport: (passport: PassportData) => void;
|
||||
}
|
||||
|
||||
const PassportCarousel = ({ passports, onUpdate, setTargetImage, setOpenModal, setCurrentPassport }: PassportCarouselProps) => {
|
||||
const [currentIndex, setCurrentIndex] = useState(0);
|
||||
|
||||
const currentPassport = passports[currentIndex];
|
||||
|
||||
useEffect(() => {
|
||||
if (currentPassport) {
|
||||
setCurrentPassport(currentPassport);
|
||||
}
|
||||
}, [currentIndex, currentPassport, setCurrentPassport]);
|
||||
|
||||
const nextSlide = () => {
|
||||
setCurrentIndex((prev) => (prev + 1) % passports.length);
|
||||
};
|
||||
|
||||
const prevSlide = () => {
|
||||
setCurrentIndex((prev) => (prev - 1 + passports.length) % passports.length);
|
||||
};
|
||||
|
||||
if (!passports || passports.length === 0) {
|
||||
return <div className="text-center text-gray-500">Passport ma'lumotlari topilmadi</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold">Passport ma'lumotlari</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">
|
||||
{currentIndex + 1} / {passports.length}
|
||||
</span>
|
||||
{passports.length > 1 && (
|
||||
<div className="flex gap-1">
|
||||
<button onClick={prevSlide} className="p-2 rounded-full hover:bg-gray-100 transition-colors" disabled={passports.length <= 1}>
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
</button>
|
||||
<button onClick={nextSlide} className="p-2 rounded-full hover:bg-gray-100 transition-colors" disabled={passports.length <= 1}>
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentPassport && (
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setTargetImage(currentPassport.passportFrontImage);
|
||||
setOpenModal(true);
|
||||
}}
|
||||
className="group relative overflow-hidden rounded-lg border-2 border-gray-200 hover:border-blue-300 transition-colors"
|
||||
>
|
||||
<h4 className="text-sm font-medium text-left p-2 bg-gray-50">Passport old tomoni</h4>
|
||||
<div className="aspect-[3/2] overflow-hidden">
|
||||
<img src={currentPassport.passportFrontImage || '/placeholder.svg'} alt="Passport front" className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-200" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setTargetImage(currentPassport.passportBackImage);
|
||||
setOpenModal(true);
|
||||
}}
|
||||
className="group relative overflow-hidden rounded-lg border-2 border-gray-200 hover:border-blue-300 transition-colors"
|
||||
>
|
||||
<h4 className="text-sm font-medium text-left p-2 bg-gray-50">Passport orqa tomoni</h4>
|
||||
<div className="aspect-[3/2] overflow-hidden">
|
||||
<img src={currentPassport.passportBackImage || '/placeholder.svg'} alt="Passport back" className="w-full h-full object-contain group-hover:scale-105 transition-transform duration-200" />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PassportCarousel;
|
||||
159
src/components/currency/CreateCurrency.jsx
Normal file
159
src/components/currency/CreateCurrency.jsx
Normal file
@@ -0,0 +1,159 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
const CreateCurrency = ({ setAlert, editingCurrency }) => {
|
||||
const refQuery = useQueryClient();
|
||||
const [formData, setFormData] = useState({
|
||||
code: '',
|
||||
nominal: '',
|
||||
rate: '',
|
||||
date: '',
|
||||
});
|
||||
|
||||
const { data } = useQuery({
|
||||
queryKey: ['currency_one'],
|
||||
queryFn: async () => {
|
||||
return await axiosInstance.get(`exchanges/${editingCurrency}`);
|
||||
},
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
enabled: !!editingCurrency,
|
||||
});
|
||||
console.log(data);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setFormData({
|
||||
code: data.code,
|
||||
nominal: data.nominal,
|
||||
rate: data.rate,
|
||||
date: data.date ? data.date.slice(0, 10) : '',
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const { mutate: createCurrency, isPending: createCurrencyLoadr } = useMutation({
|
||||
mutationFn: async (params) => {
|
||||
return axiosInstance.post('/exchanges', {
|
||||
...params,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Valyuta muvaffaqiyatli qo'shildi." });
|
||||
resetForm();
|
||||
refQuery.invalidateQueries({ queryKey: ['currency_list'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Valyuta qo'shishda xatolik yuz berdi." });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateCurrency } = useMutation({
|
||||
mutationFn: async (params) => {
|
||||
return axiosInstance.put(`/exchanges/${editingCurrency}`, {
|
||||
...params,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Valyuta muvaffaqiyatli qo'shildi." });
|
||||
resetForm();
|
||||
refQuery.invalidateQueries({ queryKey: ['currency_list'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Valyuta qo'shishda xatolik yuz berdi." });
|
||||
},
|
||||
});
|
||||
|
||||
const handleAdd = async (e) => {
|
||||
e.preventDefault();
|
||||
createCurrency({
|
||||
code: formData.code,
|
||||
nominal: 1,
|
||||
rate: formData.rate,
|
||||
date: new Date(formData.date).toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
code: '',
|
||||
nominal: '',
|
||||
rate: '',
|
||||
date: '',
|
||||
});
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleUpdate = (e) => {
|
||||
e.preventDefault();
|
||||
updateCurrency({
|
||||
code: formData.code,
|
||||
nominal: 1,
|
||||
rate: formData.rate,
|
||||
date: new Date(formData.date).toISOString(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-8 bg-white/80 backdrop-blur-sm shadow-lg rounded-lg p-6">
|
||||
<h2 className="flex items-center gap-2 text-lg font-semibold mb-4">{editingCurrency !== null ? 'Valyutani tahrirlash' : "Yangi valyuta qo'shish"}</h2>
|
||||
<form className="space-y-6" onSubmit={editingCurrency ? handleUpdate : handleAdd}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<label htmlFor="code" className="block text-sm font-medium mb-1">
|
||||
Code (USD, EUR...)
|
||||
</label>
|
||||
<input type="text" name="code" id="code" value={formData.code} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={createCurrencyLoadr} required maxLength={5} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="nominal" className="block text-sm font-medium mb-1">
|
||||
Nominal
|
||||
</label>
|
||||
<input type="number" name="nominal" id="nominal" value={1} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={true} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="rate" className="block text-sm font-medium mb-1">
|
||||
Rate (kurs)
|
||||
</label>
|
||||
<input type="number" step="0.0001" name="rate" id="rate" value={formData.rate} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={createCurrencyLoadr} required min={0} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="date" className="block text-sm font-medium mb-1">
|
||||
Date
|
||||
</label>
|
||||
<input type="date" name="date" id="date" value={formData.date} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={createCurrencyLoadr} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-4">
|
||||
<button type="submit" className="bg-[#3489e3] hover:bg-blue-500 text-white px-6 py-2 rounded-lg disabled:opacity-60" disabled={createCurrencyLoadr}>
|
||||
{editingCurrency !== null ? 'Yangilash' : "Qo'shish"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
resetForm();
|
||||
setEditingCurrency(null);
|
||||
setError('');
|
||||
}}
|
||||
className="border border-slate-300 text-slate-700 hover:bg-slate-50 px-6 py-2 rounded-lg"
|
||||
disabled={createCurrencyLoadr}
|
||||
>
|
||||
Bekor qilish
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateCurrency;
|
||||
86
src/components/currency/CurrencyList.jsx
Normal file
86
src/components/currency/CurrencyList.jsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Edit, PlusCircle, Trash2 } from 'lucide-react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
const CurrencyList = ({ setAlert, setEditingCurrency }) => {
|
||||
const refQuery = useQueryClient();
|
||||
const { data: filteredCurrencies, isLoading } = useQuery({
|
||||
queryKey: ['currency_list'],
|
||||
queryFn: async () => {
|
||||
return await axiosInstance.get('/exchanges');
|
||||
},
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteCurrency } = useMutation({
|
||||
mutationFn: (id) => {
|
||||
return axiosInstance.delete(`/exchanges/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Valyuta muvaffaqiyatli o'chirildi." });
|
||||
refQuery.invalidateQueries({ queryKey: ['currency_list'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Valyuta o'chirishda xatolik yuz berdi." });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-white/80 backdrop-blur-sm shadow-lg rounded-lg overflow-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200">
|
||||
<h3 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<PlusCircle className="h-5 w-5 text-[#3489e3]" /> Valyutalar ro'yxati ({filteredCurrencies && filteredCurrencies.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm table-auto min-w-max">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left p-4">ID</th>
|
||||
<th className="text-left p-4">Code</th>
|
||||
<th className="text-left p-4">Nominal</th>
|
||||
<th className="text-left p-4">Kurs (Rate)</th>
|
||||
<th className="text-left p-4">Sana (Date)</th>
|
||||
<th className="text-left p-4">Amallar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{filteredCurrencies &&
|
||||
filteredCurrencies.map((currency) => (
|
||||
<tr key={currency.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4 font-mono">{currency.id}</td>
|
||||
<td className="p-4 font-semibold">{currency.code}</td>
|
||||
<td className="p-4">{currency.nominal}</td>
|
||||
<td className="p-4">{currency.rate}</td>
|
||||
<td className="p-4">{currency.date ? currency.date.slice(0, 10) : ''}</td>
|
||||
<td className="p-4">
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setEditingCurrency(currency.id)} className="flex items-center gap-1 border border-yellow-300 text-yellow-700 hover:bg-yellow-50 px-3 py-1 rounded-lg" disabled={isLoading} title="Tahrirlash">
|
||||
<Edit className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Tahrirlash</span>
|
||||
</button>
|
||||
<button onClick={() => deleteCurrency(currency.id)} className="flex items-center gap-1 border border-red-300 text-red-700 hover:bg-red-50 px-3 py-1 rounded-lg" disabled={isLoading} title="O'chirish">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">O'chirish</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredCurrencies && filteredCurrencies.length === 0 && !isLoading && (
|
||||
<tr>
|
||||
<td colSpan={6} className="text-center p-4 text-slate-500">
|
||||
Hozircha valyutalar yo‘q yoki qidiruv natijasida topilmadi.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrencyList;
|
||||
184
src/components/employes/CreateEmployee.tsx
Normal file
184
src/components/employes/CreateEmployee.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { UserPlus } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
const CreateEmployee = ({ clients, setAlert, cargoId }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const refQuery = useQueryClient();
|
||||
console.log(clients);
|
||||
|
||||
const [error, setError] = useState('');
|
||||
const userRole = (() => {
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
if (!storedUser) return '';
|
||||
const parsedUser = JSON.parse(storedUser);
|
||||
const role = parsedUser?.user?.role || '';
|
||||
console.log('User Role:', role);
|
||||
return role.toUpperCase();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})();
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
username: '',
|
||||
password: '',
|
||||
fullName: '',
|
||||
role: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
cargoType: 'AVIA',
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (clients) {
|
||||
setFormData({
|
||||
address: clients.address,
|
||||
cargoType: clients.cargoType,
|
||||
fullName: clients.fullName,
|
||||
password: '',
|
||||
phone: clients.phone,
|
||||
role: clients.role,
|
||||
username: clients.username,
|
||||
});
|
||||
}
|
||||
}, [clients]);
|
||||
|
||||
const { mutate: createEmployee } = useMutation({
|
||||
mutationFn: async (params: any) => {
|
||||
return axiosInstance.post('/users/create', { ...params });
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Mijoz muvaffaqiyatli qo'shildi." });
|
||||
refQuery.invalidateQueries({ queryKey: ['employee_list'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Mijoz qo'shilmadi, xatolik yuz berdi." });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateMutate } = useMutation({
|
||||
mutationFn: async (params: any) => {
|
||||
return axiosInstance.put(`/users/update/${params.id}`, {
|
||||
...params,
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: 'Foydalanuvchi muvaffaqiyatli tahrirlandi.' });
|
||||
refQuery.invalidateQueries({ queryKey: ['clients_one', 'clients'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: 'Foydalanuvchi tahrirlanmadi, xatolik yuz berdi.' });
|
||||
},
|
||||
});
|
||||
|
||||
const handleAdd = async (e) => {
|
||||
e.preventDefault();
|
||||
if (clients) {
|
||||
updateMutate({
|
||||
id: clients.id,
|
||||
fullName: formData.fullName,
|
||||
phone: formData.phone,
|
||||
address: formData.address,
|
||||
role: formData.role,
|
||||
cargoType: formData.cargoType,
|
||||
branchId: 17,
|
||||
});
|
||||
} else {
|
||||
createEmployee({
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
fullName: formData.fullName,
|
||||
role: formData.role,
|
||||
phone: formData.phone,
|
||||
address: formData.address,
|
||||
cargoType: formData.cargoType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
username: '',
|
||||
password: '',
|
||||
fullName: '',
|
||||
role: '',
|
||||
phone: '',
|
||||
address: '',
|
||||
cargoType: '',
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{userRole === 'ADMIN' ? (
|
||||
<div className="mb-8 bg-white/80 backdrop-blur-sm shadow-lg rounded-lg p-6">
|
||||
<h2 className="flex items-center gap-2 text-lg font-semibold mb-4">
|
||||
<UserPlus className="h-5 w-5 text-[#3489e3]" />
|
||||
Yangi xodim qo'shish
|
||||
</h2>
|
||||
|
||||
<form className="space-y-6" onSubmit={handleAdd}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Username</label>
|
||||
<input type="text" name="username" value={formData.username} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={loading} required autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Parol</label>
|
||||
<input type="password" name="password" value={formData.password} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={loading} required autoComplete="new-password" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Cargo Type</label>
|
||||
<select name="cargoType" value={formData.cargoType} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={loading} required>
|
||||
<option value="AVIA">AVIA</option>
|
||||
<option value="AUTO">AUTO</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">To'liq ism</label>
|
||||
<input type="text" name="fullName" value={formData.fullName} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={loading} required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Rol</label>
|
||||
<select name="role" value={formData.role} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={loading} required>
|
||||
<option value="">Rolni tanlang</option>
|
||||
<option value="UZB_WORKER">UZB_WORKER</option>
|
||||
<option value="CHINA_WORKER">CHINA_WORKER</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Telefon</label>
|
||||
<input type="text" name="phone" value={formData.phone} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={loading} required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Manzil</label>
|
||||
<input type="text" name="address" value={formData.address} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" disabled={loading} required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-4">
|
||||
<button type="submit" className="bg-[#3489e3] hover:bg-blue-500 text-white px-6 py-2 rounded-lg disabled:opacity-60" disabled={loading}>
|
||||
{clients ? 'Yangilash' : "Qo'shish"}
|
||||
</button>
|
||||
<button type="button" onClick={resetForm} className="border border-slate-300 text-slate-700 hover:bg-slate-50 px-6 py-2 rounded-lg" disabled={loading}>
|
||||
Tozalash
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mb-6 text-red-500 font-semibold">Sizda yangi xodim qo‘shish huquqi yo‘q.</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateEmployee;
|
||||
124
src/components/employes/EmployesList.tsx
Normal file
124
src/components/employes/EmployesList.tsx
Normal file
@@ -0,0 +1,124 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Edit, Trash2, Users } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
import Pagination from '../Pagination';
|
||||
|
||||
const EmployesList = ({ alert, setAlert, setClients, setCargoId }) => {
|
||||
const query = useQueryClient();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const { data: filteredEmployees, isLoading } = useQuery({
|
||||
queryKey: ['employee_list'],
|
||||
queryFn: () => {
|
||||
return axiosInstance.get('/users/list', {
|
||||
params: {
|
||||
page: currentPage,
|
||||
sort: 'id',
|
||||
// clientName: searchTerm,
|
||||
direction: 'desc',
|
||||
},
|
||||
});
|
||||
},
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const userRole = (() => {
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
return storedUser ? JSON.parse(storedUser).user.role : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const { mutate: deleteClient } = useMutation({
|
||||
mutationFn: (id: number) => axiosInstance.delete(`/users/delete/${id}`),
|
||||
mutationKey: ['clientsDelete'],
|
||||
onSuccess: () => {
|
||||
query.invalidateQueries({ queryKey: ['employee_list'] });
|
||||
setAlert({ type: 'success', message: 'Foydalanuvchi muvaffaqiyatli o‘chirildi.' });
|
||||
setClients(null);
|
||||
setCargoId();
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: 'Foydalanuvchini o‘chirishda xatolik yuz berdi.' });
|
||||
},
|
||||
});
|
||||
|
||||
const handleEdit = (client) => {
|
||||
setClients(client);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white/80 backdrop-blur-sm shadow-lg rounded-lg overflow-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200">
|
||||
<h3 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<Users className="h-5 w-5 text-[#3489e3]" /> Xodimlar ro'yxati ({filteredEmployees?.totalElements})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm table-auto min-w-max">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left p-4">ID</th>
|
||||
<th className="text-left p-4">Username</th>
|
||||
<th className="text-left p-4">To'liq ism</th>
|
||||
<th className="text-left p-4">Telefon</th>
|
||||
<th className="text-left p-4">Manzil</th>
|
||||
<th className="text-left p-4">Rol</th>
|
||||
<th className="text-left p-4">Holati</th>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && <th className="text-left p-4">Amallar</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{filteredEmployees &&
|
||||
filteredEmployees.data.map((employee) => (
|
||||
<tr key={employee.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4 font-mono">{employee.id}</td>
|
||||
<td className="p-4">{employee.username}</td>
|
||||
<td className="p-4">{employee.fullName}</td>
|
||||
<td className="p-4">{employee.phone}</td>
|
||||
<td className="p-4">{employee.address}</td>
|
||||
<td className="p-4">{employee.role}</td>
|
||||
<td className="p-4">{employee.active ? <span className="text-green-600 font-semibold">Aktiv</span> : <span className="text-red-600 font-semibold">Bloklangan</span>}</td>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && (
|
||||
<td className="p-4">
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => handleEdit(employee)} className="flex items-center gap-1 border border-yellow-300 text-yellow-700 hover:bg-yellow-50 px-3 py-1 rounded-lg" disabled={isLoading}>
|
||||
<Edit className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Tahrirlash</span>
|
||||
</button>
|
||||
<button onClick={() => deleteClient(employee.id)} className="flex items-center gap-1 border border-red-300 text-red-700 hover:bg-red-50 px-3 py-1 rounded-lg">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">O'chirish</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{filteredEmployees && filteredEmployees.data?.length === 0 && !isLoading && (
|
||||
<tr>
|
||||
<td className="text-center p-4 text-slate-500">Hozircha xodimlar yo‘q yoki qidiruv natijasida topilmadi.</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end mt-5 justify-end">
|
||||
<Pagination currentPage={currentPage} totalPages={filteredEmployees?.totalPages || 1} totalElements={filteredEmployees?.totalElements || 0} onPageChange={handlePageChange} isLoading={isLoading} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmployesList;
|
||||
307
src/components/payments/ModalPayment.jsx
Normal file
307
src/components/payments/ModalPayment.jsx
Normal file
@@ -0,0 +1,307 @@
|
||||
'use client';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { AlertCircle, CheckCircle, Clock, CreditCard, Package, Phone, User, X, XCircle } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
const paymentStatuses = [
|
||||
{
|
||||
value: 'NEW',
|
||||
label: "To'lanmagan",
|
||||
icon: AlertCircle,
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-50',
|
||||
borderColor: 'border-red-200',
|
||||
hoverColor: 'hover:bg-red-100',
|
||||
},
|
||||
{
|
||||
value: 'PENDING',
|
||||
label: 'Kutilmoqda',
|
||||
icon: Clock,
|
||||
color: 'text-yellow-600',
|
||||
bgColor: 'bg-yellow-50',
|
||||
borderColor: 'border-yellow-200',
|
||||
hoverColor: 'hover:bg-yellow-100',
|
||||
},
|
||||
{
|
||||
value: 'PAYED',
|
||||
label: "To'langan",
|
||||
icon: CheckCircle,
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
borderColor: 'border-green-200',
|
||||
hoverColor: 'hover:bg-green-100',
|
||||
},
|
||||
];
|
||||
|
||||
const ModalPayment = ({ isOpen, onClose, packetId }) => {
|
||||
const { data, refetch, isLoading, error } = useQuery({
|
||||
queryKey: ['transactions_info'],
|
||||
queryFn: async () => await axiosInstance.get(`/admin/payments/packet/${packetId}/transactions`),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
enabled: !!packetId,
|
||||
});
|
||||
|
||||
console.log(data);
|
||||
|
||||
useEffect(() => {
|
||||
if (packetId) {
|
||||
refetch();
|
||||
}
|
||||
}, [packetId]);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return 'N/A';
|
||||
return new Date(dateString).toLocaleString('uz-UZ', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
};
|
||||
|
||||
const formatAmount = (amount) => {
|
||||
return new Intl.NumberFormat('uz-UZ').format(amount) + " so'm";
|
||||
};
|
||||
|
||||
const currentStatus = data ? paymentStatuses.find((status) => status.value === data.paymentStatus) : paymentStatuses[0];
|
||||
const CurrentIcon = currentStatus ? currentStatus.icon : paymentStatuses[0];
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[99999] flex items-center justify-center bg-black/50">
|
||||
<div className="absolute inset-0" onClick={onClose} />
|
||||
<div className="relative bg-white rounded-xl shadow-2xl w-[800px] max-w-[95vw] max-h-[90vh] overflow-hidden">
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 className="text-xl font-semibold text-gray-900">To'lov ma'lumotlari</h2>
|
||||
<button onClick={onClose} className="p-2 rounded-full hover:bg-gray-100 transition-colors">
|
||||
<X className="w-5 h-5 text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(90vh-80px)]">
|
||||
{error ? (
|
||||
<div className="flex flex-col items-center justify-center py-12">
|
||||
<XCircle className="w-12 h-12 text-red-400 mb-3" />
|
||||
<p className="text-red-600 text-center">Ma'lumotlarni yuklashda xatolik yuz berdi</p>
|
||||
<button onClick={() => refetch()} className="mt-3 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
|
||||
Qayta urinish
|
||||
</button>
|
||||
</div>
|
||||
) : isLoading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span className="ml-3 text-gray-600">Ma'lumotlar yuklanmoqda...</span>
|
||||
</div>
|
||||
) : data ? (
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="bg-gray-50 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3 flex items-center gap-2">
|
||||
<Package className="w-5 h-5" />
|
||||
Paket ma'lumotlari
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div>
|
||||
<p className="font-medium text-lg text-gray-600">Partiya nomi:</p>
|
||||
<p className="text-gray-900 text-lg">{data.partyName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Paket nomi:</span>
|
||||
<p className="text-gray-900 text-lg">{data.packetName}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Kargo ID:</span>
|
||||
<p className="text-gray-900 text-lg">{data.cargoId}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Kargo turi:</span>
|
||||
<p className="text-gray-900 text-lg">{data.cargoType}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="font-medium text-lg text-gray-600">To'lov holati:</span>
|
||||
<button className={cn('inline-flex w-fit items-center gap-1 px-2 py-1 rounded-full font-medium text-lg', currentStatus.bgColor, currentStatus.borderColor, currentStatus.color)}>
|
||||
<CurrentIcon className="h-4 w-4" />
|
||||
<span className="font-medium">{currentStatus.label}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 rounded-lg p-4">
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-3 flex items-center gap-2">
|
||||
<User className="w-5 h-5" />
|
||||
Mijoz ma'lumotlari
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-gray-500" />
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">F.I.Sh:</span>
|
||||
<p className="text-gray-900 text-lg">{data.clientFullName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Phone className="w-4 h-4 text-gray-500" />
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Telefon:</span>
|
||||
<p className="text-gray-900 text-lg">{data.clientPhoneNumber}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<CreditCard className="w-4 h-4 text-gray-500" />
|
||||
<span className="font-medium text-lg text-gray-600">To'lov turi:</span>
|
||||
</div>
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex w-fit items-center gap-1 px-2 py-1 rounded-full font-medium text-lg',
|
||||
data.paymentType === null ? 'bg-red-50 text-red-600 border-red-200' : data.paymentType === 'CASH' ? 'bg-green-50 text-green-600 border-green-200' : 'bg-green-50 text-green-600 border-green-200'
|
||||
)}
|
||||
>
|
||||
<CurrentIcon className="h-4 w-4" />
|
||||
<span className="font-medium">{data.paymentType === null ? "To'lanmagan" : data.paymentType === 'CASH' ? 'Naqd' : data.paymentType}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{data.clickTransactions && data.clickTransactions.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium text-lg text-gray-900 mb-3 flex items-center gap-2">
|
||||
<CreditCard className="w-5 h-5 text-blue-600" />
|
||||
Click tranzaksiyalari
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{data.clickTransactions.map((transaction, index) => (
|
||||
<div key={index} className="border border-gray-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-medium text-lg text-gray-900">ID: {transaction.transactionId}</span>
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full font-medium text-lg ${getStatusColor(transaction.status)}`}>
|
||||
{getStatusIcon(transaction.status)}
|
||||
{transaction.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Summa:</span>
|
||||
<p className="text-gray-900 font-semibold">{formatAmount(transaction.amount)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Provayder:</span>
|
||||
<p className="text-gray-900">{transaction.paymentProvider}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Yaratilgan:</span>
|
||||
<p className="text-gray-900">{formatDate(transaction.createTime)}</p>
|
||||
</div>
|
||||
{transaction.completeTime && (
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Tugallangan:</span>
|
||||
<p className="text-gray-900">{formatDate(transaction.completeTime)}</p>
|
||||
</div>
|
||||
)}
|
||||
{transaction.cancelTime && (
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Bekor qilingan:</span>
|
||||
<p className="text-gray-900">{formatDate(transaction.cancelTime)}</p>
|
||||
</div>
|
||||
)}
|
||||
{transaction.errorNote && (
|
||||
<div className="col-span-2">
|
||||
<span className="font-medium text-lg text-gray-600">Xato:</span>
|
||||
<p className="text-red-600">
|
||||
{transaction.errorNote} (Kod: {transaction.errorCode})
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.paymeTransactions && data.paymeTransactions.length > 0 && (
|
||||
<div>
|
||||
<h3 className="font-medium text-lg text-gray-900 mb-3 flex items-center gap-2">
|
||||
<CreditCard className="w-5 h-5 text-green-600" />
|
||||
Payme tranzaksiyalari
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{data.paymeTransactions.map((transaction, index) => (
|
||||
<div key={index} className="border border-gray-200 rounded-lg p-4 bg-white">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="font-medium text-lg text-gray-900">ID: {transaction.transactionId}</span>
|
||||
<span className={`inline-flex items-center gap-1 px-2 py-1 rounded-full font-medium text-lg ${getStatusColor(transaction.status)}`}>
|
||||
{getStatusIcon(transaction.status)}
|
||||
{transaction.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Summa:</span>
|
||||
<p className="text-gray-900 font-semibold">{formatAmount(transaction.amount)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Provayder:</span>
|
||||
<p className="text-gray-900">{transaction.paymentProvider}</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Yaratilgan:</span>
|
||||
<p className="text-gray-900">{formatDate(transaction.createTime)}</p>
|
||||
</div>
|
||||
{transaction.completeTime && (
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Tugallangan:</span>
|
||||
<p className="text-gray-900">{formatDate(transaction.completeTime)}</p>
|
||||
</div>
|
||||
)}
|
||||
{transaction.cancelTime && (
|
||||
<div>
|
||||
<span className="font-medium text-lg text-gray-600">Bekor qilingan:</span>
|
||||
<p className="text-gray-900">{formatDate(transaction.cancelTime)}</p>
|
||||
</div>
|
||||
)}
|
||||
{transaction.errorNote && (
|
||||
<div className="col-span-2">
|
||||
<span className="font-medium text-lg text-gray-600">Xato:</span>
|
||||
<p className="text-red-600">
|
||||
{transaction.errorNote} (Kod: {transaction.errorCode})
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!data.clickTransactions || data.clickTransactions.length === 0) && (!data.paymeTransactions || data.paymeTransactions.length === 0) && !(['PAYED', 'PENDING'].includes(data.paymentStatus) && data.paymentType === 'CASH') && (
|
||||
<div className="text-center py-8">
|
||||
<Clock className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-500">Hozircha tranzaksiyalar mavjud emas</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Clock className="w-12 h-12 text-gray-400 mx-auto mb-3" />
|
||||
<p className="text-gray-500">Ma'lumot topilmadi</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalPayment;
|
||||
67
src/components/payments/ModalQrCode.jsx
Normal file
67
src/components/payments/ModalQrCode.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import { QrCode, X } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
const ModalQrCode = ({ isOpen, onClose, onScan }) => {
|
||||
const inputRef = useRef(null);
|
||||
const [tempValue, setTempValue] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
onScan(tempValue);
|
||||
setTempValue('');
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[99999] flex items-center justify-center">
|
||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={onClose} />
|
||||
|
||||
<div className="relative bg-white rounded-xl shadow-2xl p-6 w-[400px] max-w-[90vw]">
|
||||
<button onClick={onClose} className="absolute top-4 right-4 p-1 rounded-full hover:bg-gray-100 transition-colors">
|
||||
<X className="size-5 text-gray-500" />
|
||||
</button>
|
||||
<div className="text-center mb-4">
|
||||
<h2 className="text-xl font-semibold text-gray-900 mb-2">QR Kodni Skanerlang</h2>
|
||||
</div>
|
||||
<input ref={inputRef} type="text" value={tempValue} onChange={(e) => setTempValue(e.target.value)} onKeyDown={handleKeyDown} className="absolute opacity-0 pointer-events-none" />
|
||||
<div className="relative flex items-center justify-center py-6">
|
||||
<QrCode className="size-40 text-[#3489e3]" />
|
||||
<div className="absolute z-10 w-44 h-[2px] bg-[#3489e3] animate-scan" />
|
||||
</div>
|
||||
</div>
|
||||
<style jsx>{`
|
||||
@keyframes scan {
|
||||
0% {
|
||||
top: 25%;
|
||||
opacity: 0.2;
|
||||
}
|
||||
50% {
|
||||
top: 75%;
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
top: 25%;
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
.animate-scan {
|
||||
animation: scan 2s infinite linear;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ModalQrCode;
|
||||
140
src/components/payments/PaymentStatusSelector.jsx
Normal file
140
src/components/payments/PaymentStatusSelector.jsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import { Check, ChevronDown, Clock, X } from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
const paymentStatuses = [
|
||||
{
|
||||
value: 'UNPAID',
|
||||
label: "To'lanmagan",
|
||||
icon: X,
|
||||
color: 'text-red-600',
|
||||
bgColor: 'bg-red-50',
|
||||
borderColor: 'border-red-200',
|
||||
hoverColor: 'hover:bg-red-100',
|
||||
},
|
||||
{
|
||||
value: 'PENDING',
|
||||
label: 'Kutilmoqda',
|
||||
icon: Clock,
|
||||
color: 'text-yellow-600',
|
||||
bgColor: 'bg-yellow-50',
|
||||
borderColor: 'border-yellow-200',
|
||||
hoverColor: 'hover:bg-yellow-100',
|
||||
},
|
||||
{
|
||||
value: 'PAYED',
|
||||
label: "To'langan",
|
||||
icon: Check,
|
||||
color: 'text-green-600',
|
||||
bgColor: 'bg-green-50',
|
||||
borderColor: 'border-green-200',
|
||||
hoverColor: 'hover:bg-green-100',
|
||||
},
|
||||
];
|
||||
|
||||
const PaymentStatusSelector = ({ value, onChange, disabled = false }) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [position, setPosition] = useState({ top: 0, left: 0, width: 0 });
|
||||
const buttonRef = useRef(null);
|
||||
|
||||
const currentStatus = paymentStatuses.find((status) => status.value === value) || paymentStatuses[0];
|
||||
const CurrentIcon = currentStatus.icon;
|
||||
|
||||
const handleSelect = (statusValue) => {
|
||||
if (statusValue === 'PAID' && (value === 'UNPAID' || value === 'PENDING')) {
|
||||
onChange('PAYED');
|
||||
} else if (statusValue === 'PAID' && value === 'PAID') {
|
||||
// Already paid, do nothing
|
||||
return;
|
||||
} else if (statusValue !== 'PAID') {
|
||||
// Show warning for non-PAID status changes
|
||||
return;
|
||||
}
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && buttonRef.current) {
|
||||
const rect = buttonRef.current.getBoundingClientRect();
|
||||
setPosition({
|
||||
top: rect.bottom + window.scrollY,
|
||||
left: rect.left + window.scrollX,
|
||||
width: rect.width,
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div className="relative w-auto">
|
||||
{/* Trigger button */}
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type="button"
|
||||
onClick={() => !disabled && setIsOpen(!isOpen)}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex items-center justify-between w-full px-3 py-2 text-sm rounded-lg border transition-all duration-200',
|
||||
currentStatus.bgColor,
|
||||
currentStatus.borderColor,
|
||||
currentStatus.color,
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : 'cursor-pointer hover:shadow-sm',
|
||||
!disabled && currentStatus.hoverColor
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<CurrentIcon className="h-4 w-4" />
|
||||
<span className="font-medium">{currentStatus.label}</span>
|
||||
</div>
|
||||
{!disabled && <ChevronDown className={cn('h-4 w-4 transition-transform duration-200', isOpen && 'rotate-180')} />}
|
||||
</button>
|
||||
|
||||
{/* Options (portal) */}
|
||||
{isOpen &&
|
||||
createPortal(
|
||||
<>
|
||||
{/* Backdrop */}
|
||||
<div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
|
||||
|
||||
{/* Dropdown */}
|
||||
<div
|
||||
className="absolute bg-white border border-slate-200 rounded-lg shadow-lg z-50 overflow-hidden"
|
||||
style={{
|
||||
top: position.top,
|
||||
left: position.left,
|
||||
width: position.width,
|
||||
position: 'absolute',
|
||||
}}
|
||||
>
|
||||
{paymentStatuses.map((status) => {
|
||||
const StatusIcon = status.icon;
|
||||
const isSelected = status.value === value;
|
||||
const isDisabled = status.value !== 'PAID' || value === 'PAID';
|
||||
|
||||
return (
|
||||
<button
|
||||
key={status.value}
|
||||
type="button"
|
||||
onClick={() => !isDisabled && handleSelect(status.value)}
|
||||
disabled={isDisabled}
|
||||
className={cn('flex items-center gap-3 w-full px-3 py-2.5 text-sm transition-colors duration-150', isDisabled ? 'opacity-50 cursor-not-allowed' : 'hover:bg-slate-50 focus:bg-slate-50 focus:outline-none cursor-pointer', isSelected && 'bg-slate-100')}
|
||||
>
|
||||
<div className={cn('flex items-center justify-center w-6 h-6 rounded-full', status.bgColor, status.borderColor, 'border')}>
|
||||
<StatusIcon className={cn('h-3 w-3', status.color)} />
|
||||
</div>
|
||||
<span className={cn('font-medium', status.color)}>{status.label}</span>
|
||||
{isSelected && <Check className="h-4 w-4 text-slate-600 ml-auto" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentStatusSelector;
|
||||
196
src/components/payments/PaymentsList.jsx
Normal file
196
src/components/payments/PaymentsList.jsx
Normal file
@@ -0,0 +1,196 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
import { QrCode, Search, Users } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
import Pagination from '../Pagination';
|
||||
import ModalPayment from './ModalPayment';
|
||||
import ModalQrCode from './ModalQrCode';
|
||||
import PaymentStatusSelector from './PaymentStatusSelector';
|
||||
|
||||
const PaymentsList = ({ setAlert }) => {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [cargoType, setCargoType] = useState('');
|
||||
const [isQrModalOpen, setIsQrModalOpen] = useState(false);
|
||||
const [isPaymentModalOpen, setIsPaymentModalOpen] = useState(false);
|
||||
const [scannedResult, setScannedResult] = useState(null);
|
||||
const [packetid, setPacketId] = useState();
|
||||
|
||||
const userRole = (() => {
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
return storedUser ? JSON.parse(storedUser).user.role : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['payments_list', currentPage, searchTerm, cargoType],
|
||||
queryFn: () =>
|
||||
axiosInstance.get('/admin/payments/overview', {
|
||||
params: {
|
||||
page: currentPage - 1,
|
||||
cargoType,
|
||||
size: 30,
|
||||
search: searchTerm,
|
||||
},
|
||||
}),
|
||||
select(data) {
|
||||
return data.data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const handlePageChange = (page) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const { mutate: paymentComplate } = useMutation({
|
||||
mutationFn: async (id) => {
|
||||
return await axiosInstance.post(`/admin/payments/packet/${id}/cash`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "To'lov holati o'zgardi" });
|
||||
refetch();
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: 'Xatolik yuz berdi' });
|
||||
},
|
||||
});
|
||||
|
||||
const handlePaymentStatusChange = async (packetId, newStatus) => {
|
||||
try {
|
||||
if (newStatus === 'PAYED') {
|
||||
paymentComplate(packetId);
|
||||
} else {
|
||||
setAlert({
|
||||
type: 'warning',
|
||||
message: "Faqat to'langan holatiga o'zgartirish mumkin",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
setAlert({ type: 'warning', message: 'Xatolik yuz berdi' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalQrCode
|
||||
isOpen={isQrModalOpen}
|
||||
onClose={() => setIsQrModalOpen(false)}
|
||||
onScan={(value) => {
|
||||
setScannedResult(value);
|
||||
setSearchTerm(value);
|
||||
refetch();
|
||||
}}
|
||||
/>
|
||||
<ModalPayment packetId={packetid} isOpen={isPaymentModalOpen} onClose={() => setIsPaymentModalOpen(false)} />
|
||||
|
||||
<div className="relative flex gap-4">
|
||||
<div className="mb-4 w-[90%]">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
Qidiruv
|
||||
</label>
|
||||
<div className="relative w-full">
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
placeholder={"Cargo Ids, Partiya nomi, Paket nomi bo'yicha qidirish"}
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full border border-slate-300 rounded-lg pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]"
|
||||
/>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4 w-[10%]">
|
||||
<select name="role" className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" onChange={(e) => setCargoType(e.target.value)}>
|
||||
<option value="">Barchasi</option>
|
||||
<option value="AVIA">AVIA</option>
|
||||
<option value="AUTO">AUTO</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mb-4">
|
||||
<button className="bg-[#28A7E8] rounded-md p-2 cursor-pointer hover:bg-[#2196d3] transition-colors" onClick={() => setIsQrModalOpen(true)}>
|
||||
<QrCode className="size-6 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{scannedResult && (
|
||||
<div className="mb-4 p-3 bg-green-100 text-green-800 rounded-md">
|
||||
QR natijasi: <span className="font-mono">{scannedResult}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white/80 backdrop-blur-sm shadow-lg rounded-lg overflow-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200">
|
||||
<h3 className="flex items-center gap-2 text-lg font-semibold">
|
||||
<Users className="h-5 w-5 text-[#3489e3]" /> Mijozlar ro'yxati ({data?.totalElements})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm table-auto min-w-max">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left p-4">ID</th>
|
||||
<th className="text-left p-4">Mijozning ismi</th>
|
||||
<th className="text-left p-4">Mijozning telefoni</th>
|
||||
<th className="text-left p-4">Cargo Idsi</th>
|
||||
<th className="text-left p-4">Partiya nomi</th>
|
||||
<th className="text-left p-4">Paket nomi</th>
|
||||
<th className="text-left p-4">To'lov usuli</th>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && (
|
||||
<>
|
||||
<th className="text-left p-4">Holati</th>
|
||||
</>
|
||||
)}
|
||||
<th className="text-left p-4">Amallar</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{data &&
|
||||
data.data.map((client) => {
|
||||
return (
|
||||
<tr key={client.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4 font-mono">{client.packetId}</td>
|
||||
<td className="p-4">{client.clientFullName}</td>
|
||||
<td className="p-4">{client.clientPhoneNumber}</td>
|
||||
<td className="p-4">{client.cargoId}</td>
|
||||
<td className="p-4">{client.partyName}</td>
|
||||
<td className="p-4 max-w-3xs">{client.packetName}</td>
|
||||
{client.paymentType === null ? <td className="p-4 text-red-500">To'lanmagan</td> : <td className="p-4 text-green-600">{client.paymentType === 'CASH' ? 'Naxt' : client.paymentType}</td>}
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && (
|
||||
<td className="p-4">
|
||||
<PaymentStatusSelector value={client.paymentStatus || 'NEW'} onChange={(value) => handlePaymentStatusChange(client.packetId, value)} disabled={client.paymentStatus === 'PAYED'} />
|
||||
</td>
|
||||
)}
|
||||
<td className="p-4">
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsPaymentModalOpen(true), setPacketId(client.packetId);
|
||||
}}
|
||||
className="bg-[#3489e3] rounded-sm px-4 py-2 text-white cursor-pointer hover:bg-[#2c6eb4]"
|
||||
>
|
||||
Batafsil
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-end mt-5 justify-end">
|
||||
<Pagination currentPage={currentPage} totalPages={data?.totalPages || 1} totalElements={data?.totalElements || 0} onPageChange={handlePageChange} isLoading={isLoading} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentsList;
|
||||
207
src/components/reference/CreateReference.jsx
Normal file
207
src/components/reference/CreateReference.jsx
Normal file
@@ -0,0 +1,207 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
const CreateReference = ({ setAlert, idHouses, setIdHouses }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [formData, setFormData] = useState({
|
||||
titleUz: '',
|
||||
titleRu: '',
|
||||
shortDescriptionUz: '',
|
||||
shortDescriptionRu: '',
|
||||
price: 0,
|
||||
exchange: 'USD',
|
||||
unit: '',
|
||||
unitValue: 1,
|
||||
cargoType: 'AVIA',
|
||||
});
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
titleUz: '',
|
||||
titleRu: '',
|
||||
shortDescriptionUz: '',
|
||||
shortDescriptionRu: '',
|
||||
price: 0,
|
||||
exchange: 'USD',
|
||||
unit: '',
|
||||
unitValue: 1,
|
||||
cargoType: '',
|
||||
});
|
||||
};
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['warhouses_one'],
|
||||
queryFn: async () => {
|
||||
return await axiosInstance.get(`/cargo-reference-book/${idHouses}`);
|
||||
},
|
||||
enabled: !!idHouses,
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (idHouses !== null) {
|
||||
refetch();
|
||||
}
|
||||
}, [idHouses]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setFormData({
|
||||
cargoType: data.cargoType,
|
||||
exchange: data.exchange,
|
||||
price: data.price,
|
||||
shortDescriptionRu: data.shortDescriptionRu,
|
||||
shortDescriptionUz: data.shortDescriptionUz,
|
||||
titleRu: data.titleRu,
|
||||
titleUz: data.titleUz,
|
||||
unit: data.unit,
|
||||
unitValue: data.unitValue,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const { mutate: CreateReference } = useMutation({
|
||||
mutationFn: async (payload) => {
|
||||
return await axiosInstance.post('/cargo-reference-book', payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Narx qo'shildi" });
|
||||
queryClient.invalidateQueries({ queryKey: ['reference_list'] });
|
||||
resetForm();
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Narx qo'shishida xatolik yuz berdi" });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateWarhouses } = useMutation({
|
||||
mutationFn: async (payload) => {
|
||||
return await axiosInstance.put(`/cargo-reference-book/${idHouses}`, payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: 'Narx yangilandi' });
|
||||
queryClient.invalidateQueries({ queryKey: ['reference_list'] });
|
||||
setIdHouses(null);
|
||||
resetForm();
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Narx qo'yangilanshda xatolik yuz berdi" });
|
||||
},
|
||||
});
|
||||
|
||||
const handleAdd = (e) => {
|
||||
e.preventDefault();
|
||||
const isFormValid = formData.titleUz.trim() !== '' && formData.titleRu.trim() !== '' && formData.shortDescriptionUz.trim() !== '' && formData.shortDescriptionRu.trim() !== '' && formData.unit.trim() !== '' && formData.cargoType.trim() !== '' && formData.price > 0 && formData.unitValue > 0;
|
||||
|
||||
if (!isFormValid) {
|
||||
setAlert({ type: 'warning', message: "Iltimos barcha maydonlarni to'ldiring!" });
|
||||
return;
|
||||
} else {
|
||||
CreateReference({
|
||||
titleUz: formData.titleUz,
|
||||
titleRu: formData.titleRu,
|
||||
shortDescriptionUz: formData.shortDescriptionUz,
|
||||
shortDescriptionRu: formData.shortDescriptionRu,
|
||||
price: formData.price,
|
||||
exchange: formData.exchange,
|
||||
unit: formData.unit,
|
||||
unitValue: formData.unitValue,
|
||||
cargoType: formData.cargoType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = (e) => {
|
||||
e.preventDefault();
|
||||
const isFormValid = formData.titleUz.trim() !== '' && formData.titleRu.trim() !== '' && formData.shortDescriptionUz.trim() !== '' && formData.shortDescriptionRu.trim() !== '' && formData.unit.trim() !== '' && formData.cargoType.trim() !== '' && formData.price > 0 && formData.unitValue > 0;
|
||||
|
||||
if (!isFormValid) {
|
||||
setAlert({ type: 'warning', message: "Iltimos barcha maydonlarni to'ldiring!" });
|
||||
return;
|
||||
} else {
|
||||
updateWarhouses({
|
||||
titleUz: formData.titleUz,
|
||||
titleRu: formData.titleRu,
|
||||
shortDescriptionUz: formData.shortDescriptionUz,
|
||||
shortDescriptionRu: formData.shortDescriptionRu,
|
||||
price: formData.price,
|
||||
exchange: formData.exchange,
|
||||
unit: formData.unit,
|
||||
unitValue: formData.unitValue,
|
||||
cargoType: formData.cargoType,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 mb-6">
|
||||
<form className="space-y-6" onSubmit={idHouses !== null ? handleUpdate : handleAdd}>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Nomi (Uz)</label>
|
||||
<input type="text" name="titleUz" value={formData.titleUz} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Nomi (Ru)</label>
|
||||
<input type="text" name="titleRu" value={formData.titleRu} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Qisqacha tarif (UZ)</label>
|
||||
<input type="text" name="shortDescriptionUz" value={formData.shortDescriptionUz} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Qisqacha tarif (RU)</label>
|
||||
<input type="text" name="shortDescriptionRu" value={formData.shortDescriptionRu} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Valyuta</label>
|
||||
<input type="text" name="exchange" disabled value={formData.exchange} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required autoComplete="off" />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">O'lchov birligi</label>
|
||||
<input type="text" name="unit" value={formData.unit} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Qiymati</label>
|
||||
<input type="text" name="unitValue" value={formData.unitValue} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Narxi</label>
|
||||
<input type="text" name="price" value={formData.price} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required />
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-1">Cargo Type</label>
|
||||
<select name="cargoType" value={formData.cargoType} onChange={handleChange} className="w-full border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]" required>
|
||||
<option value="AVIA">AVIA</option>
|
||||
<option value="AUTO">AUTO</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3 mb-8 mt-6">
|
||||
{idHouses === null && <button className="bg-[#3489e3] hover:bg-blue-500 text-white px-5 py-2 rounded-lg shadow">Qo'shish</button>}
|
||||
{idHouses !== null && <button className={`px-5 py-2 rounded-lg shadow text-white bg-[#3489e3] hover:bg-blue-500`}>Yangilash</button>}
|
||||
<button
|
||||
onClick={() => {
|
||||
setFormData({ auto: '', avia: '' }), setIdHouses(null);
|
||||
}}
|
||||
className="bg-gray-400 hover:bg-gray-500 text-white px-5 py-2 rounded-lg shadow"
|
||||
>
|
||||
Tozalash
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateReference;
|
||||
101
src/components/reference/ReferenceList.tsx
Normal file
101
src/components/reference/ReferenceList.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Edit, Trash2 } from 'lucide-react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
const ReferenceList = ({ setAlert, setIdHouses }) => {
|
||||
const refQuery = useQueryClient();
|
||||
const userRole = (() => {
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
return storedUser ? JSON.parse(storedUser).user.role : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['reference_list'],
|
||||
queryFn: async () => await axiosInstance.get('/cargo-reference-book'),
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteReference } = useMutation({
|
||||
mutationFn: async (id) => {
|
||||
return await axiosInstance.delete(`/cargo-reference-book/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Cargo narxi o'chirildi" });
|
||||
refQuery.invalidateQueries({ queryKey: ['reference_list'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'success', message: "Cargo narxi o'chirishda xatolik yuz berdi" });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white/80 backdrop-blur-sm shadow-lg rounded-lg overflow-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200">
|
||||
<h3 className="flex items-center gap-2 text-lg font-semibold">{/* <Users className="h-5 w-5 text-[#3489e3]" /> Mijozlar ro'yxati ({data?.totalElements}) */}</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm table-auto min-w-max">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left p-4">ID</th>
|
||||
<th className="text-left p-4">Tarif</th>
|
||||
<th className="text-left p-4">Tarif(RU)</th>
|
||||
<th className="text-left p-4">Qisqacha tarif</th>
|
||||
<th className="text-left p-4">Qisqacha tarif(RU)</th>
|
||||
<th className="text-left p-4">O'lchov birligi</th>
|
||||
<th className="text-left p-4">narxi</th>
|
||||
<th className="text-left p-4">Miqdori</th>
|
||||
<th className="text-left p-4">Pul birligi</th>
|
||||
<th className="text-left p-4">Cargo turi</th>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && <th className="text-left p-4">Amallar</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{data &&
|
||||
data.map((client) => {
|
||||
return (
|
||||
<tr key={client.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4">{client.id}</td>
|
||||
<td className="p-4">{client.titleUz}</td>
|
||||
<td className="p-4">{client.titleRu}</td>
|
||||
<td className="p-4">{client.shortDescriptionUz}</td>
|
||||
<td className="p-4">{client.shortDescriptionRu}</td>
|
||||
<td className="p-4">{client.unit}</td>
|
||||
<td className="p-4">{client.price}</td>
|
||||
<td className="p-4">{client.unitValue}</td>
|
||||
<td className="p-4">{client.exchange}</td>
|
||||
<td className="p-4">{client.cargoType}</td>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && (
|
||||
<td className="p-4">
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setIdHouses(client.id)} className="flex items-center gap-1 border border-yellow-300 text-yellow-700 hover:bg-yellow-50 px-3 py-1 rounded-lg" disabled={isLoading}>
|
||||
<Edit className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Tahrirlash</span>
|
||||
</button>
|
||||
<button onClick={() => deleteReference(client.id)} className="flex items-center gap-1 border border-red-300 text-red-700 hover:bg-red-50 px-3 py-1 rounded-lg">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">O'chirish</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end mt-5 justify-end">{/* <Pagination currentPage={currentPage} totalPages={data?.totalPages || 1} totalElements={data?.totalElements || 0} onPageChange={handlePageChange} isLoading={isLoading} /> */}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReferenceList;
|
||||
187
src/components/warhouses/CreateWarhouses.jsx
Normal file
187
src/components/warhouses/CreateWarhouses.jsx
Normal file
@@ -0,0 +1,187 @@
|
||||
'use client';
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useEffect, useState } from 'react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
const CreateWarhouses = ({ setAlert, idHouses, setIdHouses }) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [formData, setFormData] = useState({
|
||||
avia: '',
|
||||
auto: '',
|
||||
});
|
||||
|
||||
const { data, refetch } = useQuery({
|
||||
queryKey: ['warhouses_one'],
|
||||
queryFn: async () => {
|
||||
return await axiosInstance.get(`/warehouses/${idHouses}`);
|
||||
},
|
||||
enabled: !!idHouses,
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (idHouses !== null) {
|
||||
refetch();
|
||||
}
|
||||
}, [idHouses]);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
const aviaFormatted = data.avia.replaceAll('%s', '(id)');
|
||||
const autoFormatted = data.auto.replaceAll('%s', '(id)');
|
||||
setFormData({
|
||||
avia: aviaFormatted,
|
||||
auto: autoFormatted,
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const isFormValid = Object.values(formData).every((value) => value.trim() !== '');
|
||||
|
||||
const { mutate: createWarhouses } = useMutation({
|
||||
mutationFn: async (payload) => {
|
||||
return await axiosInstance.post('/warehouses', payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Omborxona qo'shildi" });
|
||||
queryClient.invalidateQueries({ queryKey: ['warhouses'] });
|
||||
setFormData({
|
||||
auto: '',
|
||||
avia: '',
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Omborxona qo'shishida xatolik yuz berdi" });
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: updateWarhouses } = useMutation({
|
||||
mutationFn: async (payload) => {
|
||||
return await axiosInstance.put(`/warehouses/${idHouses}`, payload);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Omborxona qo'shildi" });
|
||||
queryClient.invalidateQueries({ queryKey: ['warhouses'] });
|
||||
setIdHouses(null);
|
||||
setFormData({
|
||||
auto: '',
|
||||
avia: '',
|
||||
});
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'warning', message: "Omborxona qo'shishida xatolik yuz berdi" });
|
||||
},
|
||||
});
|
||||
|
||||
const handleAdd = () => {
|
||||
if (!isFormValid) {
|
||||
setAlert({ type: 'warning', message: "Iltimos barcha maydonlarni to'ldiring!" });
|
||||
return;
|
||||
} else {
|
||||
const aviaFormatted = formData.avia.replaceAll('(id)', '%s');
|
||||
const autoFormatted = formData.auto.replaceAll('(id)', '%s');
|
||||
createWarhouses({
|
||||
avia: aviaFormatted,
|
||||
auto: autoFormatted,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
if (!isFormValid) {
|
||||
setAlert({ type: 'warning', message: "Iltimos barcha maydonlarni to'ldiring!" });
|
||||
return;
|
||||
} else {
|
||||
const aviaFormatted = formData.avia.replaceAll('(id)', '%s');
|
||||
const autoFormatted = formData.auto.replaceAll('(id)', '%s');
|
||||
updateWarhouses({
|
||||
avia: aviaFormatted,
|
||||
auto: autoFormatted,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col bg-white shadow rounded-lg p-6 mb-6">
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{Object.keys(formData)
|
||||
.filter((key) => key !== 'id')
|
||||
.map((key) => (
|
||||
<div key={key} className="flex flex-col">
|
||||
<label className="mb-1 font-semibold text-slate-700">{key === 'avia' ? 'Avia manzil' : key === 'auto' ? 'Auto manzil' : key}</label>
|
||||
<textarea
|
||||
required
|
||||
type="text"
|
||||
className="border h-36 max-h-36 border-slate-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]"
|
||||
value={formData[key] || ''}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||
<h3 className="text-red-600 text-lg font-bold mb-3">📋 Muhim Eslatma</h3>
|
||||
|
||||
<div className="text-red-700 space-y-2">
|
||||
<p className="font-medium">
|
||||
✅ User ID sini kiritadigan joyni <span className="bg-yellow-200 px-1 rounded font-mono">(id)</span> qilib yozing
|
||||
</p>
|
||||
|
||||
<div className="mt-4">
|
||||
<h4 className="font-bold text-red-600 mb-2">📝 To'g'ri misol:</h4>
|
||||
<div className="bg-white p-3 rounded border-l-4 border-red-400 font-mono text-sm space-y-1">
|
||||
<div>
|
||||
<span className="text-gray-600">收货人:</span> (id)
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">手机号码:</span> 18335530701
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">收货地址:</span> 北京市顺义区南法信旭辉空港中心C座1004 (id)
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-600">Avia post code:</span> 101399
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 p-2 bg-yellow-100 rounded text-yellow-800 text-sm">
|
||||
<strong>Diqqat:</strong> (id) o'rniga haqiqiy user ID raqami avtomatik qo'yiladi
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-3 mb-8 mt-6">
|
||||
{idHouses === null && (
|
||||
<button className="bg-[#3489e3] hover:bg-blue-500 text-white px-5 py-2 rounded-lg shadow" onClick={handleAdd}>
|
||||
Qo'shish
|
||||
</button>
|
||||
)}
|
||||
{idHouses && data && (
|
||||
<button onClick={handleUpdate} className={`px-5 py-2 rounded-lg shadow text-white bg-[#3489e3] hover:bg-blue-500`}>
|
||||
Yangilash
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setFormData({ auto: '', avia: '' }), setIdHouses(null);
|
||||
}}
|
||||
className="bg-gray-400 hover:bg-gray-500 text-white px-5 py-2 rounded-lg shadow"
|
||||
>
|
||||
Tozalash
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateWarhouses;
|
||||
87
src/components/warhouses/WarhousesList.jsx
Normal file
87
src/components/warhouses/WarhousesList.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { Edit, Trash2 } from 'lucide-react';
|
||||
import axiosInstance from '../../api/axiosInstance';
|
||||
|
||||
const WarhousesList = ({ setAlert, setIdHouses }) => {
|
||||
const refQuery = useQueryClient();
|
||||
const userRole = (() => {
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user');
|
||||
return storedUser ? JSON.parse(storedUser).user.role : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
})();
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['warhouses'],
|
||||
queryFn: async () => await axiosInstance.get('/warehouses'),
|
||||
select(data) {
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: deleteWarhouses } = useMutation({
|
||||
mutationFn: async (id) => {
|
||||
return await axiosInstance.delete(`/warehouses/${id}`);
|
||||
},
|
||||
onSuccess: () => {
|
||||
setAlert({ type: 'success', message: "Omborxona o'chirildi" });
|
||||
refQuery.invalidateQueries({ queryKey: ['warhouses'] });
|
||||
},
|
||||
onError: () => {
|
||||
setAlert({ type: 'success', message: "Omborxona o'chirishda xatolik yuz berdi" });
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="bg-white/80 backdrop-blur-sm shadow-lg rounded-lg overflow-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200">
|
||||
<h3 className="flex items-center gap-2 text-lg font-semibold">{/* <Users className="h-5 w-5 text-[#3489e3]" /> Mijozlar ro'yxati ({data?.totalElements}) */}</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm table-auto min-w-max">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left p-4">ID</th>
|
||||
<th className="text-left p-4">AVIA manzil</th>
|
||||
<th className="text-left p-4">AUTO manzil</th>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && <th className="text-left p-4">Amallar</th>}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{data &&
|
||||
data.map((client) => {
|
||||
return (
|
||||
<tr key={client.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4 font-mono">{client.id}</td>
|
||||
<td className="p-4">{client.avia ? client.avia.split(/%s/).map((line, i) => <div key={i}>{line.trim()} (id)</div>) : <div>Ma’lumot yo‘q</div>}</td>
|
||||
<td className="p-4">{client.auto ? client.auto.split(/%s/).map((line, i) => <div key={i}>{line.trim()} (id)</div>) : <div>Ma’lumot yo‘q</div>}</td>
|
||||
{(userRole === 'ADMIN' || userRole === 'SUPER_ADMIN') && (
|
||||
<td className="p-4">
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => setIdHouses(client.id)} className="flex items-center gap-1 border border-yellow-300 text-yellow-700 hover:bg-yellow-50 px-3 py-1 rounded-lg" disabled={isLoading}>
|
||||
<Edit className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Tahrirlash</span>
|
||||
</button>
|
||||
<button onClick={() => deleteWarhouses(client.id)} className="flex items-center gap-1 border border-red-300 text-red-700 hover:bg-red-50 px-3 py-1 rounded-lg">
|
||||
<Trash2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">O'chirish</span>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-end mt-5 justify-end">{/* <Pagination currentPage={currentPage} totalPages={data?.totalPages || 1} totalElements={data?.totalElements || 0} onPageChange={handlePageChange} isLoading={isLoading} /> */}</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default WarhousesList;
|
||||
60
src/context/AuthContext.jsx
Normal file
60
src/context/AuthContext.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(() => {
|
||||
const storedData = localStorage.getItem('user');
|
||||
if (!storedData) return null;
|
||||
const parsed = JSON.parse(storedData);
|
||||
if (parsed.expiry && new Date().getTime() > parsed.expiry) {
|
||||
localStorage.removeItem('user');
|
||||
return null;
|
||||
}
|
||||
return parsed.user || null;
|
||||
});
|
||||
|
||||
const [token, setToken] = useState(() => {
|
||||
const storedData = localStorage.getItem('user');
|
||||
if (!storedData) return null;
|
||||
const parsed = JSON.parse(storedData);
|
||||
if (parsed.expiry && new Date().getTime() > parsed.expiry) {
|
||||
localStorage.removeItem('user');
|
||||
return null;
|
||||
}
|
||||
return parsed.token || null;
|
||||
});
|
||||
|
||||
const login = (userData, tokenValue) => {
|
||||
const expiry = new Date().getTime() + 9 * 60 * 60 * 1000; // 9 soat
|
||||
const userObj = { user: userData, token: tokenValue, expiry };
|
||||
localStorage.setItem('user', JSON.stringify(userObj)); // user key ostida saqlash
|
||||
setUser(userData);
|
||||
setToken(tokenValue);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
localStorage.removeItem('user'); // user key o'chirish
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
const stored = localStorage.getItem('user');
|
||||
if (stored) {
|
||||
const { expiry } = JSON.parse(stored);
|
||||
if (expiry && new Date().getTime() > expiry) logout();
|
||||
}
|
||||
}, 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ user, token, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => useContext(AuthContext);
|
||||
11
src/data/allMenus.jsx
Normal file
11
src/data/allMenus.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
// src/components/allMenus.js
|
||||
import { Home, Users, CreditCard, MessageCircle, Map as MapIcon, Package } from 'lucide-react';
|
||||
|
||||
export const allMenus = [
|
||||
{ label: "Dashboard", to: "/", icon: <Home size={18} /> },
|
||||
{ label: "Clients", to: "/clients", icon: <Users size={18} /> },
|
||||
{ label: "Payments", to: "/payments", icon: <CreditCard size={18} /> },
|
||||
{ label: "Chat", to: "/chat", icon: <MessageCircle size={18} /> },
|
||||
{ label: "Branches", to: "/branches", icon: <MapIcon size={18} /> },
|
||||
{ label: "Warehouse", to: "/warehouse", icon: <Package size={18} /> },
|
||||
];
|
||||
1
src/index.css
Normal file
1
src/index.css
Normal file
@@ -0,0 +1 @@
|
||||
@import 'tailwindcss';
|
||||
6
src/lib/utils.js
Normal file
6
src/lib/utils.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import { clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
10
src/main.jsx
Normal file
10
src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.jsx'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
|
||||
createRoot(document.getElementById('root')).render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
)
|
||||
23
src/pages/Branches.jsx
Normal file
23
src/pages/Branches.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AlertCircle, Check, Store } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import BranchesList from '../components/branches/BranchesList';
|
||||
import CreateBranches from '../components/branches/CreateBranches';
|
||||
import Alert from '../components/Error';
|
||||
import Header from '../components/Header';
|
||||
|
||||
const Branches = () => {
|
||||
const [branches, setBranches] = useState(null);
|
||||
const [alert, setAlert] = useState(null);
|
||||
return (
|
||||
<div className="p-4 max-w-7xl mx-auto">
|
||||
<Header Icon={Store} title={"Filial Ro'yxati"} text={"Filiallarni boshqarish va ro'yxatni yuritish tizimi"} />
|
||||
{alert && alert.type === 'success' ? <Alert Icon={Check} variant="success" message={alert.message} /> : alert && alert.type === 'warning' && <Alert Icon={AlertCircle} variant="warning" message={alert.message} />}
|
||||
{/* Forma */}
|
||||
<CreateBranches branches={branches} setAlert={setAlert} setBranches={setBranches} />
|
||||
{/* Jadval */}
|
||||
<BranchesList setBranches={setBranches} setAlert={setAlert} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Branches;
|
||||
25
src/pages/Clients.jsx
Normal file
25
src/pages/Clients.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { AlertCircle, Check, Users } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import ClientList from '../components/clients/ClientList';
|
||||
import CreateClient from '../components/clients/CreateClient';
|
||||
import Alert from '../components/Error';
|
||||
import Header from '../components/Header';
|
||||
|
||||
const Clients = () => {
|
||||
const [alert, setAlert] = useState(null);
|
||||
const [clients, setClients] = useState(null);
|
||||
const [cargoId, setCargoId] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-4 md:p-6 lg:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<Header Icon={Users} title={"Mijozlar Ro'yxati"} text={"Mijozlarni boshqarish va ro'yxatni yuritish tizimi"} />
|
||||
{alert && alert.type === 'success' ? <Alert Icon={Check} variant="success" message={alert.message} /> : alert && alert.type === 'warning' && <Alert Icon={AlertCircle} variant="warning" message={alert.message} />}
|
||||
<CreateClient clients={clients} setAlert={setAlert} cargoId={cargoId} />
|
||||
<ClientList alert={alert} setAlert={setAlert} setClients={setClients} setCargoId={setCargoId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Clients;
|
||||
31
src/pages/Currency.jsx
Normal file
31
src/pages/Currency.jsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import { AlertCircle, Check, PlusCircle } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CreateCurrency from '../components/currency/CreateCurrency';
|
||||
import CurrencyList from '../components/currency/CurrencyList';
|
||||
import Alert from '../components/Error';
|
||||
import Header from '../components/Header';
|
||||
|
||||
const Currency = () => {
|
||||
const [alert, setAlert] = useState(null);
|
||||
const [editingCurrency, setEditingCurrency] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-4 md:p-6 lg:p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Sarlavha */}
|
||||
<Header Icon={PlusCircle} title={'Valyutalar'} text={'Valyutalarni boshqarish tizimi'} />
|
||||
|
||||
{/* Xatolik */}
|
||||
{alert && alert.type === 'success' ? <Alert Icon={Check} variant="success" message={alert.message} /> : alert && alert.type === 'warning' && <Alert Icon={AlertCircle} variant="warning" message={alert.message} />}
|
||||
|
||||
{/* Forma */}
|
||||
<CreateCurrency setAlert={setAlert} editingCurrency={editingCurrency} />
|
||||
|
||||
{/* Jadval */}
|
||||
<CurrencyList setAlert={setAlert} setEditingCurrency={setEditingCurrency} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Currency;
|
||||
30
src/pages/Employees.jsx
Normal file
30
src/pages/Employees.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { AlertCircle, Check, Users } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import CreateEmployee from '../components/employes/CreateEmployee';
|
||||
import EmployesList from '../components/employes/EmployesList';
|
||||
import Alert from '../components/Error';
|
||||
import Header from '../components/Header';
|
||||
|
||||
const Employees = () => {
|
||||
const [alert, setAlert] = useState(null);
|
||||
const [clients, setClients] = useState(null);
|
||||
const [cargoId, setCargoId] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-4 md:p-6 lg:p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{/* Sarlavha */}
|
||||
<Header Icon={Users} title={"Xodimlar Ro'yxati"} text={"Xodimlarni boshqarish va ro'yxatni yuritish tizimi"} />
|
||||
|
||||
{/* Xatolik */}
|
||||
{alert && alert.type === 'success' ? <Alert Icon={Check} variant="success" message={alert.message} /> : alert && alert.type === 'warning' && <Alert Icon={AlertCircle} variant="warning" message={alert.message} />}
|
||||
|
||||
<CreateEmployee clients={clients} setAlert={setAlert} cargoId={cargoId} />
|
||||
{/* Jadval */}
|
||||
<EmployesList alert={alert} setAlert={setAlert} setClients={setClients} setCargoId={setCargoId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Employees;
|
||||
111
src/pages/Login.jsx
Normal file
111
src/pages/Login.jsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import React, { useState } from 'react';
|
||||
import axios from '../api/axiosInstance';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const Login = () => {
|
||||
const { login } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
// Serverga login so'rovi
|
||||
const response = await axios.post('/auth/token', { username, password });
|
||||
const { accessToken, roles } = response.data.data;
|
||||
|
||||
if (!roles || roles.length === 0) {
|
||||
setError('Foydalanuvchi rol topilmadi');
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const role = roles[0]; // birinchi rolni olamiz
|
||||
|
||||
// Login kontekstga yozish va localStorage 'user' keyi ostida saqlash
|
||||
login({ username, role }, accessToken);
|
||||
|
||||
// Rolga qarab yo'naltirish
|
||||
if (role === 'ADMIN') {
|
||||
navigate('/'); // Admin bo'lsa bosh sahifa
|
||||
} else if (role === 'UZB_WORKER' || role === 'CHINA_WORKER') {
|
||||
navigate('/payments'); // Worker bo'lsa payments sahifa
|
||||
} else {
|
||||
setError('Ruxsat berilmagan rol');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
setError('Login yoki parol noto‘g‘ri');
|
||||
console.error(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-sky-400 to-[#3489e3] px-4">
|
||||
<div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
|
||||
<h2 className="text-3xl font-bold text-center mb-6 text-[#3489e3]">
|
||||
Admin Panelga Kirish
|
||||
</h2>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-100 text-red-700 p-3 mb-4 rounded text-center font-semibold">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6" autoComplete="off">
|
||||
<div>
|
||||
<label htmlFor="username" className="block text-gray-700 font-medium mb-1">
|
||||
Foydalanuvchi nomi
|
||||
</label>
|
||||
<input
|
||||
id="username"
|
||||
type="text"
|
||||
placeholder="Foydalanuvchi nomi"
|
||||
className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]"
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="password" className="block text-gray-700 font-medium mb-1">
|
||||
Parol
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Parol"
|
||||
className="w-full border border-gray-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className={`w-full bg-[#3489e3] text-white py-2 rounded hover:bg-blue-500 transition ${loading ? 'opacity-70 cursor-not-allowed' : ''
|
||||
}`}
|
||||
>
|
||||
{loading ? 'Yuklanmoqda...' : 'Kirish'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
52
src/pages/MenuSettings.jsx
Normal file
52
src/pages/MenuSettings.jsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const availableMenus = [
|
||||
{ key: 'branches', label: 'Filiallar' },
|
||||
{ key: 'warehouse', label: 'Xitoy Ombori' },
|
||||
];
|
||||
|
||||
const MenuSettingsPage = () => {
|
||||
const { user } = useAuth();
|
||||
const [selectedMenus, setSelectedMenus] = useState(() => {
|
||||
const saved = localStorage.getItem('adminMenus');
|
||||
return saved ? JSON.parse(saved) : [];
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user?.role !== 'super-admin') return;
|
||||
localStorage.setItem('adminMenus', JSON.stringify(selectedMenus));
|
||||
}, [selectedMenus, user]);
|
||||
|
||||
const toggleMenu = (key) => {
|
||||
setSelectedMenus((prev) =>
|
||||
prev.includes(key) ? prev.filter((m) => m !== key) : [...prev, key]
|
||||
);
|
||||
};
|
||||
|
||||
if (user?.role !== 'super-admin') {
|
||||
return <p>Faqat super-admin uchun sahifa.</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-4">Oddiy Admin uchun menyu tanlash</h2>
|
||||
{availableMenus.map(({ key, label }) => (
|
||||
<label key={key} className="block mb-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMenus.includes(key)}
|
||||
onChange={() => toggleMenu(key)}
|
||||
className="mr-2"
|
||||
/>
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
<p className="mt-4 text-gray-600">
|
||||
Tanlangan menyular oddiy admin uchun sidebar menyusida ko‘rsatiladi.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MenuSettingsPage;
|
||||
21
src/pages/Payments.jsx
Normal file
21
src/pages/Payments.jsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { AlertCircle, Banknote, Check } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Alert from '../components/Error';
|
||||
import Header from '../components/Header';
|
||||
import PaymentsList from '../components/payments/PaymentsList';
|
||||
|
||||
const Payments = () => {
|
||||
const [alert, setAlert] = useState(null);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-4 md:p-6 lg:p-8">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Header Icon={Banknote} title={"To'lovlar tarixi"} text={"To'lovlarni boshqarish va ko‘rish tizimi"} />
|
||||
{alert && alert.type === 'success' ? <Alert Icon={Check} variant="success" message={alert.message} /> : alert && alert.type === 'warning' && <Alert Icon={AlertCircle} variant="warning" message={alert.message} />}
|
||||
<PaymentsList setAlert={setAlert} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Payments;
|
||||
181
src/pages/Permissions.jsx
Normal file
181
src/pages/Permissions.jsx
Normal file
@@ -0,0 +1,181 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Shield, Save, Search } from "lucide-react";
|
||||
|
||||
const Permissions = () => {
|
||||
const initialPermissions = [
|
||||
{ id: 1, name: "Mijoz qo‘shish", key: "add_client", allowed: false },
|
||||
{ id: 2, name: "Mijozni tahrirlash", key: "edit_client", allowed: false },
|
||||
{ id: 3, name: "Mijozni o‘chirish", key: "delete_client", allowed: false },
|
||||
{ id: 4, name: "Hisobotlarni ko‘rish", key: "view_reports", allowed: false },
|
||||
{ id: 5, name: "Sozlamalarni o‘zgartirish", key: "change_settings", allowed: false },
|
||||
];
|
||||
|
||||
const [permissions, setPermissions] = useState(initialPermissions);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [role, setRole] = useState("ADMIN");
|
||||
|
||||
// LocalStorage dan mavjud ruxsatlarni olish
|
||||
useEffect(() => {
|
||||
const stored = JSON.parse(localStorage.getItem("pagePermissions")) || {};
|
||||
const roleKey = role.toLowerCase();
|
||||
if (stored[roleKey]) {
|
||||
const updated = permissions.map((perm) => ({
|
||||
...perm,
|
||||
allowed: stored[roleKey][perm.key] || false,
|
||||
}));
|
||||
setPermissions(updated);
|
||||
} else {
|
||||
setPermissions(initialPermissions);
|
||||
}
|
||||
}, [role]);
|
||||
|
||||
const filteredPermissions = permissions.filter((perm) =>
|
||||
perm.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// Checkbox qiymatini o‘zgartirish
|
||||
const togglePermission = (id) => {
|
||||
setPermissions((prev) =>
|
||||
prev.map((perm) =>
|
||||
perm.id === id ? { ...perm, allowed: !perm.allowed } : perm
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
// Saqlash tugmasi
|
||||
const handleSave = () => {
|
||||
const stored = JSON.parse(localStorage.getItem("pagePermissions")) || {};
|
||||
const updatedPermissions = { ...stored };
|
||||
|
||||
// Hozirgi role uchun ruxsatlarni obyektga aylantirish
|
||||
const currentPermissions = permissions.reduce((acc, perm) => {
|
||||
acc[perm.key] = perm.allowed;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (role === "CHINA_WORKER" || role === "UZB_WORKER") {
|
||||
updatedPermissions["china_worker"] = currentPermissions;
|
||||
updatedPermissions["uzb_worker"] = currentPermissions;
|
||||
} else {
|
||||
updatedPermissions[role.toLowerCase()] = currentPermissions;
|
||||
}
|
||||
|
||||
localStorage.setItem("pagePermissions", JSON.stringify(updatedPermissions));
|
||||
|
||||
console.log("Yangi pagePermissions:", updatedPermissions);
|
||||
alert(`Ruxsatlar ${role} uchun saqlandi!`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-4 md:p-6 lg:p-8">
|
||||
<div className="max-w-5xl mx-auto">
|
||||
{/* Sarlavha */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<Shield className="h-8 w-8 text-[#3489e3]" />
|
||||
<h1 className="text-3xl font-bold text-slate-900">
|
||||
Ruxsatlarni boshqarish
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-slate-600">Rollar bo‘yicha ruxsatlarni sozlash</p>
|
||||
</div>
|
||||
|
||||
{/* Role tanlash */}
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium mb-2">
|
||||
Rolni tanlang
|
||||
</label>
|
||||
<select
|
||||
value={role}
|
||||
onChange={(e) => setRole(e.target.value)}
|
||||
className="w-full md:w-64 border border-slate-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]"
|
||||
>
|
||||
<option value="SUPER_ADMIN">SUPER_ADMIN</option>
|
||||
<option value="ADMIN">ADMIN</option>
|
||||
<option value="CHINA_WORKER">CHINA_WORKER</option>
|
||||
<option value="UZB_WORKER">UZB_WORKER</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Qidiruv */}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="search" className="sr-only">
|
||||
Qidiruv
|
||||
</label>
|
||||
<div className="relative max-w-md">
|
||||
<input
|
||||
type="text"
|
||||
id="search"
|
||||
placeholder="Ruxsat nomi bo‘yicha qidirish..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full border border-slate-300 rounded-lg pl-10 pr-4 py-2 focus:outline-none focus:ring-2 focus:ring-[#3489e3]"
|
||||
/>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Jadval */}
|
||||
<div className="bg-white/80 backdrop-blur-sm shadow-lg rounded-lg overflow-hidden">
|
||||
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-slate-800">
|
||||
Ruxsatlar ({filteredPermissions.length})
|
||||
</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm table-auto min-w-max">
|
||||
<thead className="bg-slate-50 border-b border-slate-200">
|
||||
<tr>
|
||||
<th className="text-left p-4">#</th>
|
||||
<th className="text-left p-4">Ruxsat nomi</th>
|
||||
<th className="text-left p-4">Holat</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{filteredPermissions.map((perm, index) => (
|
||||
<tr key={perm.id} className="hover:bg-slate-50 transition-colors">
|
||||
<td className="p-4">{index + 1}</td>
|
||||
<td className="p-4">{perm.name}</td>
|
||||
<td className="p-4">
|
||||
<label className="inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={perm.allowed}
|
||||
onChange={() => togglePermission(perm.id)}
|
||||
className="w-5 h-5 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<span className="ml-2 text-slate-700">
|
||||
{perm.allowed ? "Ruxsat berilgan" : "Cheklangan"}
|
||||
</span>
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{filteredPermissions.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan="3" className="text-center p-4 text-slate-500">
|
||||
Hech qanday ruxsat topilmadi.
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Saqlash tugmasi */}
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="flex items-center gap-2 bg-[#3489e3] hover:bg-blue-500 text-white px-6 py-2 rounded-lg transition"
|
||||
>
|
||||
<Save className="h-5 w-5" />
|
||||
Saqlash
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Permissions;
|
||||
23
src/pages/Reference.jsx
Normal file
23
src/pages/Reference.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AlertCircle, Check, DollarSign } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Alert from '../components/Error';
|
||||
import Header from '../components/Header';
|
||||
import CreateReference from '../components/reference/CreateReference';
|
||||
import ReferenceList from '../components/reference/ReferenceList';
|
||||
|
||||
const Reference = () => {
|
||||
const [alert, setAlert] = useState(null);
|
||||
const [idHouses, setIdHouses] = useState(null);
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-4 md:p-6 lg:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<Header Icon={DollarSign} title={"Cargo narxlari Ro'yxati"} text={"Cargo narxlarini boshqarish va ro'yxatni yuritish tizimi"} />
|
||||
{alert && alert.type === 'success' ? <Alert Icon={Check} variant="success" message={alert.message} /> : alert && alert.type === 'warning' && <Alert Icon={AlertCircle} variant="warning" message={alert.message} />}
|
||||
<CreateReference setAlert={setAlert} idHouses={idHouses} setIdHouses={setIdHouses} />
|
||||
<ReferenceList setAlert={setAlert} setIdHouses={setIdHouses} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reference;
|
||||
11
src/pages/RoleProtectedRoute.jsx
Normal file
11
src/pages/RoleProtectedRoute.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Navigate } from 'react-router-dom';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const RoleProtectedRoute = ({ children, allowedRoles }) => {
|
||||
const { user } = useAuth();
|
||||
if (!user) return <Navigate to="/login" />;
|
||||
if (!allowedRoles.includes(user.role)) return <Navigate to="/login" />;
|
||||
return children;
|
||||
};
|
||||
|
||||
export default RoleProtectedRoute;
|
||||
23
src/pages/Warhouses.jsx
Normal file
23
src/pages/Warhouses.jsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { AlertCircle, Check, Warehouse } from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import Alert from '../components/Error';
|
||||
import Header from '../components/Header';
|
||||
import CreateWarhouses from '../components/warhouses/CreateWarhouses';
|
||||
import WarhousesList from '../components/warhouses/WarhousesList';
|
||||
|
||||
const Warhouses = () => {
|
||||
const [alert, setAlert] = useState(null);
|
||||
const [idHouses, setIdHouses] = useState(null);
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100 p-4 md:p-6 lg:p-8">
|
||||
<div className="max-w-6xl mx-auto">
|
||||
<Header Icon={Warehouse} title={"Omborlar Ro'yxati"} text={"Omborlarni boshqarish va ro'yxatni yuritish tizimi"} />
|
||||
{alert && alert.type === 'success' ? <Alert Icon={Check} variant="success" message={alert.message} /> : alert && alert.type === 'warning' && <Alert Icon={AlertCircle} variant="warning" message={alert.message} />}
|
||||
<CreateWarhouses setAlert={setAlert} idHouses={idHouses} setIdHouses={setIdHouses} />
|
||||
<WarhousesList setAlert={setAlert} setIdHouses={setIdHouses} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Warhouses;
|
||||
8
vercel.json
Normal file
8
vercel.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{
|
||||
"source": "/(.*)",
|
||||
"destination": "/index.html"
|
||||
}
|
||||
]
|
||||
}
|
||||
17
vite.config.js
Normal file
17
vite.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
import react from '@vitejs/plugin-react-swc';
|
||||
import path from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3080,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user