diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 000000000..e9c55eec0 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,24 @@ +module.exports = { + // parser: '@typescript-eslint/parser', // Specifies the ESLint parser + parserOptions: { + ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features + sourceType: 'module', // Allows for the use of imports + ecmaFeatures: { + jsx: true, // Allows for the parsing of JSX + }, + }, + settings: { + react: { + version: 'detect', // Tells eslint-plugin-react to automatically detect the version of React to use + }, + }, + extends: [ + 'plugin:react/recommended', // Uses the recommended rules from @eslint-plugin-react + 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. + ], + plugins: ['react', 'react-hooks'], + rules: { + // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs + // e.g. "@typescript-eslint/explicit-function-return-type": "off", + }, +} diff --git a/README.md b/README.md index 5ca7edd8c..5cacb3465 100644 --- a/README.md +++ b/README.md @@ -1,185 +1,182 @@ -# CoreUI Free React Admin Template [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social&logo=twitter)](https://twitter.com/intent/tweet?text=CoreUI%20-%20Free%React%204%20Admin%20Template%20&url=https://coreui.io&hashtags=bootstrap,admin,template,dashboard,panel,free,angular,react,vue) - -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](https://opensource.org/licenses/MIT) -[![@coreui coreui](https://img.shields.io/badge/@coreui%20-coreui-lightgrey.svg?style=flat-square)](https://github.com/coreui/coreui) -[![npm package][npm-coreui-badge]][npm-coreui] -[![NPM downloads][npm-coreui-download]][npm-coreui] -[![@coreui react](https://img.shields.io/badge/@coreui%20-react-lightgrey.svg?style=flat-square)](https://github.com/coreui/react) -[![npm package][npm-coreui-react-badge]][npm-coreui-react] -[![NPM downloads][npm-coreui-react-download]][npm-coreui-react] - -[npm-coreui]: https://www.npmjs.com/package/@coreui/coreui -[npm-coreui-badge]: https://img.shields.io/npm/v/@coreui/coreui.png?style=flat-square -[npm-coreui-download]: https://img.shields.io/npm/dm/@coreui/coreui.svg?style=flat-square -[npm-coreui-react]: https://www.npmjs.com/package/@coreui/react -[npm-coreui-react-badge]: https://img.shields.io/npm/v/@coreui/react.png?style=flat-square -[npm-coreui-react-download]: https://img.shields.io/npm/dm/@coreui/react.svg?style=flat-square -[npm]: https://www.npmjs.com/package/@coreui/react - -[![Bootstrap Admin Template](https://assets.coreui.io/products/coreui-free-bootstrap-admin-template-light-dark.webp)](https://coreui.io/product/free-react-admin-template/) - -CoreUI is meant to be the UX game changer. Pure & transparent code is devoid of redundant components, so the app is light enough to offer ultimate user experience. This means mobile devices also, where the navigation is just as easy and intuitive as on a desktop or laptop. The CoreUI Layout API lets you customize your project for almost any device – be it Mobile, Web or WebApp – CoreUI covers them all! - -## Table of Contents - -* [Versions](#versions) -* [CoreUI PRO](#coreui-pro) -* [CoreUI PRO React Admin Templates](#coreui-pro-react-admin-templates) -* [Quick Start](#quick-start) -* [Installation](#installation) -* [Basic usage](#basic-usage) -* [What's included](#whats-included) -* [Documentation](#documentation) -* [Versioning](#versioning) -* [Creators](#creators) -* [Community](#community) -* [Support CoreUI Development](#support-coreui-development) -* [Copyright and License](#copyright-and-license) - -## Versions - -* [CoreUI Free Bootstrap Admin Template](https://github.com/coreui/coreui-free-bootstrap-admin-template) -* [CoreUI Free Angular Admin Template](https://github.com/coreui/coreui-free-angular-admin-template) -* [CoreUI Free React.js Admin Template (Vite)](https://github.com/coreui/coreui-free-react-admin-template) -* [CoreUI Free React.js Admin Template (Create React App)](https://github.com/coreui/coreui-free-react-admin-template-cra) -* [CoreUI Free Vue.js Admin Template](https://github.com/coreui/coreui-free-vue-admin-template) - -## CoreUI PRO - -* 💪 [CoreUI PRO Angular Admin Template](https://coreui.io/product/angular-dashboard-template/) -* 💪 [CoreUI PRO Bootstrap Admin Template](https://coreui.io/product/bootstrap-dashboard-template/) -* 💪 [CoreUI PRO Next.js Admin Template](https://coreui.io/product/next-js-dashboard-template/) -* 💪 [CoreUI PRO React Admin Template](https://coreui.io/product/react-dashboard-template/) -* 💪 [CoreUI PRO Vue Admin Template](https://coreui.io/product/vue-dashboard-template/) - -## CoreUI PRO React Admin Templates - -| Default Theme | Light Theme | -| --- | --- | -| [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_default_light_dark.webp)](https://coreui.io/product/react-dashboard-template/?theme=default) | [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_light_light_dark.webp)](https://coreui.io/product/react-dashboard-template/?theme=light)| - -| Modern Theme | Bright Theme | -| --- | --- | -| [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_default_v3_light_dark.webp)](https://coreui.io/product/react-dashboard-template/?theme=modern) | [![CoreUI PRO React Admin Template](https://coreui.io/images/templates/coreui_pro_light_v3_light_dark.webp)](https://coreui.io/product/react-dashboard-template/?theme=bright)| - -## Quick Start - -- [Download the latest release](https://github.com/coreui/coreui-free-react-admin-template/archive/refs/heads/main.zip) -- Clone the repo: `git clone https://github.com/coreui/coreui-free-react-admin-template.git` - -### Installation - -``` bash -$ npm install -``` +# Chorvoq GIS – Land Territory Management App -or +Chorvoq GIS is a full-stack web-based GIS (Geographic Information System) tool for managing land territories. It enables users to draw, label, and organize territorial boundaries on an interactive map. Ideal for government and businesses needing to track and manage land usage effectively. -``` bash -$ yarn install -``` +--- -### Basic usage +## 🌍 Features -``` bash -# dev server with hot reload at http://localhost:3000 -$ npm start -``` +- 📍 **Add & Save Territories** – Draw custom polygon shapes on the map and label them. +- 🗂️ **Layer Management** – Organize shapes under map layers. Each map view is a separate "layer". +- 🔎 **Searchable Layers** – Quickly locate and manage map elements. +- ✅ **Status Tracking** – Monitor creation status of each layer. +- 🗺️ **GeoServer Map Integration** – Visualize maps using GeoServer and satellite imagery. +- 🧩 **Full Stack** – React frontend, Spring Boot backend, PostgreSQL database. -or +--- -``` bash -# dev server with hot reload at http://localhost:3000 -$ yarn start -``` +## 📸 Interface Preview + +![Map Interaction](./public/map.png) +*Map with Drawing and Shape Management* + +![Layer Management](./public/layers.png) +*Layer Management Panel* + +--- + +## ⚙️ How It Works + +1. **Upload Layer** – Add a new map layer (WMS) to the system. +2. **Draw Shape** – Mark territories on the map using polygon drawing tools. +3. **Add Metadata** – Fill in details like `objectid`, `fid_shape`, `created_by`, and more. +4. **Save & Display** – Shapes are saved in the database and visualized on the map. + +Each **layer** contains multiple **shapes**, each linked to metadata and managed through the app. + +--- -Navigate to [http://localhost:3000](http://localhost:3000). The app will automatically reload if you change any of the source files. +## 🛠️ Tech Stack -#### Build +- **Frontend**: React +- **Backend**: Spring Boot – [Backend Repository](https://github.com/MuhammadayubErkinoff/FinalProject.git) +- **Map Source**: GeoServer (WMS Layers) +- **Database**: PostgreSQL +- **Containerization**: Docker + Docker Compose -Run `build` to build the project. The build artifacts will be stored in the `build/` directory. +--- + +## 🚀 Getting Started + +### 📦 Backend Setup ```bash -# build for production with minification -$ npm run build +git clone https://github.com/MuhammadayubErkinoff/FinalProject.git +cd FinalProject +./mvnw package -DskipTests +docker compose build +docker compose up -d ``` -or +This will build the Spring Boot backend, start PostgreSQL, and run all services. + +### 💻 Frontend Setup ```bash -# build for production with minification -$ yarn build +git clone https://github.com/Abdulhafiz0512/web2.git +cd web2 +npm install +npm run dev ``` -## What's included +The frontend will be available at `http://localhost:3000`. -Within the download you'll find the following directories and files, logically grouping common assets and providing both compiled and minified variations. You'll see something like this: +--- + +## 📂 Project Structure (Frontend) ``` -coreui-free-react-admin-template -├── public/ # static files -│ ├── favicon.ico -│ └── manifest.json -│ -├── src/ # project root -│ ├── assets/ # images, icons, etc. -│ ├── components/ # common components - header, footer, sidebar, etc. -│ ├── layouts/ # layout containers -│ ├── scss/ # scss styles -│ ├── views/ # application views -│ ├── _nav.js # sidebar navigation config +web2/ +├── css/ +├── eslint.config.mjs +├── index.html +├── LICENSE +├── node_modules/ +├── package.json +├── package-lock.json +├── public/ +├── README.md +├── src/ │ ├── App.js │ ├── index.js -│ ├── routes.js # routes config -│ └── store.js # template state example -│ -├── index.html # html template -├── ... -├── package.json -├── ... -└── vite.config.mjs # vite config +│ ├── _nav.js +│ ├── routes.js +│ ├── app/ +│ │ └── store.js +│ ├── assets/ +│ │ ├── brand/ +│ │ └── images/ +│ ├── components/ +│ │ ├── AppBreadcrumb.js +│ │ ├── AppContent.js +│ │ ├── AppFooter.js +│ │ ├── AppHeader.js +│ │ ├── AppSidebar.js +│ │ ├── AppSidebarNav.js +│ │ ├── DocsComponents.js +│ │ ├── DocsExample.js +│ │ ├── DocsIcons.js +│ │ ├── DocsLink.js +│ │ ├── header/ +│ │ └── index.js +│ ├── features/ +│ │ ├── access/ +│ │ └── auth/ +│ ├── layout/ +│ │ └── DefaultLayout.js +│ ├── scss/ +│ │ ├── _custom.scss +│ │ ├── _theme.scss +│ │ ├── _variables.scss +│ │ ├── examples.scss +│ │ ├── style.scss +│ │ └── vendors/ +│ ├── utils/ +│ │ ├── api/ +│ │ └── navigation/ +│ └── views/ +│ ├── about/ +│ ├── base/ +│ ├── buttons/ +│ ├── charts/ +│ ├── contact/ +│ ├── dashboard/ +│ ├── departments/ +│ ├── forms/ +│ ├── layers/ +│ ├── map/ +│ ├── notifications/ +│ ├── pages/ +│ ├── profile/ +│ ├── roles/ +│ ├── theme/ +│ ├── users/ +│ └── widgets/ +└── vite.config.mjs ``` -## Documentation - -The documentation for the CoreUI Admin Template is hosted at our website [CoreUI for React](https://coreui.io/react/docs/templates/installation/) - -## Versioning - -For transparency into our release cycle and in striving to maintain backward compatibility, CoreUI Free Admin Template is maintained under [the Semantic Versioning guidelines](http://semver.org/). - -See [the Releases section of our project](https://github.com/coreui/coreui-free-react-admin-template/releases) for changelogs for each release version. - -## Creators +--- -**Łukasz Holeczek** +## ✅ Future Improvements -* -* +- 📤 Export shapes as GeoJSON or PDF +- 🌐 Multilingual interface +- 📲 GPS location tracking & mobile support -**Andrzej Kopański** +--- -* +## 🧑‍💼 Use Cases -**CoreUI Team** +- Government land allocation systems +- Business & agricultural land management +- Environmental monitoring zones +- Utility infrastructure mapping -* -* -* +--- -## Community +## 🤝 Contributing -Get updates on CoreUI's development and chat with the project maintainers and community members. +We welcome contributions! Fork the repo, make your changes, and submit a pull request. -- Follow [@core_ui on Twitter](https://twitter.com/core_ui). -- Read and subscribe to [CoreUI Blog](https://coreui.ui/blog/). +--- -## Support CoreUI Development +## 📬 Contact -CoreUI is an MIT-licensed open source project and is completely free to use. However, the amount of effort needed to maintain and develop new features for the project is not sustainable without proper financial backing. You can support development by buying the [CoreUI PRO](https://coreui.io/pricing/?framework=react&src=github-coreui-free-react-admin-template) or by becoming a sponsor via [Open Collective](https://opencollective.com/coreui/). +Developed by **Muhammadayub Erkinov** +For inquiries or support, feel free to reach out! -## Copyright and License +--- -copyright 2025 creativeLabs Łukasz Holeczek. +## 📄 License -Code released under [the MIT license](https://github.com/coreui/coreui-free-react-admin-template/blob/main/LICENSE). \ No newline at end of file +This project is open source and available under the [MIT License](LICENSE). diff --git a/css/simplebar.css b/css/simplebar.css new file mode 100644 index 000000000..c67427a6b --- /dev/null +++ b/css/simplebar.css @@ -0,0 +1,5 @@ +.simplebar-content { + display: flex; + flex-direction: column; + min-height: 100%; +}/*# sourceMappingURL=simplebar.css.map */ \ No newline at end of file diff --git a/css/simplebar.css.map b/css/simplebar.css.map new file mode 100644 index 000000000..ba7fc8be2 --- /dev/null +++ b/css/simplebar.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../src/scss/vendors/simplebar.scss"],"names":[],"mappings":"AAAA;EACE;EACA;EACA","file":"simplebar.css"} \ No newline at end of file diff --git a/css/simplebar.min.css b/css/simplebar.min.css new file mode 100644 index 000000000..ce657866b --- /dev/null +++ b/css/simplebar.min.css @@ -0,0 +1 @@ +.simplebar-content{display:flex;flex-direction:column;min-height:100%}/*# sourceMappingURL=simplebar.min.css.map */ \ No newline at end of file diff --git a/css/simplebar.min.css.map b/css/simplebar.min.css.map new file mode 100644 index 000000000..963af7e7e --- /dev/null +++ b/css/simplebar.min.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["../src/scss/vendors/simplebar.scss"],"names":[],"mappings":"AAAA,mBACE,aACA,sBACA","file":"simplebar.min.css"} \ No newline at end of file diff --git a/index.html b/index.html index 9613ef3ee..b3ebae913 100644 --- a/index.html +++ b/index.html @@ -1,9 +1,9 @@ @@ -14,7 +14,7 @@ - CoreUI Free React.js Admin Template + Chorvoq GIS diff --git a/package.json b/package.json index 7e992e447..f54569138 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@coreui/coreui-free-react-admin-template", - "version": "5.4.0", + "version": "5.2.0", "description": "CoreUI Free React Admin Template", "homepage": ".", "bugs": { @@ -14,42 +14,51 @@ "author": "The CoreUI Team (https://github.com/orgs/coreui/people)", "scripts": { "build": "vite build", - "lint": "eslint", + "lint": "eslint \"src/**/*.js\"", "serve": "vite preview", - "start": "vite" + "start": "vite", + "start:docker": "vite --host" }, "dependencies": { - "@coreui/chartjs": "^4.1.0", - "@coreui/coreui": "^5.3.1", + "@coreui/chartjs": "^4.0.0", + "@coreui/coreui": "^5.2.0", "@coreui/icons": "^3.0.1", "@coreui/icons-react": "^2.3.0", - "@coreui/react": "^5.5.0", + "@coreui/react": "^5.4.1", "@coreui/react-chartjs": "^3.0.0", "@coreui/utils": "^2.0.2", "@popperjs/core": "^2.11.8", - "chart.js": "^4.4.7", + "@reduxjs/toolkit": "^2.5.1", + "axios": "^1.7.9", + "bootstrap": "^5.3.3", + "chart.js": "^4.4.6", "classnames": "^2.5.1", - "core-js": "^3.40.0", + "core-js": "^3.39.0", + "history": "^5.3.0", + "ol": "^10.4.0", "prop-types": "^15.8.1", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-redux": "^9.2.0", - "react-router-dom": "^7.1.5", + "rc-tree": "^5.13.0", + "react": "^18.3.1", + "react-bootstrap": "^2.10.9", + "react-dom": "^18.3.1", + "react-icons": "^5.4.0", + "react-redux": "^9.1.2", + "react-router-dom": "^6.28.0", + "react-select": "^5.10.0", "redux": "5.0.1", - "simplebar-react": "^3.3.0" + "simplebar-react": "^3.2.6" }, "devDependencies": { - "@vitejs/plugin-react": "^4.3.4", + "@vitejs/plugin-react": "^4.3.3", "autoprefixer": "^10.4.20", - "eslint": "^9.20.1", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-prettier": "^5.2.3", - "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", - "globals": "^15.15.0", - "postcss": "^8.5.2", - "prettier": "3.5.1", - "sass": "^1.85.0", - "vite": "^6.1.0" + "eslint": "^8.57.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.2.1", + "eslint-plugin-react": "^7.37.2", + "eslint-plugin-react-hooks": "^4.6.2", + "postcss": "^8.4.49", + "prettier": "3.3.3", + "sass": "^1.81.0", + "vite": "5.4.14" } } diff --git a/public/layers.png b/public/layers.png new file mode 100644 index 000000000..92e3a7b5f Binary files /dev/null and b/public/layers.png differ diff --git a/public/login.png b/public/login.png new file mode 100644 index 000000000..b424219ee Binary files /dev/null and b/public/login.png differ diff --git a/public/map.png b/public/map.png new file mode 100644 index 000000000..7eb5c50f3 Binary files /dev/null and b/public/map.png differ diff --git a/src/App.js b/src/App.js index f5b22393e..b90fdf4ca 100644 --- a/src/App.js +++ b/src/App.js @@ -1,11 +1,12 @@ -import React, { Suspense, useEffect } from 'react' -import { HashRouter, Route, Routes } from 'react-router-dom' -import { useSelector } from 'react-redux' - +import React, { Suspense, useEffect, useState } from 'react' +import { BrowserRouter as Router, Route, Routes } from 'react-router-dom' +import { useSelector, useDispatch } from 'react-redux' import { CSpinner, useColorModes } from '@coreui/react' +import { useNavigate } from 'react-router-dom' +import { setupAxiosInterceptors } from './utils/api/axiosConfig' +import { handleLogout } from './features/auth/authSlice' +import { checkAuthState } from './features/auth/authSlice' // Add this import import './scss/style.scss' - -// We use those styles to show code examples, you should remove them in your application. import './scss/examples.scss' // Containers @@ -17,29 +18,59 @@ const Register = React.lazy(() => import('./views/pages/register/Register')) const Page404 = React.lazy(() => import('./views/pages/page404/Page404')) const Page500 = React.lazy(() => import('./views/pages/page500/Page500')) +import ProtectedRoute from './features/auth/ProtectedRoute' + const App = () => { const { isColorModeSet, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') - const storedTheme = useSelector((state) => state.theme) + const storedTheme = useSelector((state) => state.ui.theme) // Changed from state.theme to state.ui.theme + const isAuthenticated = useSelector((state) => state.auth.isAuthenticated) + const [authChecked, setAuthChecked] = useState(false) + + const dispatch = useDispatch() + // Check authentication state on mount useEffect(() => { - const urlParams = new URLSearchParams(window.location.href.split('?')[1]) - const theme = urlParams.get('theme') && urlParams.get('theme').match(/^[A-Za-z0-9\s]+/)[0] - if (theme) { + dispatch(checkAuthState()) + setAuthChecked(true) + }, [dispatch]) + + // Setup axios interceptors + useEffect(() => { + const handleUnauthorized = () => { + dispatch(handleLogout()) + window.location.href = '/login' + } + + setupAxiosInterceptors(handleUnauthorized) + }, [dispatch]) + + + useEffect(() => { + const urlParams = new URLSearchParams(window.location.search) + const theme = urlParams.get('theme') + + if (theme && /^[A-Za-z0-9\s]+$/.test(theme)) { setColorMode(theme) } - if (isColorModeSet()) { - return + if (!isColorModeSet()) { + setColorMode(storedTheme) } + }, [isColorModeSet, setColorMode, storedTheme]) - setColorMode(storedTheme) - }, []) // eslint-disable-line react-hooks/exhaustive-deps + if (!authChecked) { + return ( +
+ +
+ ) + } return ( - + +
} @@ -49,11 +80,23 @@ const App = () => { } /> } /> } /> - } /> + + {/* Protected Routes */} + + + + } + /> + + {/* Catch-all 404 Route */} + } />
-
+ ) } -export default App +export default App \ No newline at end of file diff --git a/src/_nav.js b/src/_nav.js index 9f8ca150b..19acfaf81 100644 --- a/src/_nav.js +++ b/src/_nav.js @@ -1,451 +1,71 @@ -import React from 'react' -import CIcon from '@coreui/icons-react' -import { - cilBell, - cilCalculator, - cilChartPie, - cilCursor, - cilDescription, - cilDrop, - cilExternalLink, - cilNotes, - cilPencil, - cilPuzzle, - cilSpeedometer, - cilStar, -} from '@coreui/icons' -import { CNavGroup, CNavItem, CNavTitle } from '@coreui/react' +// _nav.js +import React from 'react'; +import CIcon from '@coreui/icons-react'; +import { cilSpeedometer, cilMap, cilUser, cilDescription, cilEducation, cilLayers, cilPhone } from '@coreui/icons'; +import { CNavGroup, CNavItem } from '@coreui/react'; const _nav = [ { component: CNavItem, name: 'Dashboard', to: '/dashboard', - icon: , - badge: { - color: 'info', - text: 'NEW', - }, - }, - { - component: CNavTitle, - name: 'Theme', - }, - { - component: CNavItem, - name: 'Colors', - to: '/theme/colors', - icon: , - }, - { - component: CNavItem, - name: 'Typography', - to: '/theme/typography', - icon: , - }, - { - component: CNavTitle, - name: 'Components', + icon: , + permissions: { actionName: 'Dashboard' }, }, { component: CNavGroup, - name: 'Base', - to: '/base', - icon: , + name: 'HRM', + icon: , items: [ - { - component: CNavItem, - name: 'Accordion', - to: '/base/accordion', - }, - { - component: CNavItem, - name: 'Breadcrumb', - to: '/base/breadcrumbs', - }, - { - component: CNavItem, - name: ( - - {'Calendar'} - - - ), - href: 'https://coreui.io/react/docs/components/calendar/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Cards', - to: '/base/cards', - }, - { - component: CNavItem, - name: 'Carousel', - to: '/base/carousels', - }, - { - component: CNavItem, - name: 'Collapse', - to: '/base/collapses', - }, - { - component: CNavItem, - name: 'List group', - to: '/base/list-groups', - }, - { - component: CNavItem, - name: 'Navs & Tabs', - to: '/base/navs', - }, - { - component: CNavItem, - name: 'Pagination', - to: '/base/paginations', - }, - { - component: CNavItem, - name: 'Placeholders', - to: '/base/placeholders', - }, - { - component: CNavItem, - name: 'Popovers', - to: '/base/popovers', - }, - { - component: CNavItem, - name: 'Progress', - to: '/base/progress', - }, - { - component: CNavItem, - name: 'Smart Pagination', - href: 'https://coreui.io/react/docs/components/smart-pagination/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: ( - - {'Smart Table'} - - - ), - href: 'https://coreui.io/react/docs/components/smart-table/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Spinners', - to: '/base/spinners', - }, - { - component: CNavItem, - name: 'Tables', - to: '/base/tables', - }, - { - component: CNavItem, - name: 'Tabs', - to: '/base/tabs', - }, - { - component: CNavItem, - name: 'Tooltips', - to: '/base/tooltips', - }, - { - component: CNavItem, - name: ( - - {'Virtual Scroller'} - - - ), - href: 'https://coreui.io/react/docs/components/virtual-scroller/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - ], - }, - { - component: CNavGroup, - name: 'Buttons', - to: '/buttons', - icon: , - items: [ - { - component: CNavItem, - name: 'Buttons', - to: '/buttons/buttons', - }, - { - component: CNavItem, - name: 'Buttons groups', - to: '/buttons/button-groups', - }, - { - component: CNavItem, - name: 'Dropdowns', - to: '/buttons/dropdowns', - }, - { - component: CNavItem, - name: ( - - {'Loading Button'} - - - ), - href: 'https://coreui.io/react/docs/components/loading-button/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - ], - }, - { - component: CNavGroup, - name: 'Forms', - icon: , - items: [ - { - component: CNavItem, - name: 'Form Control', - to: '/forms/form-control', - }, - { - component: CNavItem, - name: 'Select', - to: '/forms/select', - }, - { - component: CNavItem, - name: ( - - {'Multi Select'} - - - ), - href: 'https://coreui.io/react/docs/forms/multi-select/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Checks & Radios', - to: '/forms/checks-radios', - }, - { - component: CNavItem, - name: 'Range', - to: '/forms/range', - }, - { - component: CNavItem, - name: ( - - {'Range Slider'} - - - ), - href: 'https://coreui.io/react/docs/forms/range-slider/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: ( - - {'Rating'} - - - ), - href: 'https://coreui.io/react/docs/forms/rating/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Input Group', - to: '/forms/input-group', - }, - { - component: CNavItem, - name: 'Floating Labels', - to: '/forms/floating-labels', - }, - { - component: CNavItem, - name: ( - - {'Date Picker'} - - - ), - href: 'https://coreui.io/react/docs/forms/date-picker/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Date Range Picker', - href: 'https://coreui.io/react/docs/forms/date-range-picker/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: ( - - {'Time Picker'} - - - ), - href: 'https://coreui.io/react/docs/forms/time-picker/', - badge: { - color: 'danger', - text: 'PRO', - }, - }, - { - component: CNavItem, - name: 'Layout', - to: '/forms/layout', - }, - { - component: CNavItem, - name: 'Validation', - to: '/forms/validation', - }, + + { component: CNavItem, name: 'Departments', to: '/departments', permissions: { actionName: 'Seeing Departments', actionName2:"Departments Management" } }, + { component: CNavItem, name: 'Roles', to: '/roles', permissions: { actionName: 'Seeing roles', actionName2:"Role Management" } }, + { component: CNavItem, name: 'Users', to: '/users', permissions: { actionName: 'Seeing users', actionName2:'Users Management' } }, ], }, { component: CNavItem, - name: 'Charts', - to: '/charts', - icon: , - }, - { - component: CNavGroup, - name: 'Icons', - icon: , - items: [ - { - component: CNavItem, - name: 'CoreUI Free', - to: '/icons/coreui-icons', - }, - { - component: CNavItem, - name: 'CoreUI Flags', - to: '/icons/flags', - }, - { - component: CNavItem, - name: 'CoreUI Brands', - to: '/icons/brands', - }, - ], - }, - { - component: CNavGroup, - name: 'Notifications', - icon: , - items: [ - { - component: CNavItem, - name: 'Alerts', - to: '/notifications/alerts', - }, - { - component: CNavItem, - name: 'Badges', - to: '/notifications/badges', - }, - { - component: CNavItem, - name: 'Modal', - to: '/notifications/modals', - }, - { - component: CNavItem, - name: 'Toasts', - to: '/notifications/toasts', - }, - ], + name: 'Map', + to: '/map', + icon: , + permissions: { actionName: "Xaritani ko'rish",actionName2:"Map Management"}, }, { component: CNavItem, - name: 'Widgets', - to: '/widgets', - icon: , - badge: { - color: 'info', - text: 'NEW', - }, + name: 'Layers', + to: '/layers', + icon: , + permissions: { actionName:"Map Management"}, }, { - component: CNavTitle, - name: 'Extras', - }, - { - component: CNavGroup, - name: 'Pages', - icon: , - items: [ - { - component: CNavItem, - name: 'Login', - to: '/login', - }, - { - component: CNavItem, - name: 'Register', - to: '/register', - }, - { - component: CNavItem, - name: 'Error 404', - to: '/404', - }, - { - component: CNavItem, - name: 'Error 500', - to: '/500', - }, - ], + component: CNavItem, + name: 'Contact', + to: '/contact', + icon: , + permissions: { actionName: 'Dashboard' }, }, { component: CNavItem, - name: 'Docs', - href: 'https://coreui.io/react/docs/templates/installation/', - icon: , - }, -] + name: 'About', + to: '/about', + icon: , + permissions: { actionName: 'Dashboard' }, + } + // { + // component: CNavItem, + // name: 'Instructor', + // to: '/instructor', + // icon: , + // permissions: { actionName: "CMS Management" }, + // }, + // { + // component: CNavItem, + // name: 'Instructions', + // to: '/instructions', + // icon: , + // permissions: { actionName: "CMS Management", actionName2:"Seeing Instructions" }, + // }, + +]; -export default _nav +export default _nav; diff --git a/src/app/store.js b/src/app/store.js new file mode 100644 index 000000000..c23c6568d --- /dev/null +++ b/src/app/store.js @@ -0,0 +1,35 @@ +import { configureStore, createSlice } from '@reduxjs/toolkit'; +import authReducer from '../features/auth/authSlice'; + +const loadUIState = () => { + try { + const savedState = localStorage.getItem('uiState'); + return savedState ? JSON.parse(savedState) : { sidebarShow: true, theme: 'light' }; + } catch (error) { + console.error('Failed to load UI state:', error); + return { sidebarShow: true, theme: 'light' }; + } +}; + +const uiSlice = createSlice({ + name: 'ui', + initialState: loadUIState(), + reducers: { + set: (state, action) => { + const newState = { ...state, ...action.payload }; + localStorage.setItem('uiState', JSON.stringify(newState)); + return newState; + }, + }, +}); + +export const { set } = uiSlice.actions; + +const store = configureStore({ + reducer: { + ui: uiSlice.reducer, + auth: authReducer, + }, +}); + +export default store; diff --git a/src/assets/images/avatars/8.jpg b/src/assets/images/avatars/8.jpg index 4e8b48d4f..96ffdc515 100644 Binary files a/src/assets/images/avatars/8.jpg and b/src/assets/images/avatars/8.jpg differ diff --git a/src/components/AppContent.js b/src/components/AppContent.js index b9a39ef50..b2ad4cd2e 100644 --- a/src/components/AppContent.js +++ b/src/components/AppContent.js @@ -1,33 +1,41 @@ -import React, { Suspense } from 'react' -import { Navigate, Route, Routes } from 'react-router-dom' -import { CContainer, CSpinner } from '@coreui/react' - -// routes config -import routes from '../routes' +import React, { Suspense, useMemo } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import { CContainer, CSpinner } from '@coreui/react'; +import { useSelector } from 'react-redux'; +import routes from '../routes'; +import Page404 from '../views/pages/page404/Page404'; +import { filterAccessibleRoutes } from '../features/access/permission'; const AppContent = () => { + const currentUser = useSelector(state => state.auth.user); + + const accessibleRoutes = useMemo(() => filterAccessibleRoutes(routes, currentUser), [routes, currentUser]); + return ( - - }> + + + + + }> - {routes.map((route, idx) => { - return ( - route.element && ( - } - /> - ) + {accessibleRoutes.map((route, idx) => ( + route.element && ( + } /> ) - })} - } /> + ))} + + {accessibleRoutes.some(route => route.path === '/dashboard') ? ( + } /> + ) : ( + 0 ? : } /> + )} + + } /> - ) -} + ); +}; -export default React.memo(AppContent) +export default React.memo(AppContent); \ No newline at end of file diff --git a/src/components/AppFooter.js b/src/components/AppFooter.js index 217c5a04c..fd126f460 100644 --- a/src/components/AppFooter.js +++ b/src/components/AppFooter.js @@ -8,7 +8,7 @@ const AppFooter = () => { CoreUI - © 2025 creativeLabs. + © 2024 creativeLabs.
Powered by diff --git a/src/components/AppHeader.js b/src/components/AppHeader.js index b10bd7e12..c09cd28c2 100644 --- a/src/components/AppHeader.js +++ b/src/components/AppHeader.js @@ -1,6 +1,5 @@ -import React, { useEffect, useRef } from 'react' -import { NavLink } from 'react-router-dom' -import { useSelector, useDispatch } from 'react-redux' +import React, { useEffect, useRef } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; import { CContainer, CDropdown, @@ -10,77 +9,56 @@ import { CHeader, CHeaderNav, CHeaderToggler, - CNavLink, - CNavItem, useColorModes, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' +} from '@coreui/react'; +import CIcon from '@coreui/icons-react'; import { - cilBell, cilContrast, - cilEnvelopeOpen, - cilList, cilMenu, cilMoon, cilSun, -} from '@coreui/icons' +} from '@coreui/icons'; -import { AppBreadcrumb } from './index' -import { AppHeaderDropdown } from './header/index' +import { set } from '../app/store'; +import { AppBreadcrumb } from './index'; +import { AppHeaderDropdown } from './header/index'; const AppHeader = () => { - const headerRef = useRef() - const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme') + const headerRef = useRef(null); + const { colorMode, setColorMode } = useColorModes('coreui-free-react-admin-template-theme'); - const dispatch = useDispatch() - const sidebarShow = useSelector((state) => state.sidebarShow) + const dispatch = useDispatch(); + const sidebarShow = useSelector((state) => state.ui.sidebarShow); // Updated selector useEffect(() => { - document.addEventListener('scroll', () => { - headerRef.current && - headerRef.current.classList.toggle('shadow-sm', document.documentElement.scrollTop > 0) - }) - }, []) + const handleScroll = () => { + if (headerRef.current) { + headerRef.current.classList.toggle('shadow-sm', document.documentElement.scrollTop > 0); + } + }; + + document.addEventListener('scroll', handleScroll); + + return () => { + document.removeEventListener('scroll', handleScroll); + }; + }, []); return ( - - + + dispatch({ type: 'set', sidebarShow: !sidebarShow })} + onClick={() => dispatch(set({ sidebarShow: !sidebarShow }))} style={{ marginInlineStart: '-14px' }} > - - - - Dashboard - - - - Users - - - Settings - - - - - - - - - - - - - - - - - - - + + {/* Breadcrumb Navigation */} +
+ +
+
  • @@ -131,11 +109,8 @@ const AppHeader = () => { - - - - ) -} + ); +}; -export default AppHeader +export default AppHeader; diff --git a/src/components/AppSidebar.js b/src/components/AppSidebar.js index 021cb52c3..3f4952df5 100644 --- a/src/components/AppSidebar.js +++ b/src/components/AppSidebar.js @@ -1,59 +1,40 @@ -import React from 'react' -import { useSelector, useDispatch } from 'react-redux' - -import { - CCloseButton, - CSidebar, - CSidebarBrand, - CSidebarFooter, - CSidebarHeader, - CSidebarToggler, -} from '@coreui/react' -import CIcon from '@coreui/icons-react' - -import { AppSidebarNav } from './AppSidebarNav' - -import { logo } from 'src/assets/brand/logo' -import { sygnet } from 'src/assets/brand/sygnet' - -// sidebar nav config -import navigation from '../_nav' +// AppSidebar.js +import React, { useMemo } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { CCloseButton, CSidebar, CSidebarBrand, CSidebarFooter, CSidebarHeader } from '@coreui/react'; +import { AppSidebarNav } from './AppSidebarNav'; +import { set } from '../app/store'; +import navigation from '../_nav'; +import { filterAccessibleNavItems } from "../features/access/permission"; const AppSidebar = () => { - const dispatch = useDispatch() - const unfoldable = useSelector((state) => state.sidebarUnfoldable) - const sidebarShow = useSelector((state) => state.sidebarShow) + const dispatch = useDispatch(); + const unfoldable = useSelector(state => state.ui.sidebarUnfoldable); + const sidebarShow = useSelector(state => state.ui.sidebarShow); + const theme = useSelector(state => state.ui.theme); + const currentUser = useSelector(state => state.auth.user); + + const filteredNavItems = filterAccessibleNavItems(navigation, currentUser) return ( { - dispatch({ type: 'set', sidebarShow: visible }) - }} + onVisibleChange={(visible) => dispatch(set({ sidebarShow: visible }))} > - - + {unfoldable ? :

    Chorvoq

    }
    - dispatch({ type: 'set', sidebarShow: false })} - /> + dispatch(set({ sidebarShow: false }))} />
    - - - dispatch({ type: 'set', sidebarUnfoldable: !unfoldable })} - /> - + +
    - ) -} + ); +}; -export default React.memo(AppSidebar) +export default React.memo(AppSidebar); \ No newline at end of file diff --git a/src/components/AppSidebarNav.js b/src/components/AppSidebarNav.js index 7583abf49..8ee3beee9 100644 --- a/src/components/AppSidebarNav.js +++ b/src/components/AppSidebarNav.js @@ -53,7 +53,7 @@ export const AppSidebarNav = ({ items }) => { const Component = component return ( - {items?.map((item, index) => + {item.items?.map((item, index) => item.items ? navGroup(item, index) : navItem(item, index, true), )} diff --git a/src/components/header/AppHeaderDropdown.js b/src/components/header/AppHeaderDropdown.js index 30c0df82b..dfea236f5 100644 --- a/src/components/header/AppHeaderDropdown.js +++ b/src/components/header/AppHeaderDropdown.js @@ -1,7 +1,6 @@ -import React from 'react' +import React, { useEffect, useState } from 'react' import { CAvatar, - CBadge, CDropdown, CDropdownDivider, CDropdownHeader, @@ -9,59 +8,48 @@ import { CDropdownMenu, CDropdownToggle, } from '@coreui/react' -import { - cilBell, - cilCreditCard, - cilCommentSquare, - cilEnvelopeOpen, - cilFile, - cilLockLocked, - cilSettings, - cilTask, - cilUser, -} from '@coreui/icons' +import { cilUser, cilSettings, cilAccountLogout } from '@coreui/icons' import CIcon from '@coreui/icons-react' - +import { handleLogout } from '../../features/auth/authSlice' import avatar8 from './../../assets/images/avatars/8.jpg' +import { useDispatch, useSelector } from 'react-redux' +import axiosInstance from '../../utils/api/axiosConfig' const AppHeaderDropdown = () => { + const dispatch = useDispatch() + const user = useSelector((state) => state.auth.user) + // const [departmentName, setDepartmentName] = useState('') + + // useEffect(() => { + // const fetchDepartments=async()=>{ + // if (user?.departmentId) { + // const response = await axiosInstance.get(`api/departments/${user.departmentId}`) + // setDepartmentName(response.data.name) + // }} + // fetchDepartments() + + // }, [user?.departmentId]) + + const logout = () => { + dispatch(handleLogout()) + } + return ( - Account - - - Updates - - 42 - - - - - Messages - - 42 - - - - - Tasks - - 42 - - - - - Comments - - 42 - - - Settings - + + + {user.firstName} {user.lastName}
    + {user.role?.name}
    + +
    + + + + Profile @@ -69,24 +57,11 @@ const AppHeaderDropdown = () => { Settings
    - - - Payments - - 42 - - - - - Projects - - 42 - - + - - - Lock Account + + + Log Out
    diff --git a/src/features/access/permission.js b/src/features/access/permission.js new file mode 100644 index 000000000..e0ae5a485 --- /dev/null +++ b/src/features/access/permission.js @@ -0,0 +1,45 @@ +// utils/permissions.js +export const checkUserPermission = (user, path, actionName) => { + const defaultAccessiblePaths = ['/dashboard','/profile','/about','/contact']; + if (defaultAccessiblePaths.includes(path)) { + return true; + } + if (!user?.role?.actions) return false; + + if (actionName) { + + return user.role.actions.some(action => action.name === actionName); + } + + return false; + }; + + export const filterAccessibleRoutes = (routes, user) => { + return routes.filter(route => + checkUserPermission( + user, + route.path, + route.permissions?.actionName + )||checkUserPermission( user, + route.path, + route.permissions?.actionName2) + ); + }; + + export const filterAccessibleNavItems = (navItems, user) => { + return navItems + .map(item => { + if (item.items) { + + const filteredItems = filterAccessibleNavItems([...item.items], user); + return filteredItems.length > 0 ? { ...item, items: filteredItems } : null; + } + return checkUserPermission(user, item.to, item.permissions?.actionName) || + checkUserPermission(user, item.to, item.permissions?.actionName2) + ? { ...item } + : null; + }) + .filter(Boolean); + }; + + \ No newline at end of file diff --git a/src/features/auth/ProtectedComp.js b/src/features/auth/ProtectedComp.js new file mode 100644 index 000000000..1d1dcbc79 --- /dev/null +++ b/src/features/auth/ProtectedComp.js @@ -0,0 +1,24 @@ +import { useSelector } from 'react-redux'; + +const ProtectedComponent = ({ actionName, children }) => { + const currentUser = useSelector((state) => state.auth.user); + + if (!actionName) { + return null; // Do not render anything if no action is specified + } + + const hasPermission = checkUserPermission(currentUser, actionName); + + return hasPermission ? children : null; +}; + +// Helper function to check user permissions +const checkUserPermission = (user, actionName) => { + if (!user?.role?.actions?.length) { + return false; + } + + return user.role.actions.some((action) => action.name === actionName); +}; + +export default ProtectedComponent; diff --git a/src/features/auth/ProtectedRoute.js b/src/features/auth/ProtectedRoute.js new file mode 100644 index 000000000..09307c8ee --- /dev/null +++ b/src/features/auth/ProtectedRoute.js @@ -0,0 +1,33 @@ +// src/utils/auth/ProtectedRoute.js +import React from 'react' +import { Navigate, useLocation } from 'react-router-dom' +import { useSelector } from 'react-redux' +import { CSpinner } from '@coreui/react' + +const ProtectedRoute = ({ children }) => { + const location = useLocation() + const isAuthenticated = useSelector((state) => state.auth.isAuthenticated) + const token = localStorage.getItem('token') // Backup check for token + + // Add a loading state for when authentication is being checked + const authLoading = useSelector((state) => state.auth?.loading) + + // Show loading spinner while checking authentication + if (authLoading) { + return ( +
    + +
    + ) + } + + // Check both Redux state and localStorage token + if (!isAuthenticated && !token) { + // Redirect to login but save the attempted location + return + } + + return children +} + +export default ProtectedRoute \ No newline at end of file diff --git a/src/features/auth/authSlice.js b/src/features/auth/authSlice.js new file mode 100644 index 000000000..19e93dad4 --- /dev/null +++ b/src/features/auth/authSlice.js @@ -0,0 +1,123 @@ +import { createSlice } from '@reduxjs/toolkit'; +import axiosInstance from '../../utils/api/axiosConfig'; + +const loadInitialState = () => { + const token = localStorage.getItem('token'); + const user = localStorage.getItem('user'); + return { + isAuthenticated: !!token, + user: user ? JSON.parse(user) : null, + token: token || null, + error: null, + loading: false, + }; +}; + +const authSlice = createSlice({ + name: 'auth', + initialState: loadInitialState(), + reducers: { + loginStart(state) { + state.loading = true; + state.error = null; + }, + loginSuccess(state, action) { + state.isAuthenticated = true; + state.user = action.payload.user; + state.token = action.payload.token; + state.loading = false; + state.error = null; + }, + loginFailure(state, action) { + state.loading = false; + state.error = action.payload; + }, + logout(state) { + state.isAuthenticated = false; + state.user = null; + state.token = null; + }, + resetError(state) { + state.error = null; + }, + updateUserSuccess(state, action) { + state.user = { ...state.user, ...action.payload }; + // Update user in localStorage as well + localStorage.setItem('user', JSON.stringify(state.user)); + }, + }, +}); + +export const { + loginStart, + loginSuccess, + loginFailure, + logout, + resetError, + updateUserSuccess +} = authSlice.actions; + +// Login thunk +export const login = (credentials) => async (dispatch) => { + dispatch(loginStart()); + try { + const loginResponse = await axiosInstance.post('/auth/login', { + login: credentials.user, + password: credentials.password, + }); + const { token } = loginResponse.data; + const userResponse = await axiosInstance.get('/auth/me', { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const userData = userResponse.data; + localStorage.setItem('token', token); + localStorage.setItem('user', JSON.stringify(userData)); + dispatch(loginSuccess({ token, user: userData })); + return true; + } catch (error) { + const errorMsg = error.response?.data?.message || 'Login failed'; + dispatch(loginFailure(errorMsg)); + return false; + } +}; + +// Update user thunk +export const updateUser = (userData) => async (dispatch) => { + try { + const response = await axiosInstance.put('/auth/me', userData); + dispatch(updateUserSuccess(userData)); + return true; + } catch (error) { + console.error('Error updating user data:', error); + return false; + } +}; + +// Check auth state thunk +export const checkAuthState = () => (dispatch) => { + const token = localStorage.getItem('token'); + const user = localStorage.getItem('user'); + if (token && user) { + try { + const userData = JSON.parse(user); + dispatch(loginSuccess({ token, user: userData })); + } catch (error) { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + dispatch(logout()); + } + } else { + dispatch(logout()); + } +}; + +// Logout thunk +export const handleLogout = () => (dispatch) => { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + dispatch(logout()); +}; + +export default authSlice.reducer; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 11d6e8658..48886035a 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ import { Provider } from 'react-redux' import 'core-js' import App from './App' -import store from './store' +import store from './app/store' createRoot(document.getElementById('root')).render( diff --git a/src/layout/DefaultLayout.js b/src/layout/DefaultLayout.js index 19fbf225f..c22967d09 100644 --- a/src/layout/DefaultLayout.js +++ b/src/layout/DefaultLayout.js @@ -7,10 +7,10 @@ const DefaultLayout = () => {
    -
    +
    - +
    ) diff --git a/src/routes.js b/src/routes.js index d2e9d6479..23b2b296d 100644 --- a/src/routes.js +++ b/src/routes.js @@ -1,102 +1,116 @@ import React from 'react' const Dashboard = React.lazy(() => import('./views/dashboard/Dashboard')) -const Colors = React.lazy(() => import('./views/theme/colors/Colors')) -const Typography = React.lazy(() => import('./views/theme/typography/Typography')) - -// Base -const Accordion = React.lazy(() => import('./views/base/accordion/Accordion')) -const Breadcrumbs = React.lazy(() => import('./views/base/breadcrumbs/Breadcrumbs')) -const Cards = React.lazy(() => import('./views/base/cards/Cards')) -const Carousels = React.lazy(() => import('./views/base/carousels/Carousels')) -const Collapses = React.lazy(() => import('./views/base/collapses/Collapses')) -const ListGroups = React.lazy(() => import('./views/base/list-groups/ListGroups')) -const Navs = React.lazy(() => import('./views/base/navs/Navs')) -const Paginations = React.lazy(() => import('./views/base/paginations/Paginations')) -const Placeholders = React.lazy(() => import('./views/base/placeholders/Placeholders')) -const Popovers = React.lazy(() => import('./views/base/popovers/Popovers')) -const Progress = React.lazy(() => import('./views/base/progress/Progress')) -const Spinners = React.lazy(() => import('./views/base/spinners/Spinners')) -const Tabs = React.lazy(() => import('./views/base/tabs/Tabs')) -const Tables = React.lazy(() => import('./views/base/tables/Tables')) -const Tooltips = React.lazy(() => import('./views/base/tooltips/Tooltips')) - -// Buttons -const Buttons = React.lazy(() => import('./views/buttons/buttons/Buttons')) -const ButtonGroups = React.lazy(() => import('./views/buttons/button-groups/ButtonGroups')) -const Dropdowns = React.lazy(() => import('./views/buttons/dropdowns/Dropdowns')) - -//Forms -const ChecksRadios = React.lazy(() => import('./views/forms/checks-radios/ChecksRadios')) -const FloatingLabels = React.lazy(() => import('./views/forms/floating-labels/FloatingLabels')) -const FormControl = React.lazy(() => import('./views/forms/form-control/FormControl')) -const InputGroup = React.lazy(() => import('./views/forms/input-group/InputGroup')) -const Layout = React.lazy(() => import('./views/forms/layout/Layout')) -const Range = React.lazy(() => import('./views/forms/range/Range')) -const Select = React.lazy(() => import('./views/forms/select/Select')) -const Validation = React.lazy(() => import('./views/forms/validation/Validation')) - -const Charts = React.lazy(() => import('./views/charts/Charts')) - -// Icons -const CoreUIIcons = React.lazy(() => import('./views/icons/coreui-icons/CoreUIIcons')) -const Flags = React.lazy(() => import('./views/icons/flags/Flags')) -const Brands = React.lazy(() => import('./views/icons/brands/Brands')) - -// Notifications -const Alerts = React.lazy(() => import('./views/notifications/alerts/Alerts')) -const Badges = React.lazy(() => import('./views/notifications/badges/Badges')) -const Modals = React.lazy(() => import('./views/notifications/modals/Modals')) -const Toasts = React.lazy(() => import('./views/notifications/toasts/Toasts')) - -const Widgets = React.lazy(() => import('./views/widgets/Widgets')) +const Map = React.lazy(() => import('./views/map/Map')) +const Users = React.lazy(() => import('./views/users/Users')) +const Roles = React.lazy(() => import('./views/roles/Roles')) +const Departments = React.lazy(() => import('./views/departments/Departments')) +const LayerForm = React.lazy(() => import('./views/layers/layers')) +const AddLayer = React.lazy(() => import('./views/layers/add')) +const EditLayer = React.lazy(() => import('./views/layers/edit')) +const Profile = React.lazy(()=>import('./views/profile/Profile')) const routes = [ - { path: '/', exact: true, name: 'Home' }, - { path: '/dashboard', name: 'Dashboard', element: Dashboard }, - { path: '/theme', name: 'Theme', element: Colors, exact: true }, - { path: '/theme/colors', name: 'Colors', element: Colors }, - { path: '/theme/typography', name: 'Typography', element: Typography }, - { path: '/base', name: 'Base', element: Cards, exact: true }, - { path: '/base/accordion', name: 'Accordion', element: Accordion }, - { path: '/base/breadcrumbs', name: 'Breadcrumbs', element: Breadcrumbs }, - { path: '/base/cards', name: 'Cards', element: Cards }, - { path: '/base/carousels', name: 'Carousel', element: Carousels }, - { path: '/base/collapses', name: 'Collapse', element: Collapses }, - { path: '/base/list-groups', name: 'List Groups', element: ListGroups }, - { path: '/base/navs', name: 'Navs', element: Navs }, - { path: '/base/paginations', name: 'Paginations', element: Paginations }, - { path: '/base/placeholders', name: 'Placeholders', element: Placeholders }, - { path: '/base/popovers', name: 'Popovers', element: Popovers }, - { path: '/base/progress', name: 'Progress', element: Progress }, - { path: '/base/spinners', name: 'Spinners', element: Spinners }, - { path: '/base/tabs', name: 'Tabs', element: Tabs }, - { path: '/base/tables', name: 'Tables', element: Tables }, - { path: '/base/tooltips', name: 'Tooltips', element: Tooltips }, - { path: '/buttons', name: 'Buttons', element: Buttons, exact: true }, - { path: '/buttons/buttons', name: 'Buttons', element: Buttons }, - { path: '/buttons/dropdowns', name: 'Dropdowns', element: Dropdowns }, - { path: '/buttons/button-groups', name: 'Button Groups', element: ButtonGroups }, - { path: '/charts', name: 'Charts', element: Charts }, - { path: '/forms', name: 'Forms', element: FormControl, exact: true }, - { path: '/forms/form-control', name: 'Form Control', element: FormControl }, - { path: '/forms/select', name: 'Select', element: Select }, - { path: '/forms/checks-radios', name: 'Checks & Radios', element: ChecksRadios }, - { path: '/forms/range', name: 'Range', element: Range }, - { path: '/forms/input-group', name: 'Input Group', element: InputGroup }, - { path: '/forms/floating-labels', name: 'Floating Labels', element: FloatingLabels }, - { path: '/forms/layout', name: 'Layout', element: Layout }, - { path: '/forms/validation', name: 'Validation', element: Validation }, - { path: '/icons', exact: true, name: 'Icons', element: CoreUIIcons }, - { path: '/icons/coreui-icons', name: 'CoreUI Icons', element: CoreUIIcons }, - { path: '/icons/flags', name: 'Flags', element: Flags }, - { path: '/icons/brands', name: 'Brands', element: Brands }, - { path: '/notifications', name: 'Notifications', element: Alerts, exact: true }, - { path: '/notifications/alerts', name: 'Alerts', element: Alerts }, - { path: '/notifications/badges', name: 'Badges', element: Badges }, - { path: '/notifications/modals', name: 'Modals', element: Modals }, - { path: '/notifications/toasts', name: 'Toasts', element: Toasts }, - { path: '/widgets', name: 'Widgets', element: Widgets }, + { + path: '/', + exact: true, + name: 'Home', + permissions: { method: 'GET' }, + }, + { path: '/profile', name: 'Profile', element: Profile, permissions: { method: 'GET' } }, + { + path: '/map', + name: 'Map', + element: Map, + permissions: { method: 'GET', actionName: "Xaritani ko'rish", actionName2: 'Map Management' }, + }, + + { + path: '/layers', + name: 'Layers', + element: LayerForm, + permissions: { method: 'GET', actionName: 'Map Management' }, + }, + { + path: '/layers/add', + name: 'Add Layer', + element: AddLayer, + permissions: { method: 'GET', actionName: 'Map Management' }, + }, + { + path: '/layers/edit/:id', + name: 'Edit Layer', + element: EditLayer, + permissions: { method: 'GET', actionName: 'Map Management' }, + }, + { + path: '/dashboard', + name: 'Dashboard', + element: Dashboard, + permissions: { method: 'GET' }, + }, + { + path: '/users', + name: 'Users', + element: Users, + permissions: { method: 'GET', actionName: 'Seeing users', actionName2: 'Users Management' }, + }, + { + path: '/roles', + name: 'Roles', + element: Roles, + permissions: { method: 'GET', actionName: 'Seeing roles', actionName2: 'Role Management' }, + }, + { + path: '/departments', + name: 'Departments', + element: Departments, + permissions: { + method: 'GET', + actionName: 'Seeing Departments', + actionName2: 'Departments Management', + }, + }, + { + path: '/contact', + name: 'Contact', + element: React.lazy(() => import('./views/contact/Contact')), + permissions: { method: 'GET' }, + }, + { + path: '/about', + name: 'About', + element: React.lazy(() => import('./views/about/About')), + permissions: { method: 'GET' }, + }, + // { + // path: '/instructions', + // name: 'Instructions', + // element: InstructionsPage, + // permissions: { + // method: 'GET', + // actionName: 'Seeing Instructions', + // actionName2: 'CMS Management', + // }, + // }, + // { + // path: '/instructor', + // name: 'Instructor', + // element: InstructionsAdmin, + // permissions: { method: 'GET', actionName: 'CMS Management' }, + // }, + // { + // path: '/instructor/new', + // name: 'Add Instruction', + // element: InstructionForm, + // permissions: { method: 'GET', actionName: 'CMS Management' }, + // }, + // { + // path: '/instructor/edit/:id', + // name: 'Edit Instruction', + // element: InstructionForm, + // permissions: { method: 'GET', actionName: 'CMS Management' }, + // }, ] export default routes diff --git a/src/scss/_custom.scss b/src/scss/_custom.scss new file mode 100644 index 000000000..15d367af4 --- /dev/null +++ b/src/scss/_custom.scss @@ -0,0 +1 @@ +// Here you can add other styles diff --git a/src/scss/_theme.scss b/src/scss/_theme.scss new file mode 100644 index 000000000..49e1c79e6 --- /dev/null +++ b/src/scss/_theme.scss @@ -0,0 +1,64 @@ +body { + background-color: var(--cui-tertiary-bg); +} + +.wrapper { + width: 100%; + @include ltr-rtl("padding-left", var(--cui-sidebar-occupy-start, 0)); + @include ltr-rtl("padding-right", var(--cui-sidebar-occupy-end, 0)); + will-change: auto; + @include transition(padding .15s); +} + +.header > .container-fluid, +.sidebar-header { + min-height: calc(4rem + 1px); // stylelint-disable-line function-disallowed-list +} + +.sidebar-brand-full { + margin-left: 3px; +} + +.sidebar-header { + .nav-underline-border { + --cui-nav-underline-border-link-padding-x: 1rem; + --cui-nav-underline-border-gap: 0; + } + + .nav-link { + display: flex; + align-items: center; + min-height: calc(4rem + 1px); // stylelint-disable-line function-disallowed-list + } +} + +.sidebar-toggler { + @include ltr-rtl("margin-left", auto); +} + +.sidebar-narrow, +.sidebar-narrow-unfoldable:not(:hover) { + .sidebar-toggler { + @include ltr-rtl("margin-right", auto); + } +} + +.header > .container-fluid + .container-fluid { + min-height: 3rem; +} + +.footer { + min-height: calc(3rem + 1px); // stylelint-disable-line function-disallowed-list +} + +@if $enable-dark-mode { + @include color-mode(dark) { + body { + background-color: var(--cui-dark-bg-subtle); + } + + .footer { + --cui-footer-bg: var(--cui-body-bg); + } + } +} diff --git a/src/scss/_variables.scss b/src/scss/_variables.scss new file mode 100644 index 000000000..304f03b9e --- /dev/null +++ b/src/scss/_variables.scss @@ -0,0 +1,16 @@ +// Variable overrides +// +// If you want to customize your project please add your variables below. + +$enable-deprecation-messages: false !default; +$container-max-widths: ( + sm: 540px, + md: 720px, + lg: 960px, + xl: 1140px, + xxl: 1920px +); +$container-padding-x: 0; + + + diff --git a/src/scss/examples.scss b/src/scss/examples.scss index 83e43f34e..c77925f67 100644 --- a/src/scss/examples.scss +++ b/src/scss/examples.scss @@ -1,9 +1,10 @@ /* stylelint-disable scss/selector-no-redundant-nesting-selector */ -@use "@coreui/coreui/scss/variables" as * with ( - $enable-deprecation-messages: false -); -@use "@coreui/coreui/scss/mixins/breakpoints" as *; -@use "@coreui/coreui/scss/mixins/color-mode" as *; + +$enable-deprecation-messages: false !default; + +@import "@coreui/coreui/scss/functions"; +@import "@coreui/coreui/scss/variables"; +@import "@coreui/coreui/scss/mixins"; .example { &:not(:first-child) { @@ -107,10 +108,4 @@ } } } -} - -@include color-mode(dark) { - .example .tab-content { - background-color: var(--#{$prefix}secondary-bg); - } -} +} \ No newline at end of file diff --git a/src/scss/style.scss b/src/scss/style.scss index 6b8f2b2e5..4fbc82356 100644 --- a/src/scss/style.scss +++ b/src/scss/style.scss @@ -1,67 +1,15 @@ -@use "@coreui/coreui/scss/coreui" as * with ( - $enable-deprecation-messages: false, -); -@use "@coreui/chartjs/scss/coreui-chartjs"; -@use "vendors/simplebar"; +// If you want to override variables do it here +@import "variables"; -body { - background-color: var(--cui-tertiary-bg); -} +// Import styles +@import "@coreui/coreui/scss/coreui"; +@import "@coreui/chartjs/scss/coreui-chartjs"; -.wrapper { - width: 100%; - padding-inline: var(--cui-sidebar-occupy-start, 0) var(--cui-sidebar-occupy-end, 0); - will-change: auto; - @include transition(padding .15s); -} +// Vendors +@import "vendors/simplebar"; -.header > .container-fluid, -.sidebar-header { - min-height: calc(4rem + 1px); // stylelint-disable-line function-disallowed-list -} +// Custom styles for this theme +@import "theme"; -.sidebar-brand-full { - margin-left: 3px; -} - -.sidebar-header { - .nav-underline-border { - --cui-nav-underline-border-link-padding-x: 1rem; - --cui-nav-underline-border-gap: 0; - } - - .nav-link { - display: flex; - align-items: center; - min-height: calc(4rem + 1px); // stylelint-disable-line function-disallowed-list - } -} - -.sidebar-toggler { - margin-inline-start: auto; -} - -.sidebar-narrow, -.sidebar-narrow-unfoldable:not(:hover) { - .sidebar-toggler { - margin-inline-end: auto; - } -} - -.header > .container-fluid + .container-fluid { - min-height: 3rem; -} - -.footer { - min-height: calc(3rem + 1px); // stylelint-disable-line function-disallowed-list -} - -@include color-mode(dark) { - body { - background-color: var(--cui-dark-bg-subtle); - } - - .footer { - --cui-footer-bg: var(--cui-body-bg); - } -} +// If you want to add custom CSS you can put it here +@import "custom"; diff --git a/src/store.js b/src/store.js deleted file mode 100644 index 8ad30dad6..000000000 --- a/src/store.js +++ /dev/null @@ -1,18 +0,0 @@ -import { legacy_createStore as createStore } from 'redux' - -const initialState = { - sidebarShow: true, - theme: 'light', -} - -const changeState = (state = initialState, { type, ...rest }) => { - switch (type) { - case 'set': - return { ...state, ...rest } - default: - return state - } -} - -const store = createStore(changeState) -export default store diff --git a/src/utils/api/axiosConfig.js b/src/utils/api/axiosConfig.js new file mode 100644 index 000000000..a61285b8e --- /dev/null +++ b/src/utils/api/axiosConfig.js @@ -0,0 +1,38 @@ +// src/utils/api/axiosConfig.js +import axios from 'axios'; + +const axiosInstance = axios.create({ + baseURL: 'http://109.199.124.208:2000', +}); + +// Setup request interceptor +axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers['Authorization'] = `Bearer ${token}`; + } + return config; + }, + (error) => Promise.reject(error) +); + +export const setupAxiosInterceptors = (logoutCallback) => { + axiosInstance.interceptors.response.use( + (response) => response, + (error) => { + if (error.response && error.response.status === 401) { + localStorage.removeItem('token'); + localStorage.removeItem('user'); + if (logoutCallback) { + logoutCallback(); + } + } + return Promise.reject(error); + } + ); +}; + +export default axiosInstance; + + diff --git a/src/utils/navigation/navigation.js b/src/utils/navigation/navigation.js new file mode 100644 index 000000000..91ff44458 --- /dev/null +++ b/src/utils/navigation/navigation.js @@ -0,0 +1,10 @@ +// utils/navigation.js +import { createBrowserHistory } from 'history'; + +const history = createBrowserHistory(); + +export const navigate = (path, options) => { + history.push(path, options); +}; + +export default history; \ No newline at end of file diff --git a/src/views/about/About.js b/src/views/about/About.js new file mode 100644 index 000000000..5bcba63af --- /dev/null +++ b/src/views/about/About.js @@ -0,0 +1,51 @@ +import React from 'react' + +export default function About() { + return ( +
    +
    +

    About Chorvoq GIS

    +

    + Chorvoq GIS is a modern land management system built to visualize, organize, and track territorial information + through interactive maps. Whether you're a government agency allocating land or a business tracking territory usage, + Chorvoq GIS makes it simple, scalable, and visual. +

    +

    + The application allows users to draw shapes on maps, save layers, assign metadata, and more—all displayed clearly + within a user-friendly interface backed by powerful technologies like React, Spring Boot, PostgreSQL, and GeoServer. +

    +

    + Designed with simplicity and efficiency in mind, Chorvoq GIS helps bring transparency and control to land-based operations. +

    +
    +
    + ) +} + +const styles = { + container: { + display: 'flex', + justifyContent: 'center', + padding: '3rem', + backgroundColor: '#f4f6f8', + minHeight: '100vh', + }, + card: { + backgroundColor: '#fff', + padding: '2rem', + borderRadius: '16px', + boxShadow: '0 10px 30px rgba(0, 0, 0, 0.1)', + maxWidth: '800px', + lineHeight: 1.6, + }, + heading: { + fontSize: '2rem', + marginBottom: '1.5rem', + color: '#2c3e50', + }, + text: { + fontSize: '1rem', + color: '#34495e', + marginBottom: '1rem', + }, +} diff --git a/src/views/contact/Contact.js b/src/views/contact/Contact.js new file mode 100644 index 000000000..63ec7a958 --- /dev/null +++ b/src/views/contact/Contact.js @@ -0,0 +1,66 @@ +import React from 'react' + +export default function Contact() { + return ( +
    +
    +

    Contact Us

    +

    We’d love to hear from you! Reach out for support, feedback, or partnership opportunities.

    + +
    +

    Email

    +

    support@chorvoqgis.com

    +
    + +
    +

    Phone

    +

    +998 90 123 45 67

    +
    + +
    +

    Address

    +

    Tashkent, Uzbekistan

    +
    +
    +
    + ) +} + +const styles = { + container: { + display: 'flex', + justifyContent: 'center', + padding: '3rem', + backgroundColor: '#f4f6f8', + minHeight: '100vh', + }, + card: { + backgroundColor: '#fff', + padding: '2rem', + borderRadius: '16px', + boxShadow: '0 10px 30px rgba(0, 0, 0, 0.1)', + maxWidth: '600px', + }, + heading: { + fontSize: '2rem', + marginBottom: '1.5rem', + color: '#2c3e50', + }, + text: { + fontSize: '1rem', + color: '#34495e', + marginBottom: '1.5rem', + }, + infoBox: { + marginBottom: '1rem', + }, + label: { + fontSize: '1.1rem', + fontWeight: 'bold', + color: '#2c3e50', + }, + value: { + fontSize: '1rem', + color: '#7f8c8d', + }, +} diff --git a/src/views/dashboard/Dashboard.js b/src/views/dashboard/Dashboard.js index 57a55290d..c94e06ea8 100644 --- a/src/views/dashboard/Dashboard.js +++ b/src/views/dashboard/Dashboard.js @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useEffect } from 'react' import classNames from 'classnames' import { @@ -10,6 +10,7 @@ import { CCardFooter, CCardHeader, CCol, + CContainer, CProgress, CRow, CTable, @@ -53,8 +54,19 @@ import avatar6 from 'src/assets/images/avatars/6.jpg' import WidgetsBrand from '../widgets/WidgetsBrand' import WidgetsDropdown from '../widgets/WidgetsDropdown' import MainChart from './MainChart' - -const Dashboard = () => { +import axiosInstance from '../../utils/api/axiosConfig' +const Dashboard =() => { + + useEffect( ()=>{ + fetchUserInfo() +},[]) + const fetchUserInfo = async () => { + try { + const response = await axiosInstance.get('/auth/me') + } catch (error) { + console.error(error) + } + } const progressExample = [ { title: 'Visits', value: '29.703 Users', percent: 40, color: 'success' }, { title: 'Unique', value: '24.093 Users', percent: 20, color: 'info' }, @@ -177,9 +189,10 @@ const Dashboard = () => { ] return ( - <> - - + + + + @@ -233,8 +246,8 @@ const Dashboard = () => { - - + + Traffic {' & '} Sales @@ -380,7 +393,7 @@ const Dashboard = () => { - + ) } diff --git a/src/views/departments/Departments.js b/src/views/departments/Departments.js new file mode 100644 index 000000000..710612b53 --- /dev/null +++ b/src/views/departments/Departments.js @@ -0,0 +1,302 @@ +import React, { useEffect, useState } from 'react' +import axiosInstance from '../../utils/api/axiosConfig' +import { + CTable, + CTableHead, + CTableBody, + CTableRow, + CTableHeaderCell, + CTableDataCell, + CFormInput, + CFormSelect, + CButton, + CContainer, + CPagination, + CPaginationItem, + CModal, + CModalHeader, + CModalTitle, + CModalBody, + CModalFooter, + CForm, + CAlert, +} from '@coreui/react' +import { cilPencil, cilTrash } from '@coreui/icons' +import CIcon from '@coreui/icons-react' +import ProtectedComponent from '../../features/auth/ProtectedComp' +const DepartmentsTable = () => { + const [departments, setDepartments] = useState([]) + const [search, setSearch] = useState('') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [totalPages, setTotalPages] = useState(1) + const [deleteModalVisible, setDeleteModalVisible] = useState(false) + const [editModalVisible, setEditModalVisible] = useState(false) + const [addModalVisible, setAddModalVisible] = useState(false) + const [selectedDepartment, setSelectedDepartment] = useState(null) + const [errorMessage, setErrorMessage] = useState('') + const [successMessage, setSuccessMessage] = useState('') + + useEffect(() => { + fetchDepartments() + }, [page, pageSize, search]) + + const fetchDepartments = async () => { + try { + const response = await axiosInstance.get('/api/departments', { + params: { + page: page - 1, + pageSize, + name: search || undefined, + }, + }) + if (response.data) { + setDepartments(response.data.data) + setTotalPages(Math.ceil(response.data.count / pageSize)) + } + } catch (error) { + console.error('Error fetching departments:', error) + } + } + + const showDeleteConfirmation = (department) => { + setSelectedDepartment(department) + setDeleteModalVisible(true) + } + + const handleDelete = async () => { + try { + await axiosInstance.delete(`/api/departments/${selectedDepartment.id}`) + setDeleteModalVisible(false) + setSuccessMessage('Department deleted successfully') + fetchDepartments() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error deleting department') + console.error('Error deleting department:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const showEditModal = (department) => { + setSelectedDepartment(department) + setEditModalVisible(true) + } + + const showAddModal = () => { + setSelectedDepartment({ name: '' }) + setAddModalVisible(true) + } + + const handleEditSubmit = async (e) => { + e.preventDefault() + try { + await axiosInstance.put(`/api/departments/${selectedDepartment.id}`, { + name: selectedDepartment.name, + }) + setEditModalVisible(false) + setSuccessMessage('Department updated successfully') + fetchDepartments() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error updating department') + console.error('Error updating department:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const handleAddSubmit = async (e) => { + e.preventDefault() + try { + await axiosInstance.post('/api/departments', { + name: selectedDepartment.name, + }) + setAddModalVisible(false) + setSuccessMessage('Department added successfully') + fetchDepartments() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error adding department') + console.error('Error adding department:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const handleInputChange = (e) => { + const { name, value } = e.target + setSelectedDepartment((prev) => ({ + ...prev, + [name]: value, + })) + } + + return ( + + {errorMessage && {errorMessage}} + {successMessage && {successMessage}} + +
    +
    + setSearch(e.target.value)} + className="flex-grow-1" + /> + + + + Add + + +
    +
    + +
    + + + + + Name + + Operations + + + + + {departments.map((department, index) => ( + + {(page - 1) * pageSize + index + 1} + {department.name} + + + showEditModal(department)}> + + + showDeleteConfirmation(department)} + > + + + + + + ))} + + +
    + +
    +
    + setPageSize(Number(e.target.value))} + className="w-auto" + > + {[5, 10, 20, 50].map((size) => ( + + ))} + + + setPage(page - 1)}> + Prev + + {[...Array(totalPages)].map((_, i) => ( + setPage(i + 1)}> + {i + 1} + + ))} + setPage(page + 1)}> + Next + + +
    +
    + + {/* Add Department Modal */} + setAddModalVisible(false)}> + setAddModalVisible(false)}> + Add New Department + + + +
    + +
    +
    + + Create Department + +
    +
    +
    +
    + + {/* Edit Department Modal */} + setEditModalVisible(false)}> + setEditModalVisible(false)}> + Edit Department + + + +
    + +
    +
    + + Save Changes + +
    +
    +
    +
    + + {/* Delete Confirmation Modal */} + setDeleteModalVisible(false)}> + setDeleteModalVisible(false)}> + Confirm Delete + + Are you sure you want to delete {selectedDepartment?.name}? + + setDeleteModalVisible(false)}> + Cancel + + + Delete + + + +
    + ) +} + +export default DepartmentsTable \ No newline at end of file diff --git a/src/views/icons/brands/Brands.js b/src/views/icons/brands/Brands.js deleted file mode 100644 index e46ce6fed..000000000 --- a/src/views/icons/brands/Brands.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { CCard, CCardBody, CCardHeader, CCol, CRow } from '@coreui/react' -import CIcon from '@coreui/icons-react' -import { brandSet } from '@coreui/icons' -import { DocsIcons } from 'src/components' - -const toKebabCase = (str) => { - return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase() -} - -export const getIconsView = (iconset) => { - return Object.entries(iconset).map(([name, value]) => ( - - -
    {toKebabCase(name)}
    -
    - )) -} - -const CoreUIIcons = () => { - return ( - <> - - - Brand Icons - - {getIconsView(brandSet)} - - - - ) -} - -export default CoreUIIcons diff --git a/src/views/icons/coreui-icons/CoreUIIcons.js b/src/views/icons/coreui-icons/CoreUIIcons.js deleted file mode 100644 index 08fe176f7..000000000 --- a/src/views/icons/coreui-icons/CoreUIIcons.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import { CCard, CCardBody, CCardHeader, CRow } from '@coreui/react' -import { freeSet } from '@coreui/icons' -import { getIconsView } from '../brands/Brands.js' -import { DocsIcons } from 'src/components' - -const CoreUIIcons = () => { - return ( - <> - - - Free Icons - - {getIconsView(freeSet)} - - - - ) -} - -export default CoreUIIcons diff --git a/src/views/icons/flags/Flags.js b/src/views/icons/flags/Flags.js deleted file mode 100644 index 5db7e5670..000000000 --- a/src/views/icons/flags/Flags.js +++ /dev/null @@ -1,21 +0,0 @@ -import React from 'react' -import { CCard, CCardBody, CCardHeader, CRow } from '@coreui/react' -import { getIconsView } from '../brands/Brands.js' -import { flagSet } from '@coreui/icons' -import { DocsIcons } from 'src/components' - -const CoreUIIcons = () => { - return ( - <> - - - Flag Icons - - {getIconsView(flagSet)} - - - - ) -} - -export default CoreUIIcons diff --git a/src/views/icons/index.js b/src/views/icons/index.js deleted file mode 100644 index 92db64e57..000000000 --- a/src/views/icons/index.js +++ /dev/null @@ -1,5 +0,0 @@ -import CoreUIIcons from './coreui-icons' -import Flags from './flags' -import Brands from './brands' - -export { CoreUIIcons, Flags, Brands } diff --git a/src/views/layers/add.js b/src/views/layers/add.js new file mode 100644 index 000000000..66d44f5e5 --- /dev/null +++ b/src/views/layers/add.js @@ -0,0 +1,171 @@ +import React, { useState } from 'react'; +import { + CCard, + CCardBody, + CCardHeader, + CCardFooter, + CCol, + CRow, + CButton, + CForm, + CFormInput, + CFormLabel, + CSpinner, + CAlert, + CContainer, +} from '@coreui/react'; +import { cilArrowLeft, cilCloudUpload } from '@coreui/icons'; +import CIcon from '@coreui/icons-react'; +import axiosInstance from '../../utils/api/axiosConfig'; +import { useNavigate } from 'react-router-dom'; + +const AddLayer = () => { + const [uploadFile, setUploadFile] = useState(null); + const [layerName, setLayerName] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(null); + const token = localStorage.getItem('token'); + const navigate = useNavigate(); + + // Handle file upload + const handleFileChange = (event) => { + setUploadFile(event.target.files[0]); + }; + + // Display and clear alerts + const showErrorAlert = (message) => { + setError(message); + setSuccess(null); + }; + + const showSuccessAlert = (message) => { + setSuccess(message); + setError(null); + }; + + // Upload and create new layer + const uploadLayer = async () => { + if (!uploadFile || !layerName) { + showErrorAlert('Please provide both a layer name and a zip file'); + return; + } + + setLoading(true); + try { + const formData = new FormData(); + formData.append('file', uploadFile); + + const response = await axiosInstance.post( + `/layers/new/${layerName}`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data',}, + expose: ['Location'] + } + + ); + + showSuccessAlert(`Layer uploaded successfully.`); + + navigate('/layers'); + // Navigate to edit page with the newly created layer + // setTimeout(() => { + // navigate(`/layers/edit/${response.data.id}`, { state: { isNew: true } }); + // }, 1500); + + } catch (err) { + showErrorAlert(`Upload failed: ${err.response?.data?.error || err.message}`); + setLoading(false); + } + }; + + return ( + + + + + Upload New Layer + navigate('/layers')} + > + + Back to Layers + + + + {error && {error}} + {success && {success}} + + + + +
    + Layer Name + setLayerName(e.target.value)} + placeholder="Enter layer name" + disabled={loading} + /> +
    + Enter a descriptive name for your layer +
    +
    +
    + + +
    + Shapefile (ZIP) + +
    + Upload a ZIP file containing shapefile (.shp, .dbf, .shx, .prj) +
    +
    +
    +
    +
    +
    + + navigate('/layers')} + disabled={loading} + > + Cancel + + + {loading ? ( + <> + + Uploading... + + ) : ( + <> + + Upload Layer + + )} + + +
    +
    +
    + ); +}; + +export default AddLayer; \ No newline at end of file diff --git a/src/views/layers/edit.js b/src/views/layers/edit.js new file mode 100644 index 000000000..31a1af5a4 --- /dev/null +++ b/src/views/layers/edit.js @@ -0,0 +1,457 @@ +import React, { useState, useEffect } from 'react' +import { + CCard, + CCardBody, + CCardHeader, + CCardFooter, + CCol, + CRow, + CButton, + CForm, + CFormInput, + CFormCheck, + CFormLabel, + CSpinner, + CAlert, + CNav, + CNavItem, + CNavLink, + CTabContent, + CTabPane, + CContainer, +} from '@coreui/react' +import { cilArrowLeft, cilSave, cilCheck } from '@coreui/icons' +import CIcon from '@coreui/icons-react' +import axiosInstance from '../../utils/api/axiosConfig' +import { useNavigate, useParams, useLocation } from 'react-router-dom' + +const EditLayer = () => { + const { id } = useParams() + const navigate = useNavigate() + const location = useLocation() + const isNewLayer = location.state?.isNew || false + + const [activeTab, setActiveTab] = useState('general') + const [layer, setLayer] = useState(null) + const [loading, setLoading] = useState(false) + const [saving, setSaving] = useState(false) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + const [layerData, setLayerData] = useState({ + isSearchable: true, + isBackground: false, + minZoom: 1, + maxZoom: 16, + color: '#3388ff', + title: '', + fields: [], + }) + + // Fetch layer data on component mount + useEffect(() => { + fetchLayerData() + }, [id]) + + // Display and clear alerts + const showErrorAlert = (message) => { + setError(message) + setSuccess(null) + } + + const showSuccessAlert = (message) => { + setSuccess(message) + setError(null) + } + + // Clear alerts after 5 seconds + useEffect(() => { + if (error || success) { + const timer = setTimeout(() => { + setError(null) + setSuccess(null) + }, 5000) + + return () => clearTimeout(timer) + } + }, [error, success]) + + // Fetch layer data + const fetchLayerData = async () => { + if (!id) return + + setLoading(true) + try { + const response = await axiosInstance.get(`/layers/${id}`) + setLayer(response.data) + + // Initialize layer data + setLayerData({ + isSearchable: response.data.isSearchable !== undefined ? response.data.isSearchable : true, + isBackground: response.data.isBackground !== undefined ? response.data.isBackground : false, + minZoom: response.data.minZoom || 1, + maxZoom: response.data.maxZoom || 16, + color: response.data.color || '#3388ff', + title: response.data.title || response.data.name || '', + fields: response.data.fields + ? response.data.fields.map((field) => ({ + ...field, + title: field.title || field.name || '', + isMandatory: field.isMandatory || false, + isSearchable: field.isSearchable || false, + isActive: field.isActive !== undefined ? field.isActive : true, + })) + : [], + }) + + setError(null) + + // Show success message for new layer + if (isNewLayer) { + showSuccessAlert('Layer successfully uploaded! Please finalize the configuration.') + } + } catch (err) { + showErrorAlert(`Failed to fetch layer details: ${err.response?.data?.message || err.message}`) + } finally { + setLoading(false) + } + } + + // Handle field change + const handleFieldChange = (index, field, value) => { + const updatedFields = [...layerData.fields] + updatedFields[index] = { ...updatedFields[index], [field]: value } + + setLayerData({ + ...layerData, + fields: updatedFields, + }) + } + + // Save or finalize layer + const saveLayer = async () => { + if (!layer) return + + setSaving(true) + try { + // Check if layer is in READY_FOR_CREATION status (needs finalization) + if (layer.status === 'READY_FOR_CREATION') { + await axiosInstance.post(`/layers/finalize/${layer.id}`, layerData) + showSuccessAlert('Layer finalized successfully') + navigate('/layers') + } else { + // Otherwise update the existing layer + await axiosInstance.put(`/layers/${layer.id}`, layerData) + showSuccessAlert('Layer updated successfully') + navigate('/layers') + } + + // Refresh layer data + fetchLayerData() + } catch (err) { + showErrorAlert(`Save failed: ${err.response?.data?.error || err.message}`) + } finally { + setSaving(false) + } + } + + if (loading && !layer) { + return ( + + +
    + +

    Loading layer data...

    +
    +
    +
    + ) + } + + return ( + + + + + + {layer?.status === 'READY_FOR_CREATION' ? 'Finalize Layer' : 'Edit Layer'}:{' '} + {layer?.name} + + navigate('/layers')}> + + Back to Layers + + + + + {error && {error}} + {success && {success}} + + + + setActiveTab('general')} + href="#" + > + General Settings + + + + setActiveTab('fields')} + href="#" + > + Fields Configuration + + + + + + + + + +
    + Layer Name + +
    + +
    + Layer Title + + setLayerData({ + ...layerData, + title: e.target.value, + }) + } + placeholder="Enter display title for users" + /> +
    + +
    + Status +
    + + {layer?.status} + +
    +
    +
    + + +
    + Layer ID + +
    +
    + Is Searchable + + setLayerData({ + ...layerData, + isSearchable: e.target.checked, + }) + } + /> +
    Enable searching features in this layer
    +
    + +
    + Is Background Layer + + setLayerData({ + ...layerData, + isBackground: e.target.checked, + }) + } + /> +
    + Background layers are always visible without needing to be selected +
    +
    + +
    + Layer Color + + setLayerData({ + ...layerData, + color: e.target.value, + }) + } + /> + +
    +
    +
    + + + +
    + Min Zoom + + setLayerData({ + ...layerData, + minZoom: parseInt(e.target.value, 10) || 1, + }) + } + /> + +
    +
    + + +
    + Max Zoom + + setLayerData({ + ...layerData, + maxZoom: parseInt(e.target.value, 10) || 16, + }) + } + /> + +
    +
    +
    +
    +
    + + + {layerData.fields.length > 0 ? ( + layerData.fields.map((field, index) => ( + + + +
    + Field Name + handleFieldChange(index, 'name', e.target.value)}/> +
    +
    + + +
    + Field Type + +
    +
    + +
    + Field Title + handleFieldChange(index, 'title', e.target.value)} /> + +
    +
    +
    + + + +
    + + handleFieldChange(index, 'isMandatory', e.target.checked) + } + /> +
    Field is required when adding features
    +
    +
    + + +
    + + handleFieldChange(index, 'isSearchable', e.target.checked) + } + /> +
    Include this field in search operations
    +
    +
    + + +
    + + handleFieldChange(index, 'isActive', e.target.checked) + } + /> +
    + Display and use this field in the application +
    +
    +
    +
    +
    + )) + ) : ( +
    + No fields available for this layer +
    + )} +
    +
    +
    + + + navigate('/layers')} + disabled={saving} + > + Cancel + + + {saving ? ( + <> + + Saving... + + ) : ( + <> + + {layer?.status === 'READY_FOR_CREATION' ? 'Finalize Layer' : 'Save Changes'} + + )} + + +
    +
    +
    + ) +} + +export default EditLayer diff --git a/src/views/layers/layers.js b/src/views/layers/layers.js new file mode 100644 index 000000000..c092e0fbf --- /dev/null +++ b/src/views/layers/layers.js @@ -0,0 +1,370 @@ +import React, { useState, useEffect } from 'react' +import { + CCard, + CCardBody, + CCardHeader, + CCol, + CRow, + CButton, + CInputGroup, + CFormInput, + CTable, + CTableHead, + CTableRow, + CTableHeaderCell, + CTableBody, + CTableDataCell, + CSpinner, + CAlert, + CModal, + CModalHeader, + CModalTitle, + CModalBody, + CModalFooter, + CContainer, + CFormSelect, + CTooltip, + CProgress +} from '@coreui/react' +import { cilCloudUpload, cilTrash, cilSearch, cilCheck, cilCloudDownload, cilWarning, cilReload } from '@coreui/icons' +import CIcon from '@coreui/icons-react' +import axiosInstance from '../../utils/api/axiosConfig' +import { useNavigate } from 'react-router-dom' + +const WmsLayerManagement = () => { + const [layers, setLayers] = useState([]) + const [loading, setLoading] = useState(false) + const [downloadingLayer, setDownloadingLayer] = useState(null) + const [error, setError] = useState(null) + const [success, setSuccess] = useState(null) + const [searchQuery, setSearchQuery] = useState('') + const [deleteModalVisible, setDeleteModalVisible] = useState(false) + const [downloadModalVisible, setDownloadModalVisible] = useState(false) + const [selectedLayer, setSelectedLayer] = useState(null) + const [downloadFormat, setDownloadFormat] = useState('json') + + const navigate = useNavigate() + + useEffect(() => { + fetchLayers() + }, []) + + const showErrorAlert = (message) => { + setError(message) + setSuccess(null) + } + + const showSuccessAlert = (message) => { + setSuccess(message) + setError(null) + } + + useEffect(() => { + if (error || success) { + const timer = setTimeout(() => { + setError(null) + setSuccess(null) + }, 5000) + return () => clearTimeout(timer) + } + }, [error, success]) + + const fetchLayers = async () => { + setLoading(true) + try { + const url = searchQuery ? `/layers?name=${encodeURIComponent(searchQuery)}` : '/layers' + const response = await axiosInstance.get(url) + setLayers(response.data) + } catch (err) { + showErrorAlert(`Failed to fetch layers: ${err.response?.data?.message || err.message}`) + } finally { + setLoading(false) + } + } + + const deleteLayer = async () => { + if (!selectedLayer) return + setLoading(true) + try { + await axiosInstance.delete(`/layers/${selectedLayer.id}`) + showSuccessAlert(`Layer "${selectedLayer.name}" has been deleted successfully`) + fetchLayers() + } catch (err) { + showErrorAlert(`Deletion failed: ${err.response?.data?.message || err.message}`) + } finally { + setDeleteModalVisible(false) + setLoading(false) + } + } + + const showDeleteConfirmation = (e, layer) => { + e.stopPropagation() // Prevent row click from triggering + setSelectedLayer(layer) + setDeleteModalVisible(true) + } + + const showDownloadModal = (e, layer) => { + e.stopPropagation() // Prevent row click from triggering + setSelectedLayer(layer) + setDownloadFormat('json') // Reset to default format + setDownloadModalVisible(true) + } + + const downloadLayer = async () => { + if (!selectedLayer) return + + setDownloadingLayer(selectedLayer.id) + try { + // Make a request to download endpoint + const response = await axiosInstance.get(`/layers/download/${selectedLayer.name}/${downloadFormat}`, { + responseType: 'blob', // Important for file downloads + }) + + // Create a download link and trigger it + const url = window.URL.createObjectURL(new Blob([response.data])) + const link = document.createElement('a') + link.href = url + link.setAttribute('download', `${selectedLayer.name}.${downloadFormat}`) + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + + showSuccessAlert(`Layer "${selectedLayer.name}" has been downloaded successfully as ${downloadFormat.toUpperCase()}`) + } catch (err) { + showErrorAlert(`Download failed: ${err.response?.data?.message || err.message}`) + } finally { + setDownloadModalVisible(false) + setDownloadingLayer(null) + } + } + + const handleRowClick = (layer) => { + // Only navigate if not in loading state and not in READY_FOR_CREATION status + if (downloadingLayer !== layer.id) { + navigate(`/layers/edit/${layer.id}`) + } + } + + const renderLayerRow = (layer) => { + const isReadyForCreation = layer.status === 'READY_FOR_CREATION' + const rowClasses = ` + ${isReadyForCreation ? 'bg-light' : ''} + ${downloadingLayer === layer.id ? '' : 'cursor-pointer'} + ` + + return ( + handleRowClick(layer)} + style={{ cursor: downloadingLayer === layer.id ? 'default' : 'pointer' }} + > + {layer.id} + {layer.name} + {layer.title} + + {layer.status === 'CREATION_FAILED' ? ( + + Failure Reason:
    + {layer.failCause || "No specific failure reason provided"} +
  • + } + placement="top" + > + {layer.status} + + ) : ( + + {layer.status} + + )} + + + + {layer.isBackground ? ( + + ) : ( + + )} + + + {layer.isSearchable ? ( + + ) : ( + + )} + + e.stopPropagation()}> +
    + {layer.status !== 'CREATION_FAILED' ? ( + showDownloadModal(e, layer)} + disabled={downloadingLayer === layer.id} + > + {downloadingLayer === layer.id ? ( + + ) : ( + + )} + + ) : ( +
    + )} + showDeleteConfirmation(e, layer)} + disabled={downloadingLayer === layer.id} + > + + +
    +
    + + ) + } + + return ( + + + + + WMS Layer Management + + + {error && {error}} + {success && {success}} + + + + + { + setSearchQuery(e.target.value) + fetchLayers() + }} + /> + + + + + + + navigate('/layers/add')}> + + Upload New Layer + + + + + {loading ? ( +
    + +

    Loading layers...

    +
    + ) : ( + + + + ID + Name + Title + Status + Main Layer + Searchable + Actions + + + + {layers.length > 0 ? ( + layers.map(layer => renderLayerRow(layer)) + ) : ( + + +

    No layers found. Use the upload button to add a new layer.

    +
    +
    + )} +
    +
    + )} +
    +
    +
    + + {/* Delete Confirmation Modal */} + setDeleteModalVisible(false)}> + + Confirm Delete + + +

    Are you sure you want to delete the layer {selectedLayer?.name}?

    +

    This action cannot be undone.

    +
    + + setDeleteModalVisible(false)}> + Cancel + + + {loading ? ( + <> + + Deleting... + + ) : ( + 'Delete' + )} + + +
    + + {/* Download Format Selection Modal */} + setDownloadModalVisible(false)}> + + Download Layer + + +

    Select format to download {selectedLayer?.name}:

    + setDownloadFormat(e.target.value)} + disabled={downloadingLayer === selectedLayer?.id} + > + + + + + +
    + + setDownloadModalVisible(false)} disabled={downloadingLayer === selectedLayer?.id}> + Cancel + + + {downloadingLayer === selectedLayer?.id ? ( + <> + + Downloading... + + ) : ( + 'Download' + )} + + +
    +
    + ) +} + +export default WmsLayerManagement \ No newline at end of file diff --git a/src/views/map/Map.js b/src/views/map/Map.js new file mode 100644 index 000000000..938567095 --- /dev/null +++ b/src/views/map/Map.js @@ -0,0 +1,626 @@ +import React, { useEffect, useRef, useState } from 'react' +import 'ol/ol.css' +import Map from 'ol/Map' +import View from 'ol/View' +import TileLayer from 'ol/layer/Tile' +import VectorLayer from 'ol/layer/Vector' +import VectorSource from 'ol/source/Vector' +import Feature from 'ol/Feature' +import { XYZ } from 'ol/source' +import Zoom from 'ol/control/Zoom' +import WKT from 'ol/format/WKT' +import { Circle as CircleStyle, Fill, Stroke, Style } from 'ol/style' +import { useDispatch } from 'react-redux' +import { handleLogout } from '../../features/auth/authSlice' +import { CCard, CCardBody, CFormSelect } from '@coreui/react' +import './map.css' + +// Import components and utilities +import ProtectedComponent from '../../features/auth/ProtectedComp' +import FeatureProperties from './helpers/properties' +import SearchPanel from './helpers/searchingMap' +import DrawingControl from './helpers/drawingMap' +import CheckboxDropdown from './selector/selectLayer' +import ModeSelection from './helpers/modeSelection' +import axiosInstance from '../../utils/api/axiosConfig' +import { createWMSLayer } from './helpers/layerCreation' +import { addPrintStyles } from './helpers/print' +import { + WMTS_URL, + zoom, + projection, + minZoom, + maxZoom, + SEARCH_URL, + WFS_URL, + center, + mapExtent, + LAYERS_API_URL, +} from './utils/constants' + +const MapComponent = () => { + // Map and ref states + const mapRef = useRef(null) + const mapInstance = useRef(null) + const dispatch = useDispatch() + const token = localStorage.getItem('token') + + // Layer references + const backgroundLayers = useRef({}) + const mainLayers = useRef({}) + + // UI states + const [currentZoom, setCurrentZoom] = useState(zoom) + const [isEditMode, setIsEditMode] = useState(false) + const [showSearchPanel, setShowSearchPanel] = useState(false) + const [showLayerPanel, setShowLayerPanel] = useState(true) + const [modeTransitioning, setModeTransitioning] = useState(false) + + // Data states + const [allLayers, setAllLayers] = useState([]) + const [backgroundLayerData, setBackgroundLayerData] = useState([]) + const [mainLayerData, setMainLayerData] = useState([]) + const [selectedLayers, setSelectedLayers] = useState([]) + const [loadingLayers, setLoadingLayers] = useState(false) + const [featureProperties, setFeatureProperties] = useState(null) + const [shapeId, setShapeId] = useState(null) + + // Search states + const [searchQuery, setSearchQuery] = useState('') + const [searchResults, setSearchResults] = useState([]) + const searchResultsLayer = useRef(null) + + // Fetch available layers from API and categorize them + const fetchAvailableLayers = async () => { + setLoadingLayers(true) + try { + const response = await axiosInstance.get(LAYERS_API_URL) + const createdLayers = response.data.filter((layer) => layer.status === 'CREATED') + + // Separate layers based on isBackground property + const backgrounds = createdLayers.filter((layer) => layer.isBackground) + const mains = createdLayers.filter((layer) => !layer.isBackground) + + setAllLayers(createdLayers) + setBackgroundLayerData(backgrounds) + setMainLayerData(mains) + } catch (error) { + console.error('Error fetching layers:', error) + } finally { + setLoadingLayers(false) + } + } + + // Initialize map + useEffect(() => { + fetchAvailableLayers() + + if (!mapInstance.current) { + // Create base map + mapInstance.current = new Map({ + target: mapRef.current, + view: new View({ + projection: projection, + center: center, + extent: mapExtent, + minZoom: minZoom, + maxZoom: maxZoom, + zoom: zoom, + background: '#e0f7fa', + }), + controls: [ + new Zoom({ + zoomInTipLabel: 'Zoom in', + zoomOutTipLabel: 'Zoom out', + className: 'custom-zoom-control', + }), + ], + layers: [ + new TileLayer({ + source: new XYZ({ + maxZoom: 17, + url: WMTS_URL + '/{z}/{x}/{y}', + tileLoadFunction: (tile, src) => { + const currentToken = localStorage.getItem('token') + const xhr = new XMLHttpRequest() + xhr.open('GET', src, true) + xhr.setRequestHeader('Authorization', `Bearer ${currentToken}`) + xhr.responseType = 'blob' + + xhr.onload = function () { + if (xhr.status === 200) { + const image = tile.getImage() + image.src = URL.createObjectURL(xhr.response) + } else if (xhr.status === 401) { + dispatch(handleLogout()) + } + } + xhr.send() + }, + }), + }), + ], + }) + } + + return () => { + if (mapInstance.current) { + mapInstance.current.setTarget(undefined) + mapInstance.current = null + } + } + }, []) + + // Load background layers when they're available + useEffect(() => { + if (!mapInstance.current || backgroundLayerData.length === 0) return + + const oldBackgroundLayers = { ...backgroundLayers.current } + + backgroundLayers.current = {} + + backgroundLayerData.forEach((layerInfo, index) => { + const layer = createWMSLayer(layerInfo.name, layerInfo.color || '#3388ff', 1 + index, layerInfo.minZoom, layerInfo.maxZoom) + + mapInstance.current.addLayer(layer) + backgroundLayers.current[layerInfo.name] = layer + + layer.getSource().once('imageloadend', () => { + layer.setVisible(layer.values_.minZoom <= currentZoom && layer.values_.maxZoom >= currentZoom) + if (oldBackgroundLayers[layerInfo.name]) { + mapInstance.current.removeLayer(oldBackgroundLayers[layerInfo.name]) + delete oldBackgroundLayers[layerInfo.name] + } + }) + }) + + setTimeout(() => { + Object.values(oldBackgroundLayers).forEach((layer) => { + if (layer && mapInstance.current) { + mapInstance.current.removeLayer(layer) + } + }) + }, 1000) + }, [backgroundLayerData]) + // Handle map events like zoom and click + useEffect(() => { + if (!mapInstance.current) return + + const handleMoveEnd = () => { + const newZoom = mapInstance.current.getView().getZoom() + if (newZoom !== currentZoom) { + setCurrentZoom(newZoom) + + // Update layer visibility based on zoom level + const showBackground = newZoom < 14 + const showMainLayers = newZoom >= 14 + + // Update background layers visibility + Object.values(backgroundLayers.current).forEach((layer) => { + + if (layer) { + layer.setVisible(layer.values_.minZoom <= newZoom && layer.values_.maxZoom >= newZoom) + } + }) + + // Update main layers visibility + Object.values(mainLayers.current).forEach((layer) => { + console.log('Background Layer:', layer.values_.minZoom, layer.values_.maxZoom) + if (layer) { + layer.setVisible(layer.values_.minZoom <= newZoom && layer.values_.maxZoom >= newZoom) + } + }) + } + } + + const handleClick = async (event) => { + if (selectedLayers.length === 0 || !isEditMode) return + + const coordinate = mapInstance.current.getCoordinateFromPixel(event.pixel) + const [lon, lat] = coordinate + + // Use the first selected layer for clicking + const primaryLayer = selectedLayers[0] + + try { + const response = await fetch(`${WFS_URL}/${primaryLayer}?lat=${lat}&lon=${lon}`, { + headers: { Authorization: `Bearer ${token}` }, + }) + + if (response.status === 401) { + dispatch(handleLogout()) + return + } + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + const data = await response.json() + + if (data.features?.[0]) { + + console.log('Feature ID:', data.features[0].id.split('.')[1]) + setShapeId(data.features[0].id.split('.')[1]) + setFeatureProperties({ + ...data.features[0].properties, + geometry: data.features[0].geometry, + }) + } else { + setFeatureProperties(null) + } + } catch (error) { + console.error('Error fetching WFS data:', error) + setFeatureProperties(null) + } + } + + mapInstance.current.on('moveend', handleMoveEnd) + mapInstance.current.on('click', handleClick) + + return () => { + if (mapInstance.current) { + mapInstance.current.un('moveend', handleMoveEnd) + mapInstance.current.un('click', handleClick) + } + } + }, [selectedLayers, currentZoom, token, dispatch, isEditMode]) + + // Update layers when selection changes + useEffect(() => { + if (!mapInstance.current) return + + updateMapLayers() + + // Clear search results + clearResults() + }, [selectedLayers]) + + // Function to update map layers based on selection + const updateMapLayers = () => { + const oldMainLayers = { ...mainLayers.current } + + mainLayers.current = {} + + selectedLayers.forEach((layerName, index) => { + const layerInfo = allLayers.find((layer) => layer.name === layerName) + const layerColor = layerInfo?.color || '#3388ff' + + const newLayer = createWMSLayer(layerName, layerColor, 10 + index, layerInfo?.minZoom, layerInfo?.maxZoom) + + mapInstance.current.addLayer(newLayer) + mainLayers.current[layerName] = newLayer + + newLayer.getSource().once('imageloadend', () => { + newLayer.setVisible(newLayer.values_.minZoom <= currentZoom && newLayer.values_.maxZoom >= currentZoom) + + if (oldMainLayers[layerName]) { + mapInstance.current.removeLayer(oldMainLayers[layerName]) + delete oldMainLayers[layerName] + } + }) + }) + + setTimeout(() => { + Object.values(oldMainLayers).forEach((layer) => { + if (layer && mapInstance.current) { + mapInstance.current.removeLayer(layer) + } + }) + }, 1000) + } + + // Handle layer selection change + const handleLayerSelectChange = (newSelectedLayers) => { + setSelectedLayers(newSelectedLayers) + } + + // Print functionality + const handlePrint = () => { + addPrintStyles() + window.print() + } + + // Search functionality + const handleSearchQueryChange = (query) => { + setSearchQuery(query) + } + + const handleSearch = async () => { + if (!searchQuery.trim()) return + + try { + const layerIds = allLayers + .filter((layer) => selectedLayers.includes(layer.name)) + .map((layer) => layer.id) + + const response = await axiosInstance.get( + `${SEARCH_URL}?query=${encodeURIComponent(searchQuery)}${ + layerIds.length > 0 ? `&layerIds=${layerIds.join(',')}` : '' + }`, + ) + + const results = response.data || [] + setSearchResults(results) + displaySearchResults(results) + } catch (error) { + console.error('Error searching:', error) + setSearchResults([]) + } + } + + const displaySearchResults = (results) => { + // Remove existing search results layer + if (searchResultsLayer.current) { + mapInstance.current.removeLayer(searchResultsLayer.current) + } + + if (results.length === 0) return + + // Create vector source for search results + const vectorSource = new VectorSource() + const wktFormat = new WKT() + + // Add features for each result + results.forEach((result) => { + if (result.data.geom) { + try { + const wktString = result.data.geom.replace(/SRID=\d+;/, '') + const geometry = wktFormat.readGeometry(wktString) + + const feature = new Feature({ + geometry: geometry, + properties: result.data, + }) + + // Style the feature + feature.setStyle( + new Style({ + fill: new Fill({ + color: 'rgba(255, 165, 0, 0.3)', + }), + stroke: new Stroke({ + color: '#ff8c00', + width: 2, + }), + }), + ) + + vectorSource.addFeature(feature) + } catch (error) { + console.error('Error parsing geometry:', error) + } + } + }) + + // Create and add the vector layer + const vectorLayer = new VectorLayer({ + source: vectorSource, + zIndex: 10, + }) + + searchResultsLayer.current = vectorLayer + mapInstance.current.addLayer(vectorLayer) + + // Show search panel when results are displayed + setShowSearchPanel(true) + } + + const handleResultClick = (result) => { + if (result.data.geom) { + try { + // Parse WKT geometry + const wktFormat = new WKT() + const wktString = result.data.geom.replace(/SRID=\d+;/, '') + const geometry = wktFormat.readGeometry(wktString) + + // Get the center of the geometry's extent + const extent = geometry.getExtent() + const center = [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2] + + // Animate to the location + mapInstance.current.getView().animate({ + center: center, + zoom: 16, + duration: 1000, + }) + + // Highlight the selected feature + if (searchResultsLayer.current) { + const features = searchResultsLayer.current.getSource().getFeatures() + features.forEach((feature) => { + const properties = feature.get('properties') + if (properties.kadastr === result.data.kadastr) { + feature.setStyle( + new Style({ + fill: new Fill({ + color: 'rgba(15, 219, 26, 0.3)', + }), + stroke: new Stroke({ + color: 'rgba(15, 219, 25, 0.78)', + width: 3, + }), + }), + ) + } else { + feature.setStyle( + new Style({ + fill: new Fill({ + color: 'rgba(255, 165, 0, 0.3)', + }), + stroke: new Stroke({ + color: '#ff8c00', + width: 2, + }), + }), + ) + } + }) + } + } catch (error) { + console.error('Error handling result click:', error) + } + } + } + + const clearResults = () => { + setSearchQuery('') + setSearchResults([]) + if (searchResultsLayer.current) { + mapInstance.current.removeLayer(searchResultsLayer.current) + searchResultsLayer.current = null + } + } + + // Mode toggle with animation + const toggleMode = () => { + setModeTransitioning(true) + + if (!isEditMode) { + // Switching to edit mode - show single layer + if (selectedLayers.length > 1) { + setSelectedLayers([selectedLayers[0]]) + } + + setTimeout(() => { + setIsEditMode(true) + setShowSearchPanel(true) + setModeTransitioning(false) + }, 300) + } else { + // Switching to view mode - clear properties + setFeatureProperties(null) + clearResults() + setShowSearchPanel(false) + + setTimeout(() => { + setIsEditMode(false) + setModeTransitioning(false) + }, 300) + } + } + + // Reload WMS layers (used after drawing new shapes) + const reloadWMSLayers = () => { + fetchAvailableLayers() + updateMapLayers() + } + + // Add print styles when component mounts + useEffect(() => { + addPrintStyles() + }, []) + + return ( +
    +
    + + {/* Mode Selection Controls */} + + + {/* Drawing Controls (Edit Mode) */} + + {isEditMode && mapInstance.current && selectedLayers.length === 1 && ( +
    + layer.name === selectedLayers[0])} + /> +
    + )} +
    + + {/* Layer Selection Panel */} + + + {!isEditMode ? ( +
    + ({ + ...layer, + displayName: layer.title || layer.name, + }))} + selectedValues={selectedLayers} + onChange={handleLayerSelectChange} + /> +
    + ) : ( +
    + setSelectedLayers(e.target.value ? [e.target.value] : [])} + className="form-select" + size="sm" + > + + {mainLayerData.map((layer) => ( + + ))} + +
    + )} +
    +
    + + {/* Search Panel (Edit Mode) */} + {isEditMode && ( +
    + +
    + )} + + {/* Feature Properties Panel */} + {featureProperties && ( +
    + setFeatureProperties(null)} + onShapeDeleted={reloadWMSLayers} + layerFields={allLayers.find((layer) => layer.name === selectedLayers[0])?.fields} + layerName={allLayers.find((layer) => layer.name === selectedLayers[0])?.name} + shapeID={shapeId} + /> +
    + )} +
    + ) +} + +export default MapComponent diff --git a/src/views/map/helpers/drawingMap.js b/src/views/map/helpers/drawingMap.js new file mode 100644 index 000000000..27edcdeed --- /dev/null +++ b/src/views/map/helpers/drawingMap.js @@ -0,0 +1,411 @@ +import React, { useState, useEffect } from 'react' +import Draw from 'ol/interaction/Draw' +import VectorSource from 'ol/source/Vector' +import VectorLayer from 'ol/layer/Vector' +import { Style, Stroke } from 'ol/style' +import GeoJSON from 'ol/format/GeoJSON' +import { + CCard, + CCardHeader, + CCardBody, + CCardTitle, + CButton, + CForm, + CFormInput, + CFormLabel, + CRow, + CCol, + CFormFeedback, + CAlert, +} from '@coreui/react' +import axiosInstance from '../../../utils/api/axiosConfig' + +const DrawingControl = ({ map, onShapeCreated, selectedLayer }) => { + const [isDrawing, setIsDrawing] = useState(false) + const [showForm, setShowForm] = useState(false) + const [drawInteraction, setDrawInteraction] = useState(null) + const [vectorLayer, setVectorLayer] = useState(null) + const [currentFeature, setCurrentFeature] = useState(null) + const [formData, setFormData] = useState({}) + const [layerFields, setLayerFields] = useState([]) + const [validationErrors, setValidationErrors] = useState({}) + + // Replace toast with simpler message handling like in RolesTable + const [errorMessage, setErrorMessage] = useState('') + const [successMessage, setSuccessMessage] = useState('') + + useEffect(() => { + if (selectedLayer) { + const dynamicFields = selectedLayer.fields.filter( + (field) => + field.name !== 'gid' && + field.name !== 'id' && + field.name !== 'gridcode' && + field.geometryField !== field.name, + ) + setLayerFields(dynamicFields) + + const initialFormData = dynamicFields.reduce((acc, field) => { + acc[field.name] = '' + return acc + }, {}) + setFormData(initialFormData) + + const initialValidationErrors = dynamicFields.reduce((acc, field) => { + acc[field.name] = field.isMandatory ? 'This field is required' : '' + return acc + }, {}) + setValidationErrors(initialValidationErrors) + } + }, [selectedLayer]) + + const resetForm = () => { + const initialFormData = layerFields.reduce((acc, field) => { + acc[field.name] = '' + return acc + }, {}) + setFormData(initialFormData) + + const initialValidationErrors = layerFields.reduce((acc, field) => { + acc[field.name] = field.isMandatory ? 'This field is required' : '' + return acc + }, {}) + setValidationErrors(initialValidationErrors) + } + + useEffect(() => { + const source = new VectorSource() + const vector = new VectorLayer({ + source: source, + style: new Style({ + stroke: new Stroke({ + color: '#0066ff', + width: 2, + }), + }), + zIndex: 100, + }) + + map.addLayer(vector) + setVectorLayer(vector) + + return () => { + if (vector) { + map.removeLayer(vector) + } + } + }, [map]) + + const startDrawing = () => { + if (!vectorLayer) return + + const draw = new Draw({ + source: vectorLayer.getSource(), + type: 'Polygon', + }) + + draw.on('drawend', (event) => { + const feature = event.feature + setCurrentFeature(feature) + setIsDrawing(false) + setShowForm(true) + map.removeInteraction(draw) + }) + + map.addInteraction(draw) + setDrawInteraction(draw) + setIsDrawing(true) + } + + const validateField = (fieldName, value) => { + const field = layerFields.find((f) => f.name === fieldName) + + if (field.isMandatory && !value) { + return 'This field is required' + } + + switch (field.type) { + case 'integer': + return value && !Number.isInteger(Number(value)) ? 'Must be a whole number' : '' + case 'double precision': + return value && isNaN(parseFloat(value)) ? 'Must be a number' : '' + default: + return '' + } + } + + const handleInputChange = (fieldName, value) => { + const field = layerFields.find((f) => f.name === fieldName) + let processedValue = value + + if (value !== '') { + if (field.type === 'integer') { + processedValue = value === '' ? '' : parseInt(value, 10) + } else if (field.type === 'double precision') { + processedValue = value === '' ? '' : parseFloat(value) + } + } + + setFormData((prev) => ({ + ...prev, + [fieldName]: processedValue, + })) + + const error = validateField(fieldName, processedValue) + setValidationErrors((prev) => ({ + ...prev, + [fieldName]: error, + })) + } + + const handleFormSubmit = async (e) => { + e.preventDefault() + + const newValidationErrors = {} + layerFields.forEach((field) => { + const error = validateField(field.name, formData[field.name]) + if (error) { + newValidationErrors[field.name] = error + } + }) + + if (Object.keys(newValidationErrors).length > 0) { + setValidationErrors((prev) => ({ + ...prev, + ...newValidationErrors, + })) + setErrorMessage('Please fix the errors in the form before submitting.') + setTimeout(() => setErrorMessage(''), 2000) + return + } + + if (!currentFeature) { + setErrorMessage('No shape data available. Please draw a shape first.') + setTimeout(() => setErrorMessage(''), 2000) + return + } + + try { + const geoJSONFormat = new GeoJSON() + const featureGeoJSON = geoJSONFormat.writeFeatureObject(currentFeature) + + const finalData = {} + + Object.keys(formData).forEach((key) => { + const field = layerFields.find((f) => f.name === key) + const value = formData[key] + + if (value === '') { + finalData[key] = null + } else { + switch (field.type) { + case 'integer': + finalData[key] = Number.isNaN(parseInt(value)) ? null : parseInt(value, 10) + break + case 'double precision': + finalData[key] = Number.isNaN(parseFloat(value)) ? null : parseFloat(value) + break + default: + finalData[key] = value + } + } + }) + + const shapeData = { + layerName: selectedLayer.name, + coordinates: featureGeoJSON.geometry.coordinates[0], + data: finalData, + } + + await axiosInstance.post('/layers/shape', shapeData) + + setSuccessMessage(`Shape added to ${selectedLayer.name} successfully!`) + setTimeout(() => setSuccessMessage(''), 2000) + + onShapeCreated() + + resetForm() + setShowForm(false) + vectorLayer.getSource().clear() + setCurrentFeature(null) + } catch (error) { + setErrorMessage(`Error saving shape data: ${error.response?.data?.message || error.message}`) + console.error('Error saving shape:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const cancelDrawing = () => { + if (vectorLayer) { + const source = vectorLayer.getSource() + source.clear() + } + if (drawInteraction) { + map.removeInteraction(drawInteraction) + } + resetForm() + setIsDrawing(false) + setShowForm(false) + setCurrentFeature(null) + setErrorMessage('') + setSuccessMessage('') + } + + // Field name to title mapping similar to the one in FeatureProperties + const getFieldTitle = (fieldName) => { + // Default field mappings as in FeatureProperties component + const defaultFieldMappings = { + + } + + // First check if the field has a title property + const field = layerFields.find(f => f.name === fieldName) + if (field && field.title) { + return field.title + } + + // Otherwise use the mapping or fallback to the original field name + return defaultFieldMappings[fieldName] || fieldName + } + + const renderFormInput = (field) => { + const inputProps = { + id: field.name, + value: formData[field.name] === 0 ? '0' : formData[field.name] || '', + onChange: (e) => handleInputChange(field.name, e.target.value), + invalid: !!validationErrors[field.name], + valid: !validationErrors[field.name] && formData[field.name] !== '', + } + + let inputType = 'text' + let inputStep = undefined + + switch (field.type) { + case 'integer': + inputType = 'number' + inputStep = '1' + break + case 'double precision': + inputType = 'number' + inputStep = '0.01' + break + case 'date': + inputType = 'date' + break + } + + return ( + <> + + {validationErrors[field.name] && ( + {validationErrors[field.name]} + )} + + ) + } + + if (!selectedLayer) return null + + return ( + <> +
    + + + + +
    + + {showForm && ( +
    + + + Add Shape for {selectedLayer.title} + + + + {errorMessage && {errorMessage}} + {successMessage && {successMessage}} + + + + {layerFields.length > 0 ? ( + layerFields.map((field) => ( + + + {getFieldTitle(field.name)} + {field.isMandatory && ( + + * + + )} + + {renderFormInput(field)} + + )) + ) : ( + No additional fields for this layer + )} + + +
    + + Save + + + Cancel + +
    +
    +
    +
    +
    + )} + + ) +} + +export default DrawingControl \ No newline at end of file diff --git a/src/views/map/helpers/layerCreation.js b/src/views/map/helpers/layerCreation.js new file mode 100644 index 000000000..03df2af32 --- /dev/null +++ b/src/views/map/helpers/layerCreation.js @@ -0,0 +1,80 @@ +// helpers/layerCreation.js +import ImageLayer from 'ol/layer/Image' +import { ImageWMS } from 'ol/source' +import { WMS_URL } from '../utils/constants'; + +export const createWMSLayer = (layerName, layerColor = '#3388ff', zIndex = 1, minZoom, maxZoom ) => { + const token = localStorage.getItem('token') + const sldBody = ` + + + ${layerName} + + + + + + ${layerColor} + 0.5 + + + #000000 + 1 + + + + + + + + `.replace(/\s+/g, ' '); + + const wmsSource = new ImageWMS({ + url: `${WMS_URL}/${layerName}`, + params: { + TRANSPARENT: true, + SLD_BODY: sldBody, + }, + serverType: 'geoserver', + ratio: 1, + imageLoadFunction: (image, src) => { + const xhr = new XMLHttpRequest(); + xhr.open('GET', src, true); + xhr.setRequestHeader('Authorization', `Bearer ${token}`); + xhr.responseType = 'blob'; + xhr.onload = function () { + if (xhr.status === 200) { + const img = image.getImage(); + img.src = URL.createObjectURL(xhr.response); + img.onerror = function() { + console.warn(`Failed to load image for layer ${layerName}`); + wmsSource.dispatchEvent('imageloadend'); + }; + + img.onload = function() { + wmsSource.dispatchEvent('imageloadend'); + }; + } else { + console.error(`HTTP error loading WMS layer ${layerName}: ${xhr.status}`); + wmsSource.dispatchEvent('imageloadend'); + } + }; + xhr.onerror = function() { + console.error(`Network error loading WMS layer ${layerName}`); + wmsSource.dispatchEvent('imageloadend'); + }; + xhr.send(); + }, + }); + + const layer = new ImageLayer({ + source: wmsSource, + opacity: 0.8, + zIndex: zIndex, + visible: true, + minZoom: minZoom, + maxZoom: maxZoom, + }); + + return layer; +}; \ No newline at end of file diff --git a/src/views/map/helpers/modeSelection.js b/src/views/map/helpers/modeSelection.js new file mode 100644 index 000000000..b22eb7c29 --- /dev/null +++ b/src/views/map/helpers/modeSelection.js @@ -0,0 +1,62 @@ +import React from 'react' +import { CButton, CButtonGroup } from '@coreui/react' +import CIcon from '@coreui/icons-react' +import { cilPrint } from '@coreui/icons' +import ProtectedComponent from '../../../features/auth/ProtectedComp' +export default function ModeSelection({isEditMode, onToggle, onPrint, modeTransitioning}) { + return ( +
    + + + + + View + + + + Edit + + + + + + {!isEditMode && ( + + + + )} + +
    + ) +} + + \ No newline at end of file diff --git a/src/views/map/helpers/print.js b/src/views/map/helpers/print.js new file mode 100644 index 000000000..ebe7149a5 --- /dev/null +++ b/src/views/map/helpers/print.js @@ -0,0 +1,32 @@ +export const addPrintStyles = () => { + const styleId = 'map-print-styles'; + if (!document.getElementById(styleId)) { + const style = document.createElement('style'); + style.id = styleId; + style.innerHTML = ` + @media print { + body * { + visibility: hidden; + } + .map-container, .map-container * { + visibility: visible; + } + .map-container { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + } + .print-hide { + display: none !important; + } + .ol-control { + display: none !important; + } + } + `; + document.head.appendChild(style); + } +}; + diff --git a/src/views/map/helpers/properties.js b/src/views/map/helpers/properties.js new file mode 100644 index 000000000..130419859 --- /dev/null +++ b/src/views/map/helpers/properties.js @@ -0,0 +1,269 @@ +import React, { useEffect, useState } from 'react' +import { useColorModes } from '@coreui/react' +import { CModal, CModalHeader, CModalTitle, CModalBody, CModalFooter, CButton } from '@coreui/react' +import { CIcon } from '@coreui/icons-react' +import { cibOpenstreetmap } from '@coreui/icons' +import axiosInstance from '../../../utils/api/axiosConfig' +import ProtectedComponent from '../../../features/auth/ProtectedComp' + +const cleanAndFormatProperties = (properties, layerFields) => { + + const defaultFieldMappings = { + name: 'Номи', + hujjat_raq: 'Ҳужжат рақами', + hujjat_san: 'Ҳужжат санаси', + huj_maydon: 'Ҳужжат майдони', + huquq: 'Ҳуқуқ', + kadastr: 'Кадастр рақами', + kadastr_qi: 'Кадастр қиймати', + manzil: 'Манзил', + maqsad: 'Мақсад', + maydon: 'Майдон', + nomi: 'Номи', + sana: 'Сана', + toifa: 'Тоифа', + tuman: 'Туман', + tur: 'Тур', + umumiy_foy: 'Умумий фойдаланиш', + zaxvat: 'Ҳудуд', + } + + // Build field mappings from layerFields if available + let fieldMappings = { ...defaultFieldMappings } + + if (layerFields && Array.isArray(layerFields)) { + layerFields.forEach(field => { + if (field.name && field.title) { + fieldMappings[field.name] = field.title + } + }) + } + + // Exclude these fields from display + const excludeFields = [ + // 'field_1', + // 'fid', + // 'viloyat', + // 'zona', + // 'stir', + // 'subyekt', + // 'qurilish_o', + // 'hujjat', + // 'geometry', + ] + + // If layer fields are provided, use them to filter + const activeFields = layerFields + ? layerFields + .filter((field) => field.isActive && !excludeFields.includes(field.name)) + .map((field) => field.name) + : null + + return Object.entries(properties) + .filter(([key, value]) => { + // Filter based on active fields if provided + const isActiveField = !activeFields || activeFields.includes(key) + + return ( + isActiveField && + !excludeFields.includes(key) && + value !== '0' && + value !== 0 && + value !== null && + value !== undefined + ) + }) + .reduce((acc, [key, value]) => { + // Use mapped display name if available, otherwise use original key + acc[fieldMappings[key] || key] = value + return acc + }, {}) +} + +const getThemeStyles = (colorMode) => ({ + backgroundColor: colorMode === 'dark' ? '#212529' : '#f8f9fa', + color: colorMode === 'dark' ? '#fff' : '#212529', + borderBottomColor: colorMode === 'dark' ? 'rgba(255, 255, 255, 0.2)' : '#dee2e6', + labelColor: colorMode === 'dark' ? '#a8a8a8' : '#666666', +}) + +const FeatureProperties = ({ properties, onClose, onShapeDeleted, layerFields, layerName, shapeID}) => { + const { colorMode } = useColorModes('coreui-free-react-admin-template-theme') + const [themeStyles, setThemeStyles] = useState(getThemeStyles(colorMode)) + const [showDeleteModal, setShowDeleteModal] = useState(false) + const [isDeleting, setIsDeleting] = useState(false) + const [coordinates, setCoordinates] = useState(null) + + useEffect(() => { + setThemeStyles(getThemeStyles(colorMode)) + + // Extract coordinates from geometry + if (properties && properties.geometry) { + const coords = getGeometryCenter(properties.geometry) + setCoordinates(coords) + } + }, [colorMode, properties]) + + // Pass layer fields to cleanAndFormatProperties + const cleanedProps = cleanAndFormatProperties(properties, layerFields) + + // Function to get center coordinates from geometry + const getGeometryCenter = (geometry) => { + if (!geometry || !geometry.coordinates) return null + + let result = null + + if (geometry.type === 'Point') { + result = geometry.coordinates + } else if (geometry.type === 'Polygon') { + // Calculate centroid of polygon + const coords = geometry.coordinates[0] + let sumX = 0 + let sumY = 0 + + for (let i = 0; i < coords.length; i++) { + sumX += coords[i][0] + sumY += coords[i][1] + } + + result = [sumX / coords.length, sumY / coords.length] + } else if (geometry.type === 'MultiPolygon') { + // Use first polygon centroid for simplicity + const coords = geometry.coordinates[0][0] + let sumX = 0 + let sumY = 0 + + for (let i = 0; i < coords.length; i++) { + sumX += coords[i][0] + sumY += coords[i][1] + } + + result = [sumX / coords.length, sumY / coords.length] + } + + return result + } + + + + const handleDelete = async () => { + try { + setIsDeleting(true) + + + + await axiosInstance.delete('/layers/shape', { + data: { + layerName, + id: shapeID, + }, + }) + + setShowDeleteModal(false) + setIsDeleting(false) + + if (onShapeDeleted) { + onShapeDeleted() + } + + onClose() + } catch (error) { + console.error('Error deleting shape:', error) + setIsDeleting(false) + } + } + + const navigateToYandexMaps = () => { + if (coordinates) { + // Open Yandex Maps in a new tab + // Format: https://yandex.com/maps/?ll=longitude,latitude&z=15 + const url = `https://yandex.com/maps/?pt=${coordinates[0]},${coordinates[1]}&z=17&l=map`; + window.open(url, '_blank') + } + } + + return ( + <> +
    +
    +
    +

    Майдон маълумотлари

    + {coordinates && ( + + + + )} +
    +
    + +
    +
    +
    + {Object.entries(cleanedProps).map(([key, value]) => ( +
    + {key} + {value} +
    + ))} +
    +
    + + setShowDeleteModal(true)}> + Ўчириш + + +
    +
    + + setShowDeleteModal(false)} + > + + Тасдиқлаш + + Ушбу майдонни ўчиришни хоҳлайсизми? + + setShowDeleteModal(false)}> + Бекор қилиш + + + {isDeleting ? 'Ўчирилмоқда...' : 'Ўчириш'} + + + + + ) +} + +export default FeatureProperties \ No newline at end of file diff --git a/src/views/map/helpers/searchingMap.js b/src/views/map/helpers/searchingMap.js new file mode 100644 index 000000000..3616de18e --- /dev/null +++ b/src/views/map/helpers/searchingMap.js @@ -0,0 +1,168 @@ +import React, { useEffect, useRef } from 'react' +import { CFormInput, CButton, CCard, CCardHeader, CCardBody, CAlert } from '@coreui/react' + +const SearchPanel = ({ + searchQuery, + onSearchQueryChange, + onSearch, + searchResults, + onResultClick, + onClearResults, +}) => { + const debounceRef = useRef(null) + + useEffect(() => { + + if (debounceRef.current) clearTimeout(debounceRef.current) + + debounceRef.current = setTimeout(() => { + if (searchQuery.trim()) { + onSearch(searchQuery) + } else { + onClearResults() + } + }, 400) + + // Clean up on unmount or query change + return () => clearTimeout(debounceRef.current) + }, [searchQuery]) + + return ( + + +
    + onSearchQueryChange(e.target.value)} + className="form-controller" + style={{ + backgroundColor: 'white', + color: 'black', + border: 'none', + boxShadow: 'none', + + }} + /> + {searchQuery !== '' && ( + + + + + + + )} +
    +
    + + {searchResults === null && ( + + An error occurred while searching. Please try again. + + )} + + {searchResults !== null && searchResults.length === 0 && searchQuery.trim() !== '' && ( + + No results found for "{searchQuery}". + + )} + + {searchResults !== null && searchResults.length > 0 && ( +
    +
    +
    + Search Results ({searchResults.length}) +
    +
    +
      + {searchResults.map((result, index) => ( +
    • onResultClick(result)} + className="search-result-item" + style={{ + borderBottom: '1px solid #EEEEEE', + backgroundColor: '#FFFFFF', + ':hover': { + backgroundColor: '#F0F0F0', + }, + }} + > +
      {result.data.name}
      +
      + Cadastre: {result.data.kadastr} +
      +
      + Address: {result.data.manzil} +
      +
    • + ))} +
    +
    + )} +
    +
    + ) +} + +export default SearchPanel diff --git a/src/views/map/map.css b/src/views/map/map.css new file mode 100644 index 000000000..653d1eb26 --- /dev/null +++ b/src/views/map/map.css @@ -0,0 +1,362 @@ +.map-container { + position: relative; + width: 100%; + height:calc(100vh - 68px); + background-color: #f8f9fa; +} + +.map { + width: 100%; + height: 100%; +} + +.layer-panel { + position: absolute; + top: 10px; + right: 10px; + background: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 8px; + box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); + z-index: 1000; + max-width: 250px; +} + +.toggle-button { + background-color: #007bff; + color: white; + border: none; + padding: 8px; + width: 100%; + border-radius: 6px; + cursor: pointer; + font-size: 14px; +} + +.toggle-button:hover { + background-color: #0056b3; +} + +.layer-list { + margin-top: 10px; + max-height: 200px; + overflow-y: auto; + padding: 5px; + border: 1px solid #ddd; + border-radius: 6px; + background: white; +} + +.layer-item { + display: flex; + align-items: center; + margin-bottom: 5px; + font-size: 14px; + color:#0056b3; +} + +.layer-item input { + margin-right: 8px; +} +.feature-properties { + position: absolute; + top: 90px; + right: 10px; + background: rgba(0, 0, 0, 0.8); + padding: 20px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + max-width: 500px; + max-height: 500px; + overflow-y: auto; + color: white; +} + +.feature-properties h3 { + font-size: 18px; +} + +.properties-list { + display: table; + width: 100%; + border-spacing: 0 8px; +} + +.property-item { + display: table-row; +} + +.property-item strong { + display: table-cell; + padding-right: 20px; + white-space: nowrap; + color: #a8a8a8; + text-transform: capitalize; +} + +.property-item span { + display: table-cell; + word-break: break-word; +} + +.feature-properties::-webkit-scrollbar { + width: 8px; +} + +.feature-properties::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.1); + border-radius: 4px; +} + +.feature-properties::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.3); + border-radius: 4px; +} + +.feature-properties::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.4); +} +.close-button { + background: none; + border: none; + font-size: 16px; + cursor: pointer; + color: red; +} + +.close-button:hover { + color: darkred; +} +.feature-header { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 16px; + font-weight: bold; + + margin: 0 0 15px 0; + padding-bottom: 10px; + border-bottom: 1px solid rgba(255, 255, 255, 0.2); +} + +.search-panel { + position: absolute; + top: 10px; + left:15px; + outline: none; + border: none; + z-index: 2; + width: 400px; + max-height: 400px; +} + +.search-header { + background-color: #fff; + border-radius: 4px 4px 0 0; +} + +.search-input-container { + display: flex; + gap: 5px; +} + +.search-input-container .form-control { + flex: 1; + outline: none; + border: none; +} +.form-controller ::placeholder { + color: black !important; +} +.form-controller::-webkit-input-placeholder { + color: black !important; + opacity: 1 !important; +} + +.form-controller::-moz-placeholder { + color: black !important; + opacity: 1 !important; +} + +.form-controller:-ms-input-placeholder { + color: black !important; + opacity: 1 !important; +} + +.form-controller:-moz-placeholder { + color: black !important; + opacity: 1 !important; +} +.search-results { + max-height: 300px; + overflow-y: auto; +} + +.search-results-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px 10px; +} + +.search-results h5 { + margin: 0; /* Remove default margins */ + font-size: 14px; + color: #0056b3; + font-weight: 600; +} + +.close-button { + display: flex; + justify-content: center; + align-items: center; + padding: 5px; + border-radius: 50%; +} + +.search-results-list { + list-style: none; + padding: 0; + margin: 0; +} + +.search-result-item { + padding: 8px; + cursor: pointer; + border-bottom: 1px solid #eee; + font-size: 13px; +} + +.search-result-item:hover { + background-color: #f0f0f0; +} + +.search-result-item:last-child { + border-bottom: none; +} + +.search-results::-webkit-scrollbar { + width: 0px; +} + +.search-results::-webkit-scrollbar:hover { + width: 6px; +} + +.search-results::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +.search-results::-webkit-scrollbar-thumb:hover { + background-color: rgba(0, 0, 0, 0.5); +} +.toggle-button { + background-color: #007bff; + color: white; + border: none; + padding: 8px; + width: 100%; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s ease; +} + +.toggle-button:hover { + background-color: #0056b3; +} +.form-select { + width: 200px; + padding: 10px 14px; + border: none; + box-shadow: none; + border-radius: 4px; + outline: none; + height: 44px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; +} +/* Enhanced Zoom Control Styles */ +.custom-zoom-control { + position: absolute; + bottom: 10px; + right: 10px; + background: transparent; + border: none; + box-shadow: none; +} + +.custom-zoom-control button { + width: 35px; + height: 35px; + margin: 4px 0; + background-color: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + color: black; + font-size: 20px; + font-weight: bold; + display: flex; + justify-content: center; + align-items: center; + transition: all 0.2s ease; + cursor: pointer; +} + +.custom-zoom-control button:hover { + background-color: #fff; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + transform: translateY(-2px); +} + +.custom-zoom-control button:active { + background-color: #f8f9fa; + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); + transform: translateY(0); +} + +/* Customize individual zoom buttons */ +.custom-zoom-control .ol-zoom-in { + border-radius: 8px 8px 4px 4px; +} + +.custom-zoom-control .ol-zoom-out { + border-radius: 4px 4px 8px 8px; +} + +/* Adding a subtle divider between buttons */ +.custom-zoom-control .ol-zoom-in:after { + content: ''; + position: absolute; + bottom: -2px; + left: 10%; + width: 80%; + height: 1px; + background-color: rgba(0, 0, 0, 0.1); +} + +/* Add responsive styles for mobile */ +@media (max-width: 768px) { + .custom-zoom-control { + bottom: 60px; + } + + .custom-zoom-control button { + width: 36px; + height: 36px; + font-size: 18px; + } +} + +/* Make sure buttons don't appear when printing */ +@media print { + .custom-zoom-control { + display: none !important; + } +} \ No newline at end of file diff --git a/src/views/map/selector/selectLayer.css b/src/views/map/selector/selectLayer.css new file mode 100644 index 000000000..65be4cff5 --- /dev/null +++ b/src/views/map/selector/selectLayer.css @@ -0,0 +1,146 @@ +.layer-dropdown-wrapper { + width: 200px; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, + 'Open Sans', sans-serif; +} +.layer-dropdown { + width: 100%; +} +.layer-dropdown .layer-dropdown-toggle { + width: 100%; + border: 1px solid rgba(255, 255, 255, 0.2); + padding: 10px 14px; + border-radius: 6px; + + backdrop-filter: blur(4px); + display: flex; + align-items: center; + justify-content: space-between; + + transition: all 0.2s ease; + + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + width: 100%; +} + +.layer-dropdown .layer-dropdown-toggle:hover { + background-color: rgba(30, 41, 59, 0.9); + border-color: rgba(255, 255, 255, 0.3); +} + +.layer-dropdown-text { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.layer-dropdown .layer-dropdown-menu { + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + background-color: rgba(30, 41, 59, 0.95); + backdrop-filter: blur(8px); + max-height: 250px; + overflow-y: auto; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); + animation: layerDropdownFade 0.2s ease; + width: 100%; + padding: 0; + margin-top: 5px; +} + +@keyframes layerDropdownFade { + from { + opacity: 0; + transform: translateY(-5px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.layer-dropdown-item { + padding: 10px 14px; + transition: background-color 0.2s ease; + + cursor: default; +} + +.layer-dropdown-item:hover { + background-color: rgba(59, 130, 246, 0.2); +} + +.layer-checkbox-label { + display: flex; + align-items: center; + cursor: pointer; + width: 100%; + font-size: 14px; +} + +.layer-checkbox { + -webkit-appearance: none; + appearance: none; + background-color: rgba(255, 255, 255, 0.1); + margin: 0; + width: 18px; + height: 18px; + border: 1px solid rgba(30, 24, 24, 0.3); + border-radius: 4px; + transform: translateY(-0.075em); + display: grid; + place-content: center; + margin-right: 10px; + transition: all 0.2s ease; +} + +.layer-checkbox::before { + content: ''; + width: 10px; + height: 10px; + transform: scale(0); + transition: transform 0.2s ease; + box-shadow: inset 1em 1em rgba(59, 130, 246, 0.9); + transform-origin: center; + clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%); +} + +.layer-checkbox:checked { + background-color: rgba(59, 130, 246, 0.2); + border-color: rgba(59, 130, 246, 0.6); +} + +.layer-checkbox:checked::before { + transform: scale(1); +} + +.layer-checkbox:focus { + outline: none; + border-color: rgba(59, 130, 246, 0.8); +} + +.layer-dropdown-menu::-webkit-scrollbar { + width: 6px; +} + +.layer-dropdown-menu::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); + border-radius: 3px; +} + +.layer-dropdown-menu::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 3px; +} + +.layer-dropdown-menu::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); +} + +.layer-dropdown-empty { + padding: 12px 14px; + color: rgba(255, 255, 255, 0.5); + text-align: center; + font-style: italic; + font-size: 13px; +} diff --git a/src/views/map/selector/selectLayer.js b/src/views/map/selector/selectLayer.js new file mode 100644 index 000000000..0885fa59c --- /dev/null +++ b/src/views/map/selector/selectLayer.js @@ -0,0 +1,79 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { + CDropdown, + CDropdownToggle, + CDropdownMenu, + CDropdownItem +} from '@coreui/react'; +import './selectLayer.css'; + +const CheckboxDropdown = ({ options = [], selectedValues = [], onChange }) => { + const [visible, setVisible] = useState(false); + const dropdownRef = useRef(null); + + useEffect(() => { + const handleClickOutside = (event) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setVisible(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + const handleCheckboxChange = (value) => { + const newSelectedValues = selectedValues.includes(value) + ? selectedValues.filter((v) => v !== value) + : [...selectedValues, value]; + onChange(newSelectedValues); + }; + + // Find display names for selected layers to show in the dropdown title + const getSelectedDisplayNames = () => { + return selectedValues.map(value => { + const option = options.find(opt => opt.name === value); + return option?.displayName || option?.title || value; + }); + }; + + const selectedDisplayNames = getSelectedDisplayNames(); + const displayText = selectedValues.length > 0 + ? `${selectedValues.length} layer${selectedValues.length > 1 ? 's' : ''} selected` + : "Select layers"; + + return ( +
    + setVisible(!visible)}> + + {displayText} + + + + {options.length === 0 ? ( +
    No layers available
    + ) : ( + options.map((option) => ( + + + + )) + )} +
    +
    +
    + ); +}; + +export default CheckboxDropdown; \ No newline at end of file diff --git a/src/views/map/utils/constants.js b/src/views/map/utils/constants.js new file mode 100644 index 000000000..c678f025f --- /dev/null +++ b/src/views/map/utils/constants.js @@ -0,0 +1,11 @@ +export const WMS_URL = 'http://109.199.124.208:2000/layers/wms' +export const WFS_URL = 'http://109.199.124.208:2000/layers/wfs' +export const WMTS_URL = 'http://109.199.124.208:2000/layers/wmts' +export const SEARCH_URL = 'http://109.199.124.208:2000/layers/search' +export const LAYERS_API_URL = 'http://109.199.124.208:2000/layers' +export const mapExtent = [69.5, 41, 70.5, 42] +export const minZoom = 10 +export const maxZoom = 17 +export const zoom = 13 +export const center = [69.95, 41.65] +export const projection = 'EPSG:4326' \ No newline at end of file diff --git a/src/views/pages/login/Login.js b/src/views/pages/login/Login.js index 1b2ee0baa..f7d8df0ed 100644 --- a/src/views/pages/login/Login.js +++ b/src/views/pages/login/Login.js @@ -1,5 +1,6 @@ -import React from 'react' -import { Link } from 'react-router-dom' +import React, { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { useDispatch, useSelector } from 'react-redux' import { CButton, CCard, @@ -12,28 +13,63 @@ import { CInputGroup, CInputGroupText, CRow, + CAlert, + CSpinner } from '@coreui/react' import CIcon from '@coreui/icons-react' import { cilLockLocked, cilUser } from '@coreui/icons' +import { login } from '../../../features/auth/authSlice' const Login = () => { + const [user, setUsername] = useState('') + const [password, setPassword] = useState('') + const dispatch = useDispatch() + const navigate = useNavigate() + const { loading, error, isAuthenticated } = useSelector((state) => state.auth) + + // Redirect if user is already logged in + useEffect(() => { + if (isAuthenticated) { + navigate('/', { replace: true }) + } + }, [isAuthenticated, navigate]) + + const handleSubmit = async (e) => { + e.preventDefault() + + const success = await dispatch(login({ user, password })) + if (success) { + navigate('/', { replace: true }) + } + } + return (
    - - + + - -

    Login

    -

    Sign In to your account

    + +

    Login

    +

    Sign In to your account

    + + {error && {error}} + - + setUsername(e.target.value)} + required + /> + @@ -42,12 +78,16 @@ const Login = () => { type="password" placeholder="Password" autoComplete="current-password" + value={password} + onChange={(e) => setPassword(e.target.value)} + required /> + - - Login + + {loading ? : 'Login'} @@ -59,22 +99,6 @@ const Login = () => {
    - - -
    -

    Sign up

    -

    - Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. -

    - - - Register Now! - - -
    -
    -
    diff --git a/src/views/pages/page403/unauthoried.js b/src/views/pages/page403/unauthoried.js new file mode 100644 index 000000000..cea622b63 --- /dev/null +++ b/src/views/pages/page403/unauthoried.js @@ -0,0 +1,14 @@ +import React from 'react'; + +const Unauthorized = () => { + return ( +
    +
    +

    403 - Unauthorized

    +

    You do not have permission to access this page.

    +
    +
    + ); +}; + +export default Unauthorized; \ No newline at end of file diff --git a/src/views/pages/page404/Page404.js b/src/views/pages/page404/Page404.js index d7fe9a0a2..579711470 100644 --- a/src/views/pages/page404/Page404.js +++ b/src/views/pages/page404/Page404.js @@ -21,16 +21,12 @@ const Page404 = () => {

    404

    Oops! You{"'"}re lost.

    - The page you are looking for was not found. + The page you are looking for was not found.

    + Go back to home page! +
    - - - - - - Search - + diff --git a/src/views/profile/Profile.js b/src/views/profile/Profile.js new file mode 100644 index 000000000..9f203bce4 --- /dev/null +++ b/src/views/profile/Profile.js @@ -0,0 +1,281 @@ +import React, { useState, useEffect } from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { + CContainer, + CCard, + CCardBody, + CCardHeader, + CCardFooter, + CCol, + CRow, + CForm, + CFormLabel, + CFormInput, + CButton, + CListGroup, + CListGroupItem, + CBadge, + CSpinner +} from '@coreui/react'; +import { updateUser } from '../../features/auth/authSlice'; +import axiosInstance from '../../utils/api/axiosConfig'; + +const UserProfilePage = () => { + const user = useSelector((state) => state.auth.user); + const dispatch = useDispatch(); + + const [isEditing, setIsEditing] = useState(false); + const [editedFirstName, setEditedFirstName] = useState(''); + const [editedLastName, setEditedLastName] = useState(''); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + const [departments, setDepartments] = useState([]); + const [departmentName, setDepartmentName] = useState(''); + const [isLoadingDepartments, setIsLoadingDepartments] = useState(false); + + // Set edited name values whenever user data changes + useEffect(() => { + if (user) { + setEditedFirstName(user.firstName); + setEditedLastName(user.lastName); + fetchDepartments(); + } + }, [user]); + + const fetchDepartments = async () => { + if (!user || !user.departmentId) return; + + setIsLoadingDepartments(true); + try { + const response = await axiosInstance.get('/api/departments', { + params: { + page: 0, + pageSize: 100, // Get a reasonable number of departments + }, + }); + + if (response.data && response.data.data) { + setDepartments(response.data.data); + + // Find the department that matches the user's departmentId + const userDepartment = response.data.data.find( + (dept) => dept.id === user.departmentId + ); + + if (userDepartment) { + setDepartmentName(userDepartment.name); + } else { + setDepartmentName('Unknown Department'); + } + } + } catch (error) { + console.error('Error fetching departments:', error); + setDepartmentName('Unknown Department'); + } finally { + setIsLoadingDepartments(false); + } + }; + + const handleSave = async () => { + setIsSaving(true); + setError(null); + + try { + // Use the updateUser thunk action + const userData = { + firstName: editedFirstName, + lastName: editedLastName + }; + + const success = await dispatch(updateUser(userData)); + + if (success) { + setIsEditing(false); + } else { + setError('Failed to update user data. Please try again later.'); + } + } catch (err) { + console.error('Error updating user data:', err); + setError('Failed to update user data. Please try again later.'); + } finally { + setIsSaving(false); + } + }; + + const handleCancel = () => { + // Reset to current user data from Redux + setEditedFirstName(user.firstName); + setEditedLastName(user.lastName); + setIsEditing(false); + setError(null); + }; + + if (!user) { + return ( + + + + ); + } + + return ( + + + + + + User Profile + + + {error && ( +
    + {error} +
    + )} + + + {/* + + User ID + + +

    {user.id}

    +
    +
    */} + + + + First Name + + + {isEditing ? ( + setEditedFirstName(e.target.value)} + /> + ) : ( +

    {user.firstName}

    + )} +
    +
    + + + + Last Name + + + {isEditing ? ( + setEditedLastName(e.target.value)} + /> + ) : ( +

    {user.lastName}

    + )} +
    +
    + + + + Email + + +

    {user.login}

    +
    +
    + + + + Role + + +

    + {user.role.name} +

    +
    +
    + + + + Department + + +

    + {isLoadingDepartments ? ( + + ) : ( + <> + {departmentName} + + )} +

    +
    +
    +
    +
    + + {isEditing ? ( + <> + + {isSaving ? : 'Save Changes'} + + + Cancel + + + ) : ( + setIsEditing(true)}> + Edit Profile + + )} + +
    +
    + + + + + Permissions + + + + {user.role.actions.map((action) => ( + +
    {action.name}
    +
    + {action.allowedMethods.map((method, index) => ( + + + + ))} +
    +
    + ))} +
    +
    +
    +
    +
    +
    + ); +}; + +export default UserProfilePage; \ No newline at end of file diff --git a/src/views/roles/Roles.js b/src/views/roles/Roles.js new file mode 100644 index 000000000..eb082017f --- /dev/null +++ b/src/views/roles/Roles.js @@ -0,0 +1,379 @@ +import React, { useEffect, useState } from 'react' +import axiosInstance from '../../utils/api/axiosConfig' +import ProtectedComponent from '../../features/auth/ProtectedComp' +import { + CTable, + CTableHead, + CTableBody, + CTableRow, + CTableHeaderCell, + CTableDataCell, + CFormInput, + CFormSelect, + CButton, + CContainer, + CPagination, + CPaginationItem, + CModal, + CModalHeader, + CModalTitle, + CModalBody, + CModalFooter, + CForm, + CAlert, + CFormCheck, + CBadge, +} from '@coreui/react' +import { cilPencil, cilTrash, cilPlus } from '@coreui/icons' +import CIcon from '@coreui/icons-react' +import { checkUserPermission } from '../../features/access/permission' +import { useSelector } from 'react-redux'; +const RolesTable = () => { + const [roles, setRoles] = useState([]) + const [search, setSearch] = useState('') + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [totalPages, setTotalPages] = useState(1) + const [deleteModalVisible, setDeleteModalVisible] = useState(false) + const [editModalVisible, setEditModalVisible] = useState(false) + const [addModalVisible, setAddModalVisible] = useState(false) + const [selectedRole, setSelectedRole] = useState(null) + const [errorMessage, setErrorMessage] = useState('') + const [successMessage, setSuccessMessage] = useState('') + const [actions, setActions] = useState([]) + const currentUser = useSelector((state) => state.auth.user) + + useEffect(() => { + fetchRoles() + if (checkUserPermission(currentUser, '/roles', 'Actions')) { + fetchActions() + } + }, [page, pageSize, search]) + + const fetchRoles = async () => { + try { + const response = await axiosInstance.get('/api/roles', { + params: { + page: page - 1, + pageSize, + name: search || undefined, + }, + }) + if (response.data) { + setRoles(response.data.data) + } + + setTotalPages(Math.ceil(response.data.count / pageSize)) + } catch (error) { + console.error('Error fetching roles:', error) + } + } + + const fetchActions = async () => { + try { + const response = await axiosInstance.get('/api/actions') + setActions(response.data.data || []) + } catch (error) { + console.error('Error fetching actions:', error) + } + } + + const showDeleteConfirmation = (role) => { + setSelectedRole(role) + setDeleteModalVisible(true) + } + + const handleDelete = async () => { + try { + await axiosInstance.delete(`/api/roles/${selectedRole.id}`) + setDeleteModalVisible(false) + setSuccessMessage('Role deleted successfully') + fetchRoles() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error deleting role') + console.error('Error deleting role:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const showEditModal = (role) => { + setSelectedRole({ + ...role, + actionIds: role.actions.map((action) => action.id), + }) + setEditModalVisible(true) + } + + const showAddModal = () => { + setSelectedRole({ + name: '', + actionIds: [], + }) + setAddModalVisible(true) + } + + const handleEditSubmit = async (e) => { + e.preventDefault() + try { + await axiosInstance.put(`/api/roles/${selectedRole.id}`, { + name: selectedRole.name, + actionIds: selectedRole.actionIds, + }) + setEditModalVisible(false) + setSuccessMessage('Role updated successfully') + fetchRoles() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error updating role') + console.error('Error updating role:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const handleAddSubmit = async (e) => { + e.preventDefault() + try { + await axiosInstance.post('/api/roles', { + name: selectedRole.name, + actionIds: selectedRole.actionIds, + }) + setAddModalVisible(false) + setSuccessMessage('Role added successfully') + fetchRoles() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error adding role') + console.error('Error adding role:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const handleInputChange = (e) => { + const { name, value } = e.target + setSelectedRole((prev) => ({ + ...prev, + [name]: value, + })) + } + + const handleActionChange = (actionId) => { + setSelectedRole((prev) => { + const actionIds = prev.actionIds.includes(actionId) + ? prev.actionIds.filter((id) => id !== actionId) + : [...prev.actionIds, actionId] + return { ...prev, actionIds } + }) + } + + return ( + + {errorMessage && {errorMessage}} + {successMessage && {successMessage}} + +
    +
    + setSearch(e.target.value)} + className="flex-grow-1" + /> + + + {/* */} + Add + + +
    +
    + + {/* Table Container */} +
    + + + + + Name + Actions + + Operations + + + + + {roles + ? roles.map((role, index) => ( + + {(page - 1) * pageSize + index + 1} + {role.name} + +
    + {role.actions.map((action) => ( + + {action.name} + + ))} +
    +
    + + + showEditModal(role)}> + + + showDeleteConfirmation(role)} + > + + + + +
    + )) + : null} +
    +
    +
    + + {/* Pagination Section */} +
    +
    + setPageSize(Number(e.target.value))} + className="w-auto" + > + {[5, 10, 20, 50].map((size) => ( + + ))} + + + setPage(page - 1)}> + Prev + + {[...Array(totalPages)].map((_, i) => ( + setPage(i + 1)}> + {i + 1} + + ))} + setPage(page + 1)}> + Next + + +
    +
    + + {/* Add Role Modal */} + setAddModalVisible(false)}> + setAddModalVisible(false)}> + Add New Role + + + +
    + +
    +
    + + {actions.map((action) => ( + handleActionChange(action.id)} + /> + ))} +
    +
    + + Create Role + +
    +
    +
    +
    + + {/* Edit Role Modal */} + setEditModalVisible(false)}> + setEditModalVisible(false)}> + Edit Role + + + +
    + +
    +
    + + {actions.map((action) => ( + handleActionChange(action.id)} + /> + ))} +
    +
    + + Save Changes + +
    +
    +
    +
    + + {/* Delete Confirmation Modal */} + setDeleteModalVisible(false)}> + setDeleteModalVisible(false)}> + Confirm Delete + + Are you sure you want to delete {selectedRole?.name}? + + setDeleteModalVisible(false)}> + Cancel + + + Delete + + + +
    + ) +} + +export default RolesTable diff --git a/src/views/users/Users.js b/src/views/users/Users.js new file mode 100644 index 000000000..3ef530e27 --- /dev/null +++ b/src/views/users/Users.js @@ -0,0 +1,493 @@ +import React, { useEffect, useState } from 'react' +import axiosInstance from '../../utils/api/axiosConfig' +import { + CTable, + CTableHead, + CTableBody, + CTableRow, + CTableHeaderCell, + CTableDataCell, + CFormInput, + CFormSelect, + CButton, + CContainer, + CPagination, + CPaginationItem, + CModal, + CModalHeader, + CModalTitle, + CModalBody, + CModalFooter, + CForm, + CAlert, +} from '@coreui/react' +import { cilPencil, cilTrash, cilPlus } from '@coreui/icons' // Added cilPlus for the Add button +import CIcon from '@coreui/icons-react' +import ProtectedComponent from '../../features/auth/ProtectedComp' +const UsersTable = () => { + const [users, setUsers] = useState([]) + const [search, setSearch] = useState('') + const [roleFilter, setRoleFilter] = useState('') + const [departments, setDepartments] = useState([]) + const [page, setPage] = useState(1) + const [pageSize, setPageSize] = useState(10) + const [totalPages, setTotalPages] = useState(1) + const [deleteModalVisible, setDeleteModalVisible] = useState(false) + const [editModalVisible, setEditModalVisible] = useState(false) + const [addModalVisible, setAddModalVisible] = useState(false) // State for Add User modal + const [selectedUser, setSelectedUser] = useState(null) + const [errorMessage, setErrorMessage] = useState('') + const [successMessage, setSuccessMessage] = useState('') + const [roles, setRoles] = useState([]) + + useEffect(() => { + fetchUsers() + fetchDepartments() + fetchRoles() + }, [page, pageSize, search, roleFilter]) + + const fetchUsers = async () => { + try { + const response = await axiosInstance.get('/api/users', { + params: { + page: page - 1, + pageSize, + name: search || undefined, + roleId: roleFilter || undefined, + }, + }) + setUsers(response.data.data) + setTotalPages(Math.ceil(response.data.count / pageSize)) + } catch (error) { + console.error('Error fetching users:', error) + } + } + const fetchDepartments = async () => { + try { + const response = await axiosInstance.get('/api/departments', { + params: { page: 0, pageSize: 100 }, + }) + setDepartments(response.data.data) + } catch (error) { + console.error('Error fetching departments:', error) + } + } + const fetchRoles = async () => { + try { + const response = await axiosInstance.get('/api/roles', { + params: { page: 0, pageSize: 100 }, + }) + setRoles(response.data.data) + } catch (error) { + console.error('Error fetching roles:', error) + } + } + + const getDepartmentName = (departmentId) => { + const department = departments.find((dept) => dept.id === departmentId) + return department ? department.name : 'N/A' + } + const showDeleteConfirmation = (user) => { + setSelectedUser(user) + setDeleteModalVisible(true) + } + + const handleDelete = async () => { + try { + await axiosInstance.delete(`/api/users/${selectedUser.id}`) + setDeleteModalVisible(false) + setSuccessMessage('User deleted successfully') + fetchUsers() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error deleting user') + console.error('Error deleting user:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const showEditModal = (user) => { + setSelectedUser({ ...user, departmentId: user.departmentId || '' }) + setEditModalVisible(true) + } + + const showAddModal = () => { + setSelectedUser({ + firstName: '', + lastName: '', + login: '', + role: { id: '', name: '' }, + departmentId: '', + }) + setAddModalVisible(true) + } + + const handleEditSubmit = async (e) => { + e.preventDefault() + try { + await axiosInstance.put(`/api/users/${selectedUser.id}`, { + firstName: selectedUser.firstName, + lastName: selectedUser.lastName, + login: selectedUser.login, + roleId: selectedUser.role.id, + departmentId: selectedUser.departmentId, + }) + setEditModalVisible(false) + setSuccessMessage('User updated successfully') + fetchUsers() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error updating user') + console.error('Error updating user:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const handleAddSubmit = async (e) => { + e.preventDefault() + try { + await axiosInstance.post('/api/users', { + firstName: selectedUser.firstName, + lastName: selectedUser.lastName, + email: selectedUser.login, + password: selectedUser.password, + roleId: parseInt(selectedUser.role.id, 10), + departmentId: parseInt(selectedUser.departmentId, 10), + }) + setAddModalVisible(false) + setSuccessMessage('User added successfully') + fetchUsers() + setTimeout(() => setSuccessMessage(''), 2000) + } catch (error) { + setErrorMessage('Error adding user') + console.error('Error adding user:', error) + setTimeout(() => setErrorMessage(''), 2000) + } + } + + const handleInputChange = (e) => { + const { name, value } = e.target + setSelectedUser((prev) => ({ + ...prev, + [name]: value, + })) + } + + return ( + + {errorMessage && {errorMessage}} + {successMessage && {successMessage}} + +
    +
    +
    + setSearch(e.target.value)} + className="flex-grow-1" + /> + setRoleFilter(e.target.value)} + style={{ maxWidth: '200px' }} + > + + {roles.map((role) => ( + + ))} + +
    + + + {/* */} + Add User + + +
    +
    + + {/* Table Container */} +
    + + + + + Full Name + Email + Role + Department + + Actions + + + + + {users.map((user, index) => ( + + {(page - 1) * pageSize + index + 1} + + {user.firstName} {user.lastName} + + {user.login} + {user.role.name} + {getDepartmentName(user.departmentId)} + + + showEditModal(user)}> + + + showDeleteConfirmation(user)} + > + + + + + + ))} + + +
    + +
    +
    + setPageSize(Number(e.target.value))} + className="w-auto" + > + {[5, 10, 20, 50].map((size) => ( + + ))} + + + setPage(page - 1)}> + Prev + + {[...Array(totalPages)].map((_, i) => ( + setPage(i + 1)}> + {i + 1} + + ))} + setPage(page + 1)}> + Next + + +
    +
    + + setAddModalVisible(false)}> + setAddModalVisible(false)}> + Add New User + + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + setSelectedUser((prev) => ({ + ...prev, + role: { + id: e.target.value, + name: e.target.options[e.target.selectedIndex].text, + }, + })) + } + required + > + + {roles.map((role) => ( + + ))} + +
    +
    + + + {departments.map((dept) => ( + + ))} + +
    +
    + + Create User + +
    +
    +
    +
    + + setEditModalVisible(false)}> + setEditModalVisible(false)}> + Edit User + + + +
    + +
    +
    + +
    +
    + +
    +
    + + setSelectedUser((prev) => ({ + ...prev, + role: { + id: e.target.value, + name: e.target.options[e.target.selectedIndex].text, + }, + })) + } + required + > + + {roles.map((role) => ( + + ))} + +
    +
    + + + {departments.map((dept) => ( + + ))} + +
    +
    + + Save Changes + +
    +
    +
    +
    + + setDeleteModalVisible(false)}> + setDeleteModalVisible(false)}> + Confirm Delete + + + Are you sure you want to delete {selectedUser?.firstName} {selectedUser?.lastName}? + + + setDeleteModalVisible(false)}> + Cancel + + + Delete + + + +
    + ) +} + +export default UsersTable diff --git a/src/views/users/UsersTable.css b/src/views/users/UsersTable.css new file mode 100644 index 000000000..e69de29bb