Initial commit

This commit is contained in:
Samandar Turgunboyev
2025-09-09 10:46:03 +05:00
commit 1e5357ec39
53 changed files with 34753 additions and 0 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
VITE_API_BASE_URL=https://api.cpcargo.uz/api/v1/

24
.gitignore vendored Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

46
package.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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;

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

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

View File

@@ -0,0 +1,17 @@
import { Search } from 'lucide-react';
const Searchs = ({ searchTerm, loading, setSearchTerm, placeholder = 'Ism, telefon yoki manzil boyicha 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
View 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;

View 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="Ochirish">
<Trash2 className="h-4 w-4" />
<span className="hidden sm:inline">Ochirish</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;

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

View 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 ochirildi.' });
setClients(null);
setCargoId();
},
onError: () => {
setAlert({ type: 'warning', message: 'Foydalanuvchini ochirishda 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;

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

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

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

View 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 yoq yoki qidiruv natijasida topilmadi.
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
);
};
export default CurrencyList;

View 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 qoshish huquqi yoq.</div>
)}
</>
);
};
export default CreateEmployee;

View 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 ochirildi.' });
setClients(null);
setCargoId();
},
onError: () => {
setAlert({ type: 'warning', message: 'Foydalanuvchini ochirishda 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 yoq 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;

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

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

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

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

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

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

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

View 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>Malumot yoq</div>}</td>
<td className="p-4">{client.auto ? client.auto.split(/%s/).map((line, i) => <div key={i}>{line.trim()} (id)</div>) : <div>Malumot yoq</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;

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

@@ -0,0 +1 @@
@import 'tailwindcss';

6
src/lib/utils.js Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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 notogri');
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;

View 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 korsatiladi.
</p>
</div>
);
};
export default MenuSettingsPage;

21
src/pages/Payments.jsx Normal file
View 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 korish 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
View 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 qoshish", key: "add_client", allowed: false },
{ id: 2, name: "Mijozni tahrirlash", key: "edit_client", allowed: false },
{ id: 3, name: "Mijozni ochirish", key: "delete_client", allowed: false },
{ id: 4, name: "Hisobotlarni korish", key: "view_reports", allowed: false },
{ id: 5, name: "Sozlamalarni ozgartirish", 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 ozgartirish
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 boyicha 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 boyicha 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
View 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;

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

@@ -0,0 +1,8 @@
{
"rewrites": [
{
"source": "/(.*)",
"destination": "/index.html"
}
]
}

17
vite.config.js Normal file
View 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,
},
});

11122
yarn.lock Normal file

File diff suppressed because it is too large Load Diff