DONE: first init
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM node:21-alpine AS build
|
||||
WORKDIR /admin-app
|
||||
COPY package*.json .
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 5001
|
||||
CMD ["npm", "run", "preview"]
|
||||
@@ -0,0 +1,7 @@
|
||||
## Build instructions
|
||||
|
||||
- cd **admin-app**
|
||||
- npm **install**
|
||||
- npm **run dev**
|
||||
|
||||
#### **NOTE:** requires Node 21
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link href="https://api.fontshare.com/v2/css?f[]=general-sans@200,300,400,500,600,700&display=swap" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BBQMS Admin App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4803
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "admin-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 5000",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-oauth/google": "^0.12.1",
|
||||
"bootstrap": "^5.3.3",
|
||||
"bootstrap-icons": "^1.11.3",
|
||||
"formik": "^2.4.5",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-icons": "^5.0.1",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"rsuite": "^5.59.0",
|
||||
"validator": "^13.11.0",
|
||||
"yup": "^1.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
}
|
||||
Generated
+3645
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
.background-color-app{
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import Header from './components/Header/Header.jsx';
|
||||
import { SERVER_URL } from './constants.js';
|
||||
import { UserContext } from './context/UserContext.jsx';
|
||||
import { fetchData } from './fetching/Fetch.js';
|
||||
import AuthGuard from './components/AuthGuard/AuthGuard';
|
||||
import LoginScreen from './pages/LoginScreen/LoginScreen';
|
||||
import CompanyInfoUpdate from './pages/CompanyInfoUpdate/CompanyInfoUpdate';
|
||||
import HomePage from './pages/HomePage/HomePage';
|
||||
import NotFound from './pages/NotFound/NotFound.jsx';
|
||||
import CanAccess from './components/CanAccess/CanAccess';
|
||||
import HomePageCard from './components/HomePageCard/HomePageCard';
|
||||
import AdminProfile from './pages/AdminProfile/AdminProfile';
|
||||
import LoginAuth from './components/LoginAuth/LoginAuth';
|
||||
import ManageAdmins from './pages/AdminManagingScreen/AdminManagingScreen';
|
||||
import ManageServices from './pages/ManageServices/ManageServices';
|
||||
import ManageBranches from './pages/ManageBranchesScreen/ManageBranchesScreen';
|
||||
import ManageGroups from './pages/ManageGroupsScreen/ManageGroupsScreen';
|
||||
import ManageStations from './pages/ManageStationScreen/ManageStationScreen';
|
||||
import ManageDisplays from './pages/ManageDisplays/ManageDisplays';
|
||||
import ManageUsers from './pages/UserManagingScreen/UserManagingScreen';
|
||||
import ViewQueues from './pages/ViewBranchQueues/ViewBranchQueues';
|
||||
import { ROLES } from './constants.js';
|
||||
|
||||
export default function App() {
|
||||
const [user, setUser] = useState();
|
||||
/*
|
||||
Kada se logiramo, ako vec postoji token u localStorage, provjerimo da li je validan (nije istekao)
|
||||
Ako je validan, ulogujemo usera, ako nije ocistimo storage od starih podataka
|
||||
*/
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
const url = `${ SERVER_URL }/api/v1/auth`;
|
||||
fetchData(url, 'GET')
|
||||
.then(({ data, success }) => {
|
||||
if (success) {
|
||||
setUser(JSON.parse(localStorage.getItem('userData')));
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userData');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserContext.Provider value={ { user, setUser } }>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route exact path="/:tenantCode/manage/displays" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageDisplays />
|
||||
</AuthGuard> } />
|
||||
<Route exact path="/:tenantCode/manage/stations" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageStations />
|
||||
</AuthGuard> } />
|
||||
<Route exact path="/:tenantCode/manage/groups" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageGroups />
|
||||
</AuthGuard> } />
|
||||
|
||||
<Route exact path="/:tenantCode/manage/branches" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageBranches />
|
||||
</AuthGuard> } />
|
||||
|
||||
<Route exact path="/:tenantCode/companydetails"
|
||||
element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<CompanyInfoUpdate />
|
||||
</AuthGuard>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/:tenantCode/manage/services" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageServices />
|
||||
</AuthGuard> }
|
||||
/>
|
||||
<Route exact path="/:tenantCode/manage/users" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageUsers />
|
||||
</AuthGuard>
|
||||
} />
|
||||
<Route exact path="/login" element={ <LoginScreen /> } />
|
||||
<Route exact path="/:tenantCode/manage/admins" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN] }>
|
||||
<ManageAdmins />
|
||||
</AuthGuard> } />
|
||||
<Route exact path="/profile" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<AdminProfile />
|
||||
</AuthGuard> } />
|
||||
<Route exact path="/" element={ <LoginScreen /> } />
|
||||
<Route exact path="/loginauth"
|
||||
element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<LoginAuth />
|
||||
</AuthGuard>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/:tenantCode/queues" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ViewQueues />
|
||||
</AuthGuard> } />
|
||||
|
||||
<Route exact path="/:tenantCode/home" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<HomePage></HomePage>
|
||||
<CanAccess roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<>
|
||||
{ user && (
|
||||
<>
|
||||
<HomePageCard
|
||||
title="Manage displays"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/displays` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage groups"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/groups` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage branches"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/branches` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage services"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/services` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage teller stations"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/stations` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage company details"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/companydetails` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="View queues"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/queues` }
|
||||
/>
|
||||
</>
|
||||
) }
|
||||
</>
|
||||
</CanAccess>
|
||||
|
||||
<CanAccess roles={ [ROLES.ROLE_SUPER_ADMIN] }>
|
||||
<>
|
||||
{ user && (
|
||||
<>
|
||||
<HomePageCard title="Manage administrators"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/admins` }></HomePageCard>
|
||||
<HomePageCard
|
||||
title="Manage users"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/users` }
|
||||
/>
|
||||
</>
|
||||
) }
|
||||
</>
|
||||
</CanAccess>
|
||||
|
||||
<CanAccess roles={ [ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
{ user && (
|
||||
<HomePageCard
|
||||
title="Manage users"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/users` }
|
||||
/>
|
||||
) }
|
||||
</CanAccess>
|
||||
</AuthGuard>
|
||||
} />
|
||||
<Route path="*" element={ <NotFound /> } />
|
||||
</Routes>
|
||||
</UserContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function AuthGuard({ children, roles }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const storedUserData = localStorage.getItem('userData');
|
||||
const user = storedUserData ? JSON.parse(storedUserData) : null;
|
||||
|
||||
if (!user) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasNecessaryRole = roles.some(role => user.roles.find(userRole => userRole === role));
|
||||
|
||||
if (!hasNecessaryRole) {
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ children }
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
|
||||
export default function CanAccess( {children, roles} ){
|
||||
|
||||
const {user, setUser} = useContext(UserContext);
|
||||
const hasRole = user && user.roles && user.roles.some(userRole => roles.find(role => role === userRole));
|
||||
return hasRole ? children : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
header.main-header {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin: 0 0 0 -2%;
|
||||
background-color: #d3e2f8;
|
||||
width: 101vw;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
.header-logo-png {
|
||||
width: 200px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.header-logout-btn {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
justify-content: center; /* Center content horizontally */
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
box-shadow: 3px 8px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
width: 130px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.header-logout-btn:hover {
|
||||
background-color: var(--light-blue);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.header-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
margin-right: 2%;
|
||||
margin-bottom: 0.5%;
|
||||
|
||||
}
|
||||
|
||||
.header-profile-png {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useContext } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { lastPathPart } from '../../utils/StringUtils.js';
|
||||
import profileImage from '../../../assets/profile-user.png'
|
||||
|
||||
import './Header.css';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate();
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('userData');
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
const path = window.location.pathname;
|
||||
const showBackButton = !!user
|
||||
&& '/' !== path
|
||||
&& '/login' !== path
|
||||
&& 'home' !== lastPathPart(window.location.href);
|
||||
|
||||
const goHome = () => {
|
||||
if (user?.tenantCode) {
|
||||
navigate(`/${user.tenantCode}/home`);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="main-header">
|
||||
<h2 className="header-logo" onClick={ goHome }>BBQMS</h2>
|
||||
|
||||
<div className="header-logout">
|
||||
{ !!user && (
|
||||
<button className="header-logout-btn" onClick={ handleLogout }>
|
||||
Logout
|
||||
</button>
|
||||
) }
|
||||
</div>
|
||||
|
||||
<div className="header-profile" onClick={ () => navigate('/profile') }>
|
||||
<img src={ profileImage } className="header-profile-png" alt="Profile image" />
|
||||
</div>
|
||||
</header>
|
||||
{ showBackButton && (
|
||||
<Button variant="secondary"
|
||||
className="mt-2 px-4"
|
||||
onClick={ goHome }>
|
||||
Back
|
||||
</Button>
|
||||
) }
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
.button-hp{
|
||||
height: 80%;
|
||||
display: inline-block;
|
||||
margin-top: 50%;
|
||||
box-shadow: 3px 6px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-hp{
|
||||
width: 14%;
|
||||
border-radius: 10px;
|
||||
float: left;
|
||||
margin-right: 4%;
|
||||
margin-left: 6%;
|
||||
margin-top: 6%;
|
||||
}
|
||||
.card-title-hp{
|
||||
text-align: center;
|
||||
color: #334257;
|
||||
height: 10%;
|
||||
max-height: 10%;
|
||||
}
|
||||
.card-text-hp{
|
||||
text-align: center;
|
||||
color: #334257;
|
||||
}
|
||||
.button-container-hp{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import './HomePageCard.css'
|
||||
|
||||
export default function HomePageCard( {title, backgroundColor, buttonColor, url} ) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div>
|
||||
<div className="card card-hp" style={{border: '1px solid #334257', backgroundColor: backgroundColor, height: '200px'}}>
|
||||
<div className="card-body">
|
||||
<h5 className="card-title card-title-hp" style={{color: 'white'}}>{title}</h5>
|
||||
<p className="card-text card-text-hp" style={{color: 'white'}}></p>
|
||||
<div className="button-container-hp">
|
||||
<button className="btn btn-primary button-hp"
|
||||
style={{backgroundColor: buttonColor}} onClick ={ () => navigate(url) } >Open</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import './HomePageCard.css'
|
||||
|
||||
export default function HomePageCardLight() {
|
||||
return (
|
||||
<div>
|
||||
<div className="card card-hp" style={{border: '1px solid #334257', backgroundColor: '#548CA8'}}>
|
||||
<div className="card-body">
|
||||
<h5 className="card-title card-title-hp" style={{color: 'white'}}>Card title</h5>
|
||||
<p className="card-text card-text-hp" style={{color: 'white'}}>Some quick example text to build on
|
||||
the card title and make up the bulk of
|
||||
the card's content.</p>
|
||||
<div className="button-container-hp">
|
||||
<a href="#" className="btn btn-primary button-hp"
|
||||
style={{backgroundColor: '#476072', border: '1px solid #548CA8'}}>Go somewhere</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#auth-container *{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#auth-container{
|
||||
font-family: "Poppins", sans-serif;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#authForm {
|
||||
border: 1px solid black;
|
||||
padding: 40px;
|
||||
width: 1000px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputs > * {
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.button-auth {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 24px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.button-auth:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import React, { useEffect, useRef, useReducer, useContext } from "react";
|
||||
import "./LoginAuth.css";
|
||||
import { SERVER_URL } from "../../constants.js";
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function doSubmit(submittedValues) {
|
||||
console.log(`Submitted: ${submittedValues.join("")}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function clampIndex(index) {
|
||||
if (index > 6) {
|
||||
return 6;
|
||||
} else if (index < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "INPUT":
|
||||
return {
|
||||
...state,
|
||||
inputValues: [
|
||||
...state.inputValues.slice(0, action.payload.index),
|
||||
action.payload.value,
|
||||
...state.inputValues.slice(action.payload.index + 1)
|
||||
],
|
||||
focusedIndex: clampIndex(state.focusedIndex + 1)
|
||||
};
|
||||
|
||||
case "BACK":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: clampIndex(state.focusedIndex - 1)
|
||||
};
|
||||
|
||||
case "PASTE":
|
||||
return {
|
||||
...state,
|
||||
inputValues: state.inputValues.map(
|
||||
(_, index) => action.payload.pastedValue[index] || ""
|
||||
)
|
||||
};
|
||||
|
||||
case "FOCUS":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: action.payload.focusedIndex
|
||||
};
|
||||
|
||||
case "VERIFY":
|
||||
return {
|
||||
...state,
|
||||
status: "pending"
|
||||
};
|
||||
|
||||
case "VERIFY_SUCCESS":
|
||||
return {
|
||||
...state,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
case "RESET_INPUTS":
|
||||
return {
|
||||
...state,
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error("unknown action");
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
export default function LoginAuth() {
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer(
|
||||
reducer,
|
||||
initialState
|
||||
);
|
||||
|
||||
function handleInput(index, value) {
|
||||
dispatch({ type: "INPUT", payload: { index, value } });
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
dispatch({ type: "BACK" });
|
||||
}
|
||||
|
||||
function handlePaste(pastedValue) {
|
||||
dispatch({ type: "PASTE", payload: { pastedValue } });
|
||||
|
||||
if (pastedValue.length === 6) {
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(pastedValue.split("")).then(() =>
|
||||
dispatch({ type: "VERIFY_SUCCESS" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(focusedIndex) {
|
||||
dispatch({ type: "FOCUS", payload: { focusedIndex } });
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
|
||||
//e.preventDefault();
|
||||
|
||||
dispatch({ type: "VERIFY" });
|
||||
|
||||
try {
|
||||
const storedUserData = localStorage.getItem('userData');
|
||||
const userData = storedUserData ? JSON.parse(storedUserData) : null;
|
||||
if (!userData) {
|
||||
throw new Error("Email not found in localStorage");
|
||||
}
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/auth/tfa`;
|
||||
const { data, success } = await fetchData(url, 'POST', {
|
||||
code: inputValues.join(""),
|
||||
email: userData.email
|
||||
});
|
||||
|
||||
if (success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
setUser(data.userData);
|
||||
navigate(`/${ data.userData.tenantCode }/home`);
|
||||
} else {
|
||||
throw new Error("Code could not be verified. It is incorrect or expired.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Code could not be verified. It is incorrect or expired.");
|
||||
resetInputs();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function resetInputs() {
|
||||
dispatch({ type: "RESET_INPUTS" });
|
||||
}
|
||||
let navigate = useNavigate();
|
||||
function routeChange() {
|
||||
handleSubmit({});
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="auth-container">
|
||||
<form id="authForm" onSubmit={handleSubmit}>
|
||||
<div className="inputs">
|
||||
{inputValues.map((value, index) => {
|
||||
return (
|
||||
<Input
|
||||
key={index}
|
||||
index={index}
|
||||
value={value}
|
||||
onChange={handleInput}
|
||||
onBackspace={handleBack}
|
||||
onPaste={handlePaste}
|
||||
isFocused={index === focusedIndex}
|
||||
onFocus={handleFocus}
|
||||
isDisabled={status === "pending"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button className="button-auth" disabled={status === "pending"} onClick={routeChange}>
|
||||
{status === "pending" ? "VERIFYING..." : "VERIFY"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Input({
|
||||
index,
|
||||
value,
|
||||
onChange,
|
||||
onPaste,
|
||||
onBackspace,
|
||||
isFocused,
|
||||
onFocus,
|
||||
isDisabled
|
||||
}) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (ref.current !== document.activeElement && isFocused) {
|
||||
ref.current.focus();
|
||||
}
|
||||
});
|
||||
}, [isFocused]);
|
||||
|
||||
function handleChange(e) {
|
||||
onChange(index, e.target.value);
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
onPaste(e.clipboardData.getData("text"));
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === "Backspace") {
|
||||
onBackspace();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(e) {
|
||||
e.target.setSelectionRange(0, 1);
|
||||
onFocus(index);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength="1"
|
||||
onFocus={handleFocus}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#login-form {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="text"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
#registration{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { GoogleLogin } from '@react-oauth/google';
|
||||
import React, { useState } from "react";
|
||||
import validator from "validator";
|
||||
import "./LoginForm.css";
|
||||
import LoginAuth from "../LoginAuth/LoginAuth";
|
||||
import { Route, Routes, useNavigate, Link } from "react-router-dom";
|
||||
import { SERVER_URL } from "../../constants.js";
|
||||
|
||||
const LoginForm = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function handleGoogleLogin(credentialResponse) {
|
||||
const response = await fetch(`${SERVER_URL}/api/v1/auth/login/oauth2/google`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
googleToken: credentialResponse.credential
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
setError("Error while trying to log in with Google.");
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
localStorage.setItem('userData', JSON.stringify(data));
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
console.log("username: " + username);
|
||||
console.log("password: " + password);
|
||||
|
||||
if (!username.trim()) {
|
||||
setError("Username is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.isEmail(username) && !validator.isMobilePhone(username, "any")) {
|
||||
setError("Invalid username format. Please enter a valid email or phone number.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
setError("Password is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${SERVER_URL}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: username,
|
||||
password: password
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
localStorage.setItem('userData', JSON.stringify(data));
|
||||
|
||||
if (response.ok) {
|
||||
setIsSubmitted(true);
|
||||
navigate('/');
|
||||
} else if (response.status === 403) {
|
||||
setError("Your credentials are incorrect.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setError("An error occurred. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsernameChange = (event) => {
|
||||
setUsername(event.target.value);
|
||||
setError("");
|
||||
};
|
||||
|
||||
const handlePasswordChange = (event) => {
|
||||
setPassword(event.target.value);
|
||||
setError("");
|
||||
};
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='/' element={<LoginAuth />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="login-form">
|
||||
<h1>LOGIN</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Email or phone number:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={username}
|
||||
onChange={handleUsernameChange}
|
||||
/>
|
||||
{error && (error.includes("Username") || error.includes("Invalid")) && <p className="error">{error}</p>}
|
||||
{error && (error.includes("credentials")) && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
{error && error.includes("Password") && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<input type="submit" value="Submit" />
|
||||
<p id="registration">
|
||||
Not registered? <Link to="/registration">Create an account</Link>
|
||||
</p>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
@@ -0,0 +1,66 @@
|
||||
.auth-container-reg *{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.auth-container-reg{
|
||||
font-family: "Poppins", sans-serif;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.authForm-reg {
|
||||
border: 1px solid black;
|
||||
padding: 0px 40px 40px 40px;
|
||||
width: 1000px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.QRCodeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.QRCodeContainer h1 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.inputs-reg {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputs-reg > * {
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 24px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { useEffect, useRef, useReducer, useState } from "react";
|
||||
import "./RegistrationAuth.css";
|
||||
import { SERVER_URL } from "../../constants";
|
||||
|
||||
function doSubmit(submittedValues) {
|
||||
console.log(`Submitted: ${submittedValues.join("")}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function clampIndex(index) {
|
||||
if (index > 6) {
|
||||
return 6;
|
||||
} else if (index < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "INPUT":
|
||||
return {
|
||||
...state,
|
||||
inputValues: [
|
||||
...state.inputValues.slice(0, action.payload.index),
|
||||
action.payload.value,
|
||||
...state.inputValues.slice(action.payload.index + 1)
|
||||
],
|
||||
focusedIndex: clampIndex(state.focusedIndex + 1)
|
||||
};
|
||||
|
||||
case "BACK":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: clampIndex(state.focusedIndex - 1)
|
||||
};
|
||||
|
||||
case "PASTE":
|
||||
return {
|
||||
...state,
|
||||
inputValues: state.inputValues.map(
|
||||
(_, index) => action.payload.pastedValue[index] || ""
|
||||
)
|
||||
};
|
||||
|
||||
case "FOCUS":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: action.payload.focusedIndex
|
||||
};
|
||||
|
||||
case "VERIFY":
|
||||
return {
|
||||
...state,
|
||||
status: "pending"
|
||||
};
|
||||
|
||||
case "VERIFY_SUCCESS":
|
||||
return {
|
||||
...state,
|
||||
status: "idle"
|
||||
};
|
||||
case "RESET_INPUTS":
|
||||
return {
|
||||
...state,
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
default:
|
||||
throw new Error("unknown action");
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
export default function RefistrationAuth( {qrCode, email} ) {
|
||||
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer(
|
||||
reducer,
|
||||
initialState
|
||||
);
|
||||
|
||||
function handleInput(index, value) {
|
||||
dispatch({ type: "INPUT", payload: { index, value } });
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
dispatch({ type: "BACK" });
|
||||
}
|
||||
|
||||
function handlePaste(pastedValue) {
|
||||
dispatch({ type: "PASTE", payload: { pastedValue } });
|
||||
|
||||
if (pastedValue.length === 6) {
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(pastedValue.split("")).then(() =>
|
||||
dispatch({ type: "VERIFY_SUCCESS" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(focusedIndex) {
|
||||
dispatch({ type: "FOCUS", payload: { focusedIndex } });
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(inputValues).then(() => dispatch({ type: "VERIFY_SUCCESS" }));
|
||||
}
|
||||
|
||||
const checkInputValues = async () =>{
|
||||
const enteredValues = inputValues.join("");
|
||||
const requestBody = {
|
||||
code: enteredValues,
|
||||
email: email
|
||||
}
|
||||
try{
|
||||
const response = await fetch(SERVER_URL + '/api/v1/auth/tfa', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
if (response.ok) {
|
||||
dispatch({ type: "VERIFY_SUCCESS" });
|
||||
} else {
|
||||
throw new Error("Code could not be verified. It is incorrect or expired.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Code could not be verified. It is incorrect.");
|
||||
resetInputs();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function resetInputs() {
|
||||
dispatch({ type: "RESET_INPUTS" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth-container-reg">
|
||||
<form className="authForm-reg" onSubmit={handleSubmit}>
|
||||
<div className="QRCodeContainer">
|
||||
<h1>Scan QR code</h1>
|
||||
<img src={qrCode} height='300px'></img>
|
||||
</div>
|
||||
<div className="inputs-reg">
|
||||
{inputValues.map((value, index) => {
|
||||
return (
|
||||
<Input
|
||||
key={index}
|
||||
index={index}
|
||||
value={value}
|
||||
onChange={handleInput}
|
||||
onBackspace={handleBack}
|
||||
onPaste={handlePaste}
|
||||
isFocused={index === focusedIndex}
|
||||
onFocus={handleFocus}
|
||||
isDisabled={status === "pending"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button onClick={checkInputValues} disabled={status === "pending"}>
|
||||
{status === "pending" ? "VERIFYING..." : "VERIFY"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Input({
|
||||
index,
|
||||
value,
|
||||
onChange,
|
||||
onPaste,
|
||||
onBackspace,
|
||||
isFocused,
|
||||
onFocus,
|
||||
isDisabled
|
||||
}) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (ref.current !== document.activeElement && isFocused) {
|
||||
ref.current.focus();
|
||||
}
|
||||
});
|
||||
}, [isFocused]);
|
||||
|
||||
function handleChange(e) {
|
||||
onChange(index, e.target.value);
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
onPaste(e.clipboardData.getData("text"));
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === "Backspace") {
|
||||
onBackspace();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(e) {
|
||||
e.target.setSelectionRange(0, 1);
|
||||
onFocus(index);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength="1"
|
||||
onFocus={handleFocus}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
.login-form-reg {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-form-reg form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="text"],
|
||||
.login-form-reg form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.login{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-reg{
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Route, Routes, useNavigate, Link } from "react-router-dom";
|
||||
import { useFormik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
import "./RegistrationForm.css";
|
||||
import RegistrationAuth from '../RegistrationAuth/RegistrationAuth';
|
||||
import { SERVER_URL } from '../../constants';
|
||||
|
||||
const userSchema = yup.object().shape({
|
||||
email: yup.string().email("Please enter a valid email").required("Email is required"),
|
||||
password: yup.string().min(4).max(10).required("Password is required")
|
||||
});
|
||||
|
||||
export default function RegistrationForm() {
|
||||
const navigate = useNavigate();
|
||||
const [nextPage, setNextPage] = useState(false);
|
||||
const [qrCode, setQrCode] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
validationSchema: userSchema,
|
||||
onSubmit: async (values, { setFieldError }) => {
|
||||
try {
|
||||
const isValid = await userSchema.isValid(values);
|
||||
if (isValid) {
|
||||
const response = await fetch(SERVER_URL + '/api/v1/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
phone_number: "00000"
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const body = await response.json();
|
||||
const responseBody = await fetch(SERVER_URL + `/api/v1/auth/tfa?email=${body.email}`);
|
||||
const responseData = await responseBody.json();
|
||||
setQrCode(responseData.qrCode);
|
||||
setEmail(body.email);
|
||||
setNextPage(true);
|
||||
} else if (response.status === 400) {
|
||||
setFieldError('email', 'Email already in use');
|
||||
} else {
|
||||
console.error('Failed to register:', response.statusText);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (nextPage) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='*' element={<RegistrationAuth qrCode={qrCode} email={email} />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-form-reg">
|
||||
<h1>REGISTER ACCOUNT</h1>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<label htmlFor="email">Email or phone number:</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="text"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.email}
|
||||
/>
|
||||
{formik.touched.email && formik.errors.email ? (
|
||||
<p className="error-reg">{formik.errors.email}</p>
|
||||
) : null}
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.password}
|
||||
/>
|
||||
{formik.touched.password && formik.errors.password ? (
|
||||
<p className="error-reg">{formik.errors.password}</p>
|
||||
) : null}
|
||||
<input type="submit" value="Create new account"/>
|
||||
<p className="login">
|
||||
Already have an account? <Link to="/login">Login</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useSortContext } from '../../context/SortContext.jsx';
|
||||
import { useSortSearchState } from '../../hooks/sortParamsHooks.jsx';
|
||||
|
||||
export function SortableHeader({ columnName, children }) {
|
||||
const { sort, updateSort } = useSortSearchState(columnName);
|
||||
const sortContext = useSortContext();
|
||||
|
||||
function handleSort() {
|
||||
const newSort = updateSort();
|
||||
sortContext.onSort(newSort);
|
||||
}
|
||||
|
||||
return (
|
||||
<th style={{ cursor: 'pointer' }}
|
||||
onClick={ handleSort }>
|
||||
{ children }
|
||||
</th>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useState } from 'react';
|
||||
import { createClassName } from '../../utils/StringUtils.js'
|
||||
|
||||
export function TimePicker({ title, onChange, className, defaultHour, defaultMinute }) {
|
||||
const [ hours, setHours ] = useState(defaultHour ?? '');
|
||||
const [ minutes, setMinutes ] = useState(defaultMinute ?? '');
|
||||
|
||||
function handleChange(hours, minutes) {
|
||||
onChange({
|
||||
hour: hours && hours !== '' ? parseInt(hours) : defaultHour ?? 0,
|
||||
minutes: minutes && minutes !== '' ? parseInt(minutes) : defaultMinute ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
function handleHourChange(value) {
|
||||
setHours(value);
|
||||
handleChange(value, minutes);
|
||||
}
|
||||
|
||||
function handleMinuteChange(value) {
|
||||
setMinutes(value);
|
||||
handleChange(hours, value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ createClassName([ 'd-inline-flex flex-column gap-1', className ]) }>
|
||||
<div>{ title }</div>
|
||||
<div className="d-inline-flex gap-2 align-items-center">
|
||||
<TimePart value={ hours }
|
||||
placeholder="HH"
|
||||
min={ 0 }
|
||||
max={ 23 }
|
||||
onChange={ handleHourChange } />
|
||||
:
|
||||
<TimePart value={ minutes }
|
||||
placeholder="MM"
|
||||
min={ 0 }
|
||||
max={ 59 }
|
||||
onChange={ handleMinuteChange } />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TimePart({ value, placeholder, onChange, min, max }) {
|
||||
function handleChange(newValueString) {
|
||||
if (newValueString !== '') {
|
||||
if (isNaN(newValueString)) {
|
||||
onChange('');
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = parseInt(newValueString);
|
||||
|
||||
if (newValue <= max && newValue >= min) {
|
||||
onChange(newValue)
|
||||
}
|
||||
} else {
|
||||
onChange('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<input className="border py-1 px-2 text-center rounded-1"
|
||||
value={ value && value !== '' && parseInt(value) }
|
||||
placeholder={ placeholder }
|
||||
onChange={ e => handleChange(e.target.value) }
|
||||
size={ 3 }
|
||||
min={ min }
|
||||
max={ max } />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const SERVER_URL = 'http://localhost:8080';
|
||||
|
||||
export const ROLES = {
|
||||
ROLE_SUPER_ADMIN : "ROLE_SUPER_ADMIN",
|
||||
ROLE_BRANCH_ADMIN : "ROLE_BRANCH_ADMIN"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const SortContext = createContext({ onSort: () => {} });
|
||||
|
||||
export function useSortContext() {
|
||||
return useContext(SortContext);
|
||||
}
|
||||
|
||||
export function SortContextProvider({ onSort, children }) {
|
||||
return (
|
||||
<SortContext.Provider value={{ onSort }}>
|
||||
{ children }
|
||||
</SortContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const UserContext = createContext();
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Koristiti ovu funkciju za fetchanje u buducnosti kad god je to moguce.
|
||||
*/
|
||||
export async function fetchData(url, method, body) {
|
||||
const headers = new Headers();
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers.append('Authorization', `Bearer ${ token }`);
|
||||
}
|
||||
|
||||
headers.append('Content-Type', 'application/json');
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method || 'GET',
|
||||
headers: headers,
|
||||
body: body ? JSON.stringify(body) : null
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const data = res.ok && res.body ? await res.json() : null;
|
||||
|
||||
if (res.ok) {
|
||||
//na svaki ispravan rezultat treba da dobijemo novi token da refreshamo stari
|
||||
const newToken = res.headers.get('Auth-Token');
|
||||
if (newToken) {
|
||||
localStorage.setItem('token', newToken);
|
||||
}
|
||||
}
|
||||
|
||||
return { data: data, success: res.ok };
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* A hook for using the native JS interval API. The interval is released after the user component dismounts.
|
||||
* @param callback function to be executed
|
||||
* @param period interval between executions of the callback function. The first execution happens instantly.
|
||||
* @param deps {array} optional array of dependencies
|
||||
*/
|
||||
export function useInterval(callback, period, deps) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => callback(), period)
|
||||
return () => clearInterval(interval);
|
||||
}, [ period, ...deps ]);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export function useSortSearchState(column) {
|
||||
const [ search, setSearch ] = useSearchParams();
|
||||
|
||||
function serializeToUrl(column, direction) {
|
||||
return `${column},${direction}`;
|
||||
}
|
||||
|
||||
function deserializeFromUrl(sortQueryParam) {
|
||||
if (sortQueryParam) {
|
||||
const sortParts = sortQueryParam.split(',')
|
||||
return {
|
||||
column: sortParts[0],
|
||||
direction: sortParts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function searchContainsColumn(column) {
|
||||
const sortParam = getCurrentSort();
|
||||
|
||||
if (sortParam) {
|
||||
return deserializeFromUrl(sortParam).column === column;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function changeSortDirection() {
|
||||
const sortParam = getCurrentSort();
|
||||
|
||||
if (sortParam) {
|
||||
const { column, direction } = deserializeFromUrl(sortParam);
|
||||
|
||||
if (direction === 'asc') {
|
||||
setSortDirection(column, 'desc');
|
||||
} else if (direction === 'desc') {
|
||||
removeSort()
|
||||
} else {
|
||||
setSortDirection(column, 'asc');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSortDirection(column, direction = 'asc') {
|
||||
search.set('sort', serializeToUrl(column, direction));
|
||||
setSearch(prev => search);
|
||||
}
|
||||
|
||||
function getCurrentSort() {
|
||||
return search.get('sort');
|
||||
}
|
||||
|
||||
function removeSort() {
|
||||
search.delete('sort');
|
||||
setSearch(prev => search)
|
||||
}
|
||||
|
||||
function updateSort(column) {
|
||||
if (searchContainsColumn(column)) {
|
||||
changeSortDirection();
|
||||
} else {
|
||||
setSortDirection(column, 'asc');
|
||||
}
|
||||
return getCurrentSort();
|
||||
}
|
||||
|
||||
return { sort: getCurrentSort(), updateSort: () => updateSort(column) };
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'General Sans', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0 20px;
|
||||
background-color: ghostwhite;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* ovdje definisite konstante boje i sl. koje cete koristiti na vise mjesta */
|
||||
--blue: #334257;
|
||||
--light-blue: #548CA8;
|
||||
--dark-blue: #476072;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { GoogleOAuthProvider } from "@react-oauth/google";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "./App";
|
||||
//import ManageServices from "./pages/ManageServices/ManageServices";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<GoogleOAuthProvider clientId="776973117081-smp0drfulvkjk8s55ifr2i7k3uklpr04.apps.googleusercontent.com">
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</GoogleOAuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,232 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Modal, Form } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const styles = {
|
||||
primaryButton: {
|
||||
backgroundColor: "var(--light-blue)",
|
||||
borderColor: "var(--light-blue)",
|
||||
},
|
||||
infoButton: {
|
||||
backgroundColor: "var(--light-blue)",
|
||||
color: 'white',
|
||||
borderColor: "var(--light-blue)",
|
||||
},
|
||||
modalHeader: {
|
||||
backgroundColor: "var(--blue)",
|
||||
color: 'white',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const AdminManageScreen = () => {
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [admins, setAdmins] = useState([]);
|
||||
const [adminEmail, setAdminEmail] = useState('');
|
||||
const [adminPassword, setAdminPassword] = useState('');
|
||||
const [selectedAdminIndex, setSelectedAdminIndex] = useState(null);
|
||||
const [token, setToken] = useState('');
|
||||
const [emailError, setEmailError] = useState('');
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (storedToken) {
|
||||
setToken(storedToken);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchAdmins();
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const fetchAdmins = async () => {
|
||||
try {
|
||||
const requestBody = JSON.stringify({
|
||||
roleName: 'ROLE_BRANCH_ADMIN'
|
||||
});
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAdmins(data);
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const validateEmail = (email) => {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
};
|
||||
|
||||
const handleAddAdmin = async () => {
|
||||
const requestBody = {
|
||||
email: adminEmail,
|
||||
password: adminPassword,
|
||||
roleName: 'ROLE_BRANCH_ADMIN'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAdmins([...admins, data]);
|
||||
setShowModal(false);
|
||||
setAdminEmail('');
|
||||
setAdminPassword('');
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAdmin = async () => {
|
||||
if (!validateEmail(adminEmail)) {
|
||||
setEmailError('Invalid email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setEmailError('');
|
||||
|
||||
try {
|
||||
const updatedAdmin = {
|
||||
email: adminEmail
|
||||
};
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${admins[selectedAdminIndex].id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: JSON.stringify(updatedAdmin),
|
||||
});
|
||||
if (response.ok) {
|
||||
const updatedAdmins = [...admins];
|
||||
updatedAdmins[selectedAdminIndex].email = adminEmail;
|
||||
setAdmins(updatedAdmins);
|
||||
setShowModal(false);
|
||||
setAdminEmail('');
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAdmin = async (userId) => {
|
||||
try {
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const updatedAdmins = admins.filter(admin => admin.id !== userId);
|
||||
setAdmins(updatedAdmins);
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = (index) => {
|
||||
const admin = admins[index];
|
||||
setSelectedAdminIndex(index);
|
||||
setAdminEmail(admin.email);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<h2>Manage Administrators</h2>
|
||||
<Button variant="primary" style={styles.primaryButton} className="mb-3" onClick={() => { setShowModal(true); setSelectedAdminIndex(null); }}>Add Admin</Button>
|
||||
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{admins.map((admin, index) => (
|
||||
<tr key={index}>
|
||||
<td>{admin.id}</td>
|
||||
<td>{admin.email}</td>
|
||||
<td>
|
||||
<Button variant="info" style={styles.infoButton} onClick={() => handleEditClick(index)}>Edit</Button>{' '}
|
||||
<Button variant="danger" onClick={() => handleDeleteAdmin(admin.id)}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Modal show={showModal} onHide={() => { setShowModal(false); setSelectedAdminIndex(null); }}>
|
||||
<Modal.Header closeButton style={styles.modalHeader}>
|
||||
<Modal.Title>{selectedAdminIndex !== null ? 'EDIT ADMIN' : 'ADD ADMIN'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group controlId="formAdminEmail" className="mb-3">
|
||||
<Form.Label>Email</Form.Label>
|
||||
<Form.Control type="email" placeholder="Enter admin email" value={adminEmail} onChange={(e) => setAdminEmail(e.target.value)} />
|
||||
{emailError && <div style={{ color: 'red' }}>{emailError}</div>}
|
||||
</Form.Group>
|
||||
{selectedAdminIndex === null && (
|
||||
<Form.Group controlId="formAdminPassword" className="mb-3">
|
||||
<Form.Label>Password</Form.Label>
|
||||
<Form.Control type="password" placeholder="Enter admin password" value={adminPassword} onChange={(e) => setAdminPassword(e.target.value)} />
|
||||
{passwordError && <div style={{ color: 'red' }}>{passwordError}</div>}
|
||||
</Form.Group>
|
||||
)}
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => { setShowModal(false); setSelectedAdminIndex(null); }}>Close</Button>
|
||||
{selectedAdminIndex !== null ?
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleEditAdmin}>Save Changes</Button> :
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleAddAdmin}>Add Admin</Button>
|
||||
}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminManageScreen;
|
||||
@@ -0,0 +1,45 @@
|
||||
#account-settings{
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#check-box{
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
#check-box, label{
|
||||
margin-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
background-color: #548CA8;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#QR-code input[type="submit"][disabled] {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#QR-code img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { SERVER_URL } from '../../constants';
|
||||
import "./AdminProfile.css"
|
||||
|
||||
export default function AdminProfile(){
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isQRCodeEnabled, setIsQRCodeEnabled] = useState(false);
|
||||
const [qrCodeSrc, setQrCodeSrc] = useState('');
|
||||
const [userData, setUserData] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const storedUserData = localStorage.getItem('userData');
|
||||
setUserData(JSON.parse(storedUserData));
|
||||
|
||||
const storedIsTfa = localStorage.getItem('isTfa');
|
||||
let isTfa = JSON.parse(storedIsTfa);
|
||||
|
||||
setIsChecked(isTfa);
|
||||
setIsQRCodeEnabled(isTfa);
|
||||
}, []);
|
||||
|
||||
const handleSaveChanges = async () =>{
|
||||
const url = `${ SERVER_URL }/api/v1/auth/tfa`;
|
||||
const { data, success } = await fetchData(url, 'PUT', {
|
||||
isTfa: isChecked
|
||||
});
|
||||
localStorage.setItem('isTfa', isChecked);
|
||||
setIsQRCodeEnabled(isChecked);
|
||||
if(!isChecked){
|
||||
setQrCodeSrc('');
|
||||
}
|
||||
if(success){
|
||||
let message = 'Success: Your changes have been successfully submitted.';
|
||||
if(isChecked){
|
||||
message = message + '\nPlease scan QR code.';
|
||||
}
|
||||
alert(message);
|
||||
}else{
|
||||
alert('An error occurred. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheckBoxChange = () =>{
|
||||
setIsChecked(!isChecked);
|
||||
}
|
||||
|
||||
const handleGenerateQRCode = () =>{
|
||||
if(isQRCodeEnabled){
|
||||
const url = `${ SERVER_URL }/api/v1/auth/tfa?email=${userData.email}`;
|
||||
fetchData(url, 'GET')
|
||||
.then(({ data, success }) => {
|
||||
if (success) {
|
||||
setQrCodeSrc(data.qrCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="account-settings">
|
||||
<h1>Account settings</h1>
|
||||
<div id="check-box">
|
||||
<form>
|
||||
<label htmlFor="2fa">Use two-factor authentication:</label>
|
||||
<input type="checkbox" id="2fa" name="2fa" checked={isChecked} onChange={handleCheckBoxChange}></input>
|
||||
</form>
|
||||
</div>
|
||||
<div id="QR-code">
|
||||
<input type="submit" value="Generate QR code" disabled={!isQRCodeEnabled} onClick={handleGenerateQRCode}/>
|
||||
<img src={qrCodeSrc}></img>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Save changes" onClick={handleSaveChanges}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
.company-info-update-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.heading2 {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.form-container-comp {
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
background-color: #ffff;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.form-group-comp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.labels-comp {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #666362;
|
||||
font-family: "Poppins", sans-serif;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inputs-comp[type="text"],
|
||||
.inputs-comp[type="file"],
|
||||
textarea,
|
||||
select {
|
||||
width: 80%;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
text-indent: 5px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.inputs-comp[type="file"] {
|
||||
width: 80%;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0.03%;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin: 10px auto 0;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.left-column .labels-comp {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.right-column .form-group-comp {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.right-column .labels-comp {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
?????
|
||||
*/
|
||||
.right-column .inputs-comp,
|
||||
.right-column textarea,
|
||||
.right-column select {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background-color: #476072;
|
||||
/
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
width: 40%;
|
||||
height: 100px;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
border: solid 1px black;
|
||||
}
|
||||
|
||||
.welcome-message > p {
|
||||
padding: 4px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.company-info-submit-btn {
|
||||
display: flex;
|
||||
margin: 10px auto;
|
||||
background-color: var(--blue);
|
||||
padding: 10px 40px;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import './CompanyInfoUpdate.css';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
|
||||
export default function CompanyInfoUpdate() {
|
||||
const navigate = useNavigate();
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [hqAddress, setHQAddress] = useState('');
|
||||
const [welcomeMessage, setWelcomeMessage] = useState('');
|
||||
const [font, setFont] = useState('Arial');
|
||||
const [file, setFile] = useState('');
|
||||
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
useEffect(() => {
|
||||
// Na pocetku popunimo polja sa vec postojecim podacima
|
||||
const url = `${ SERVER_URL }/api/v1/tenants/${ tenantCode }`;
|
||||
|
||||
fetchData(url, 'GET')
|
||||
.then(({ data, success }) => {
|
||||
if (success) {
|
||||
setName(data.name);
|
||||
setHQAddress(data.hqAddress);
|
||||
setWelcomeMessage(data.welcomeMessage);
|
||||
setFont(data.font);
|
||||
setFile(data.logo.base64Logo);
|
||||
}
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
function handleChange(e) {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
setFile(reader.result);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
try {
|
||||
const url = `${ SERVER_URL }/api/v1/tenants/${ tenantCode }`;
|
||||
const { data, success } = await fetchData(url, 'PUT', {
|
||||
name: name,
|
||||
hqAddress: hqAddress,
|
||||
font: font,
|
||||
welcomeMessage: welcomeMessage,
|
||||
logo: file
|
||||
});
|
||||
|
||||
if (success) {
|
||||
alert('Success: Your changes have been successfully submitted.');
|
||||
} else {
|
||||
alert(`Error while trying to save your changes. Please try again.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error: An error occurred while submitting your changes.');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="company-info-update-wrapper">
|
||||
<div className="heading2">
|
||||
<h2>COMPANY DETAILS</h2>
|
||||
</div>
|
||||
<div className="form-container-comp">
|
||||
<div className="left-column">
|
||||
<form>
|
||||
<div className="form-group-comp">
|
||||
<label className="labels-comp" htmlFor="name" id="Name">Name</label>
|
||||
<input className="inputs-comp"
|
||||
type="text"
|
||||
id="name"
|
||||
value={ name }
|
||||
onChange={ (e) => setName(e.target.value) }
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group-comp">
|
||||
<label className="labels-comp" htmlFor="logo" id="Logo">Logo</label>
|
||||
<input className="inputs-comp"
|
||||
type="file"
|
||||
onChange={ handleChange }
|
||||
accept="image/*"
|
||||
required
|
||||
/>
|
||||
{ file && <img className="logo" src={ file } alt="Uploaded Logo" /> }
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="right-column">
|
||||
<form>
|
||||
<div className="form-group-comp">
|
||||
<label className="labels-comp" htmlFor="hqAddress">HQ Address</label>
|
||||
<input className="inputs-comp"
|
||||
type="text"
|
||||
id="hqAddress"
|
||||
value={ hqAddress }
|
||||
onChange={ (e) => setHQAddress(e.target.value) }
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group-comp">
|
||||
<label className="labels-comp" htmlFor="welcomeMessage">Welcome Message</label>
|
||||
<input className="inputs-comp"
|
||||
type="text"
|
||||
id="welcomeMessage"
|
||||
value={ welcomeMessage }
|
||||
onChange={ (e) => setWelcomeMessage(e.target.value) }
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="welcome-message">
|
||||
<p style={ { fontFamily: font } }>{ welcomeMessage }</p>
|
||||
</div>
|
||||
|
||||
<div className="fontSelect">
|
||||
<label className="labels-comp" htmlFor="font">Font</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="font"
|
||||
value={ font }
|
||||
onChange={ (e) => setFont(e.target.value) }
|
||||
required
|
||||
>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Verdana">Verdana</option>
|
||||
<option value="Helvetica">Helvetica</option>
|
||||
<option value="Montserrat">Montserrat</option>
|
||||
<option value="Calibri">Calibri</option>
|
||||
<option value="Futura">Futura</option>
|
||||
<option value="Bodoni">Bodoni</option>
|
||||
<option value="Rockwell">Rockwell</option>
|
||||
<option value="Comic Sans MS">Comic Sans MS</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" onClick={ submitForm } className="company-info-submit-btn">Submit</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.h1-hp{
|
||||
text-align: center;
|
||||
margin-top: 1%;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
import './HomePage.css'
|
||||
export default function HomePage() {
|
||||
|
||||
return (
|
||||
<main className="background-hp">
|
||||
<h1 className="h1-hp">Dashboard</h1>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
#login-form {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="text"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#registration {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="text"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#registration {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import validator from 'validator';
|
||||
import './LoginScreen.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!username.trim()) {
|
||||
setError('Username is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.isEmail(username) && !validator.isMobilePhone(username, 'any')) {
|
||||
setError('Invalid username format. Please enter a valid email or phone number.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
setError('Password is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `${SERVER_URL}/api/v1/auth/login`;
|
||||
const { data, success } = await fetchData(url, 'POST', {
|
||||
email: username,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (success) {
|
||||
if (data.userData == undefined) {
|
||||
localStorage.setItem('userData', JSON.stringify(data));
|
||||
} else {
|
||||
localStorage.setItem('userData', JSON.stringify(data.userData));
|
||||
localStorage.setItem('token', data.token);
|
||||
setUser(data.userData);
|
||||
}
|
||||
setIsSubmitted(true);
|
||||
|
||||
if (data.token) {
|
||||
localStorage.setItem('isTfa', false);
|
||||
navigate(`/${data.userData.tenantCode}/home`);
|
||||
} else {
|
||||
localStorage.setItem('isTfa', true);
|
||||
navigate('/loginauth');
|
||||
}
|
||||
} else {
|
||||
setError('Your credentials are incorrect.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setError('An error occurred. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsernameChange = (event) => {
|
||||
setUsername(event.target.value);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handlePasswordChange = (event) => {
|
||||
setPassword(event.target.value);
|
||||
setError('');
|
||||
};
|
||||
/*
|
||||
if (isSubmitted) {
|
||||
//navigate('/loginAuth');
|
||||
navigate('/companydetails');
|
||||
}*/
|
||||
|
||||
return (
|
||||
<div id="login-form">
|
||||
<h1>LOGIN</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Email or phone number:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={username}
|
||||
onChange={handleUsernameChange}
|
||||
/>
|
||||
{error && (error.includes('Username') || error.includes('Invalid')) &&
|
||||
<p className="error">{error}</p>}
|
||||
{error && (error.includes('credentials')) && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
{error && error.includes('Password') && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<input type="submit" value="Submit" />
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,302 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Modal, Form, ListGroup } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
|
||||
const styles = {
|
||||
primaryButton: {
|
||||
backgroundColor: '#548CA8',
|
||||
borderColor: '#548CA8',
|
||||
},
|
||||
infoButton: {
|
||||
backgroundColor: '#548CA8',
|
||||
color: 'white',
|
||||
borderColor: '#548CA8',
|
||||
},
|
||||
modalHeader: {
|
||||
backgroundColor: '#334257',
|
||||
color: 'white',
|
||||
},
|
||||
};
|
||||
|
||||
const ManageBranchesScreen = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [manageBranches, setManageBranches] = useState([]);
|
||||
const [branchName, setBranchName] = useState('');
|
||||
const [selectedBranchIndex, setSelectedBranchIndex] = useState(null);
|
||||
const [tellerStations, setTellerStations] = useState([]);
|
||||
const [newStationName, setNewStationName] = useState('');
|
||||
const [deleteConfirmation, setDeleteConfirmation] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const url = `${SERVER_URL}/api/v1/branches/${tenantCode}`;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBranches = async () => {
|
||||
try {
|
||||
const response = await fetchData(url, 'GET');
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
setManageBranches(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to fetch branches.');
|
||||
}
|
||||
};
|
||||
fetchBranches();
|
||||
}, [SERVER_URL, url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showModal) {
|
||||
setBranchName('');
|
||||
setTellerStations([]);
|
||||
setSelectedBranchIndex(null);
|
||||
setErrorMessage('');
|
||||
}
|
||||
}, [showModal]);
|
||||
|
||||
const handleEditClick = (index) => {
|
||||
const branch = manageBranches[index];
|
||||
setSelectedBranchIndex(index);
|
||||
setBranchName(branch.name);
|
||||
setTellerStations(branch.tellerStations ? [...branch.tellerStations] : []);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleAddBranch = async () => {
|
||||
if (branchName.trim() === '' || tellerStations.length === 0) {
|
||||
setErrorMessage('Please fill out all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newBranch = {
|
||||
name: branchName,
|
||||
tellerStations: tellerStations.map(station => station.name)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchData(url, 'POST', newBranch);
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
setManageBranches([...manageBranches, response.data]);
|
||||
setShowModal(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to create branch.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTellerStation = () => {
|
||||
if (newStationName.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStation = { id: tellerStations.length + 1, name: newStationName };
|
||||
setTellerStations([...tellerStations, newStation]);
|
||||
setNewStationName('');
|
||||
};
|
||||
|
||||
const handleEditTellerStation = async () => {
|
||||
if (newStationName.trim() === '' || selectedBranchIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const branchId = manageBranches[selectedBranchIndex].id;
|
||||
const newStation = { name: newStationName };
|
||||
|
||||
try {
|
||||
const response = await fetchData(`${url}/${branchId}/stations`, 'POST', newStation);
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const updatedStations = [...tellerStations, response.data];
|
||||
setTellerStations(updatedStations);
|
||||
setNewStationName('');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to add teller station.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTellerStation = (index) => {
|
||||
const updatedStations = [...tellerStations];
|
||||
updatedStations.splice(index, 1);
|
||||
setTellerStations(updatedStations);
|
||||
};
|
||||
|
||||
const handleEditRemoveTellerStation = async (index) => {
|
||||
const stationToRemove = tellerStations[index];
|
||||
const branchId = manageBranches[selectedBranchIndex].id;
|
||||
const stationId = stationToRemove.id;
|
||||
|
||||
try {
|
||||
const response = await fetchData(`${url}/${branchId}/stations/${stationId}`, 'DELETE');
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const updatedStations = [...tellerStations];
|
||||
updatedStations.splice(index, 1);
|
||||
setTellerStations(updatedStations);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to remove teller station.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteBranch = (index) => {
|
||||
setDeleteConfirmation(true);
|
||||
setSelectedBranchIndex(index);
|
||||
};
|
||||
|
||||
const confirmDeleteBranch = async () => {
|
||||
const branchId = manageBranches[selectedBranchIndex].id;
|
||||
const urlToDelete = `${url}/${branchId}`;
|
||||
|
||||
try {
|
||||
const response = await fetchData(urlToDelete, 'DELETE');
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const updatedBranches = [...manageBranches];
|
||||
updatedBranches.splice(selectedBranchIndex, 1);
|
||||
setManageBranches(updatedBranches);
|
||||
setDeleteConfirmation(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to delete branch.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditBranch = async () => {
|
||||
if (branchName.trim() === '' || tellerStations.length === 0 || selectedBranchIndex === null) {
|
||||
setErrorMessage('Please fill out all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const branchId = manageBranches[selectedBranchIndex].id;
|
||||
const updatedBranch = {
|
||||
name: branchName
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchData(`${url}/${branchId}`, 'PUT', updatedBranch);
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const updatedBranches = [...manageBranches];
|
||||
updatedBranches[selectedBranchIndex] = response.data;
|
||||
setManageBranches(updatedBranches);
|
||||
setShowModal(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to update branch.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Manage Branches</h2>
|
||||
<Button variant="primary" style={styles.primaryButton} className="mb-3" onClick={() => { setShowModal(true); setSelectedBranchIndex(null); }}>Add Branch</Button>
|
||||
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Teller Stations</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{manageBranches.map((branch, index) => (
|
||||
<tr key={index}>
|
||||
<td>{branch.id}</td>
|
||||
<td>{branch.name}</td>
|
||||
<td>{branch.tellerStations ? branch.tellerStations.map(station => station.name).join(', ') : '-'}</td>
|
||||
<td>
|
||||
<Button variant="info" style={styles.infoButton} onClick={() => handleEditClick(index)}>Edit</Button>{' '}
|
||||
<Button variant="danger" onClick={() => handleDeleteBranch(index)}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Modal show={showModal} onHide={() => { setShowModal(false); setSelectedBranchIndex(null); }}>
|
||||
<Modal.Header closeButton style={styles.modalHeader}>
|
||||
<Modal.Title>{selectedBranchIndex !== null ? 'EDIT BRANCH' : 'ADD BRANCH'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group controlId="formBranchName" className="mb-3">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control type="text" placeholder="Enter branch name" value={branchName} onChange={(e) => setBranchName(e.target.value)} />
|
||||
</Form.Group>
|
||||
<Form.Group controlId="formNewStation" className="mb-3">
|
||||
<Form.Label>New Teller Station</Form.Label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Form.Control type="text" placeholder="Enter new station name" value={newStationName} onChange={(e) => setNewStationName(e.target.value)} />
|
||||
<Button variant="primary" onClick={selectedBranchIndex !== null ? handleEditTellerStation : handleAddTellerStation} style={{ backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8', marginLeft: '10px' }}>Add</Button>
|
||||
</div>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="formTellerStations" className="mb-3">
|
||||
<Form.Label>Teller Stations</Form.Label>
|
||||
<ListGroup>
|
||||
{tellerStations.map((station, index) => (
|
||||
<ListGroup.Item key={index}>
|
||||
{station.name}{' '}
|
||||
{selectedBranchIndex !== null ?
|
||||
<Button variant="danger" size="sm" onClick={() => handleEditRemoveTellerStation(index)}>X</Button> :
|
||||
<Button variant="danger" size="sm" onClick={() => handleRemoveTellerStation(index)}>X</Button>
|
||||
}
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => { setShowModal(false); setSelectedBranchIndex(null); }}>Close</Button>
|
||||
{selectedBranchIndex !== null ?
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleEditBranch}>Save Changes</Button> :
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleAddBranch}>Add Branch</Button>
|
||||
}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={deleteConfirmation} onHide={() => setDeleteConfirmation(false)}>
|
||||
<Modal.Header closeButton style={{ backgroundColor: '#334257', color: 'white' }}>
|
||||
<Modal.Title>CONFIRMATION</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to delete this branch?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setDeleteConfirmation(false)}>Cancel</Button>
|
||||
<Button variant="danger" onClick={confirmDeleteBranch}>Delete</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={errorMessage !== ''} onHide={() => setErrorMessage('')} backdrop="static" keyboard={false}>
|
||||
<Modal.Header closeButton style={{ backgroundColor: '#dc3545', color: 'white' }}>
|
||||
<Modal.Title>ERROR</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>{errorMessage}</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setErrorMessage('')}>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageBranchesScreen;
|
||||
@@ -0,0 +1,39 @@
|
||||
#root-displays {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#table-custom-displays {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-custom-displays {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.button-custom-blue-displays {
|
||||
background-color: var(--light-blue);
|
||||
}
|
||||
|
||||
.button-custom-blue-displays:hover {
|
||||
background-color: #476072;
|
||||
}
|
||||
|
||||
.button-custom-blue-displays:active {
|
||||
background-color: var(--blue) !important;
|
||||
}
|
||||
|
||||
#button-add-displays {
|
||||
width: 100%;
|
||||
max-width: 175px;
|
||||
}
|
||||
|
||||
.modal-custom-header-displays {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import Table from "react-bootstrap/Table";
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
import Form from "react-bootstrap/Form";
|
||||
import "./ManageDisplays.css";
|
||||
import { fetchData } from "../../fetching/Fetch";
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
|
||||
const ManageDisplays = () => {
|
||||
const { tenantCode } = useParams();
|
||||
const [displays, setDisplays] = useState([]);
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [selectedDisplayId, setSelectedDisplayId] = useState(-1);
|
||||
const [displayNameInput, setDisplayNameInput] = useState("");
|
||||
const [selectedBranchId, setSelectedBranchId] = useState(-1);
|
||||
const [branches, setBranches] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(`${SERVER_URL}/api/v1/branches/${tenantCode}`, "GET")
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
setBranches(res.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching branches:", error);
|
||||
});
|
||||
}, [tenantCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (branches.length !== 0) {
|
||||
setSelectedBranchId(branches[0].id);
|
||||
}
|
||||
}, [branches]);
|
||||
|
||||
const getDisplays = () => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/displays/${tenantCode}`,
|
||||
"GET"
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
setDisplays(res.data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addDisplay = (displayName, selectedBranchId) => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/displays/${tenantCode}`,
|
||||
"POST",
|
||||
{ name: displayName, branchId: selectedBranchId}
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getDisplays();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const editDisplay = (displayName) => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/displays/${tenantCode}/${selectedDisplayId}`,
|
||||
"PUT",
|
||||
{ name: displayName}
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getDisplays();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteDisplay = () => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/displays/${tenantCode}/${selectedDisplayId}`,
|
||||
"DELETE"
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getDisplays();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDisplays();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="root-displays">
|
||||
<h2>Manage Displays</h2>
|
||||
<Table id="table-custom-displays" variant="light" striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Branch</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displays.map((display) => (
|
||||
<tr key={display.id}>
|
||||
<td>{display.id}</td>
|
||||
<td>{display.name}</td>
|
||||
<td>{display.branch.name}</td>
|
||||
<td>
|
||||
<Button
|
||||
id="button-edit"
|
||||
className="button-custom-displays button-custom-blue-displays"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setDisplayNameInput(display.name);
|
||||
setShowEdit(true);
|
||||
setSelectedDisplayId(display.id);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
id="button-delete"
|
||||
className="button-custom"
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
setSelectedDisplayId(display.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Button
|
||||
id="button-add-displays"
|
||||
className="button-custom-displays button-custom-blue-displays"
|
||||
variant="success"
|
||||
onClick={() => {
|
||||
setShowAdd(true);
|
||||
}}
|
||||
>
|
||||
Add Display
|
||||
</Button>
|
||||
<Modal show={showAdd}>
|
||||
<Modal.Header className="modal-custom-header-displays">
|
||||
<Modal.Title>Add Display</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group className="mb-3" controlId="formBasicEmail">
|
||||
<Form.Label>Display Name</Form.Label>
|
||||
<Form.Control
|
||||
value={displayNameInput}
|
||||
onChange={(e) => setDisplayNameInput(e.target.value)}
|
||||
type="text"
|
||||
placeholder="Enter Display Name"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId="formBasicBranch">
|
||||
<Form.Label>Branch</Form.Label>
|
||||
<Form.Select value = {selectedBranchId} onChange = {(b) => {
|
||||
setSelectedBranchId(b.target.value);
|
||||
}}>
|
||||
{branches.map(branch => (
|
||||
<option key={branch.id} value={branch.id}>{branch.name}</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDisplayNameInput("");
|
||||
setShowAdd(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="button-custom-blue-displays"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (displayNameInput === "") {
|
||||
alert("Display name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
setShowAdd(false);
|
||||
addDisplay(displayNameInput, selectedBranchId);
|
||||
setDisplayNameInput("");
|
||||
}}
|
||||
>
|
||||
Add Display
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showEdit}>
|
||||
<Modal.Header className="modal-custom-header">
|
||||
<Modal.Title>Edit Display</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group className="mb-3" controlId="formBasicEmail">
|
||||
<Form.Label>Display Name</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter Display Name"
|
||||
value={displayNameInput}
|
||||
onChange={(e) => setDisplayNameInput(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDisplayNameInput("");
|
||||
setShowEdit(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="button-custom-blue-displays"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (displayNameInput === "") {
|
||||
alert("Display name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
setShowEdit(false);
|
||||
editDisplay(displayNameInput, selectedBranchId);
|
||||
setDisplayNameInput("");
|
||||
}}
|
||||
>
|
||||
Edit Display
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showDelete}>
|
||||
<Modal.Header className="modal-custom-header">
|
||||
<Modal.Title>Delete Display</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to delete this display?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDisplayNameInput("");
|
||||
setShowDelete(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setShowDelete(false);
|
||||
deleteDisplay();
|
||||
}}
|
||||
>
|
||||
Delete Display
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageDisplays;
|
||||
@@ -0,0 +1,345 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Modal, Form, Dropdown, DropdownButton } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
|
||||
export default function ManageGroupsScreen() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [deleteConfirmation, setDeleteConfirmation] = useState(false);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [groupName, setGroupName] = useState('');
|
||||
const [selectedBranches, setSelectedBranches] = useState([]);
|
||||
const [selectedServices, setSelectedServices] = useState([]);
|
||||
const [availableBranches, setAvailableBranches] = useState([]);
|
||||
const [availableServices, setAvailableServices] = useState([]);
|
||||
const [selectedGroup, setSelectedGroup] = useState();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showModal) {
|
||||
// Reset modal state when it closes
|
||||
setGroupName('')
|
||||
setSelectedGroup(undefined)
|
||||
setSelectedBranches([])
|
||||
setSelectedServices([])
|
||||
setAvailableBranches([])
|
||||
setAvailableServices([])
|
||||
setErrorMessage('')
|
||||
}
|
||||
}, [showModal]);
|
||||
|
||||
const isValid = groupName.trim() !== '' && selectedBranches.length !== 0 && selectedServices.length !== 0
|
||||
|
||||
function fetchAvailableBranches(group) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ group.id }/assignable/branch`)
|
||||
.then(response => response.data)
|
||||
.then(setAvailableBranches)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchAvailableServices(group) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ group.id }/assignable/service`)
|
||||
.then(response => response.data)
|
||||
.then(setAvailableServices)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchGroups() {
|
||||
fetchData(`${ url }groups/${ tenantCode }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setGroups)
|
||||
}
|
||||
|
||||
function handleAddGroup() {
|
||||
if (!isValid) {
|
||||
setErrorMessage('Please fill out all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const groupData = {
|
||||
name: groupName,
|
||||
branchIds: selectedBranches.map(branch => branch.id),
|
||||
serviceIds: selectedServices.map(service => service.id)
|
||||
};
|
||||
|
||||
fetchData(`${ url }groups/${ tenantCode }`, 'POST', groupData)
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
|
||||
setShowModal(false)
|
||||
}
|
||||
|
||||
function handleEditGroup() {
|
||||
if (!isValid) {
|
||||
setErrorMessage('Please fill out all fields.');
|
||||
return
|
||||
}
|
||||
|
||||
const updatedGroup = {
|
||||
...selectedGroup,
|
||||
name: groupName,
|
||||
branches: selectedBranches,
|
||||
services: selectedServices
|
||||
};
|
||||
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup?.id }`, 'PUT', updatedGroup)
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
|
||||
setShowModal(false)
|
||||
}
|
||||
|
||||
function handleDeleteGroup(group) {
|
||||
setSelectedGroup(group)
|
||||
setDeleteConfirmation(true);
|
||||
}
|
||||
|
||||
function confirmDeleteGroup() {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup?.id }`, 'DELETE')
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
|
||||
setDeleteConfirmation(false)
|
||||
setSelectedGroup(undefined)
|
||||
}
|
||||
|
||||
function handleEditClick(group) {
|
||||
setSelectedGroup(group)
|
||||
setGroupName(group.name)
|
||||
|
||||
setSelectedBranches(group.branches)
|
||||
setSelectedServices(group.services)
|
||||
|
||||
fetchAvailableBranches(group)
|
||||
fetchAvailableServices(group)
|
||||
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
function handleBranchSelection(branchId) {
|
||||
const branchToAdd = availableBranches.find(branch => branch.id == branchId)
|
||||
setSelectedBranches([ ...selectedBranches, branchToAdd ])
|
||||
|
||||
if (selectedGroup) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup.id }/branches/${ branchId }`, 'PUT')
|
||||
.then(() => fetchAvailableBranches(selectedGroup))
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
} else {
|
||||
const updatedAvailable = availableBranches.filter(branch => branch.id != branchId)
|
||||
setAvailableBranches(updatedAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveBranch(branch) {
|
||||
const updatedBranches = selectedBranches.filter(selectedBranch => selectedBranch.id !== branch.id);
|
||||
setSelectedBranches(updatedBranches);
|
||||
|
||||
if (selectedGroup) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup?.id }/branches/${ branch.id }`, 'DELETE')
|
||||
.then(() => fetchAvailableBranches(selectedGroup))
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
} else {
|
||||
setAvailableBranches([ ...availableBranches, branch ])
|
||||
}
|
||||
}
|
||||
|
||||
function handleServiceSelection(serviceId) {
|
||||
const serviceToAdd = availableServices.find(service => service.id == serviceId)
|
||||
setSelectedServices([ ...selectedServices, serviceToAdd ])
|
||||
|
||||
if (selectedGroup) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup.id }/services/${ serviceId }`, 'PUT')
|
||||
.then(() => fetchAvailableServices(selectedGroup))
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
} else {
|
||||
const updatedAvailable = availableServices.filter(service => service.id != serviceId)
|
||||
setAvailableServices(updatedAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveService(service) {
|
||||
const updatedServices = selectedServices.filter(selectedService => selectedService.id !== service.id);
|
||||
setSelectedServices(updatedServices);
|
||||
|
||||
if (selectedGroup) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup?.id }/services/${ service.id }`, 'DELETE')
|
||||
.then(() => fetchAvailableServices(selectedGroup))
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
} else {
|
||||
setAvailableServices([ ...availableServices, service ])
|
||||
}
|
||||
}
|
||||
|
||||
function onStartAddGroup() {
|
||||
setSelectedBranches([])
|
||||
setSelectedServices([])
|
||||
|
||||
fetchData(`${ url }branches/${ tenantCode }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setAvailableBranches)
|
||||
.catch(console.error)
|
||||
|
||||
fetchData(`${ url }tenants/${ tenantCode }/services`)
|
||||
.then(response => response.data)
|
||||
.then(setAvailableServices)
|
||||
.catch(console.error)
|
||||
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Groups of Branches</h2>
|
||||
<Button variant="primary" style={ { backgroundColor: '#548CA8', borderColor: '#548CA8' } } className="mb-3"
|
||||
onClick={ onStartAddGroup }>Add Group</Button>
|
||||
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Branches</th>
|
||||
<th>Services</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ groups.map((group, index) => (
|
||||
<tr key={ index }>
|
||||
<td>{ group.id }</td>
|
||||
<td>{ group.name }</td>
|
||||
<td>{ group.branches.map(branch => branch.name).join(', ') }</td>
|
||||
<td>{ group.services.map(service => service.name).join(', ') }</td>
|
||||
<td className="d-flex justify-content-center gap-2">
|
||||
<Button variant="info"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ () => handleEditClick(group) }>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<Button variant="danger"
|
||||
onClick={ () => handleDeleteGroup(group) }>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)) }
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Modal show={ showModal } onHide={ () => {
|
||||
setShowModal(false);
|
||||
} }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#334257', color: 'white' } }>
|
||||
<Modal.Title>{ selectedGroup ? 'EDIT GROUP' : 'ADD GROUP' }</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group controlId="formGroupName" className="mb-3">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control type="text"
|
||||
placeholder="Enter group name"
|
||||
value={ groupName }
|
||||
onChange={ (e) => setGroupName(e.target.value) } />
|
||||
</Form.Group>
|
||||
<Form.Group controlId="formGroupBranches" className="mb-3">
|
||||
{ selectedBranches.map((branch, index) => (
|
||||
<span key={ index } className="badge bg-secondary m-1"
|
||||
style={ { display: 'inline-flex', alignItems: 'center' } }>
|
||||
{ branch.name }{ ' ' }
|
||||
<Button variant="danger" size="sm" style={ {
|
||||
marginLeft: '5px',
|
||||
padding: '2px 5px',
|
||||
backgroundColor: '#dc3545',
|
||||
borderColor: '#dc3545'
|
||||
} } onClick={ () => handleRemoveBranch(branch) }>X</Button>
|
||||
</span>
|
||||
)) }
|
||||
<DropdownButton
|
||||
title="Select Branches"
|
||||
onSelect={ (eventKey) => handleBranchSelection(eventKey) }
|
||||
variant="btn btn-outline-secondary"
|
||||
>
|
||||
{ availableBranches.map((branch, index) => (
|
||||
<Dropdown.Item key={ index } eventKey={ branch.id }>{ branch.name }</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="formGroupServices" className="mb-3">
|
||||
{ selectedServices.map((service, index) => (
|
||||
<span key={ index } className="badge bg-secondary m-1"
|
||||
style={ { display: 'inline-flex', alignItems: 'center' } }>
|
||||
{ service.name }{ ' ' }
|
||||
<Button variant="danger" size="sm" style={ {
|
||||
marginLeft: '5px',
|
||||
padding: '2px 5px',
|
||||
backgroundColor: '#dc3545',
|
||||
borderColor: '#dc3545'
|
||||
} } onClick={ () => handleRemoveService(service) }>X</Button>
|
||||
</span>
|
||||
)) }
|
||||
<DropdownButton
|
||||
title="Select Services"
|
||||
onSelect={ (eventKey) => handleServiceSelection(eventKey) }
|
||||
variant="btn btn-outline-secondary"
|
||||
>
|
||||
{ availableServices.map((service, index) => (
|
||||
<Dropdown.Item key={ index }
|
||||
eventKey={ service.id }>{ service.name }</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{ selectedGroup ?
|
||||
<Button variant="primary" style={ { backgroundColor: '#548CA8', borderColor: '#548CA8' } }
|
||||
onClick={ handleEditGroup }>Save Changes</Button> :
|
||||
<Button variant="primary" style={ { backgroundColor: '#548CA8', borderColor: '#548CA8' } }
|
||||
onClick={ handleAddGroup }>Add Group</Button>
|
||||
}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={ deleteConfirmation } onHide={ () => setDeleteConfirmation(false) }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#334257', color: 'white' } }>
|
||||
<Modal.Title>CONFIRMATION</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to delete this group?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={ () => setDeleteConfirmation(false) }>Cancel</Button>
|
||||
<Button variant="danger" onClick={ confirmDeleteGroup }>Delete</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={ errorMessage !== '' } onHide={ () => setErrorMessage('') } backdrop="static"
|
||||
keyboard={ false }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#dc3545', color: 'white' } }>
|
||||
<Modal.Title>ERROR</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>{ errorMessage }</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={ () => setErrorMessage('') }>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
#root-services {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#table-custom-services {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-custom-services {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.button-custom-blue-services {
|
||||
background-color: var(--light-blue);
|
||||
}
|
||||
|
||||
.button-custom-blue-services:hover {
|
||||
background-color: #476072;
|
||||
}
|
||||
|
||||
.button-custom-blue-services:active {
|
||||
background-color: var(--blue) !important;
|
||||
}
|
||||
|
||||
#button-add-services {
|
||||
width: 100%;
|
||||
max-width: 175px;
|
||||
}
|
||||
|
||||
.modal-custom-header-services {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import Header from "../../components/Header/Header";
|
||||
import Table from "react-bootstrap/Table";
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
import Form from "react-bootstrap/Form";
|
||||
import "./ManageServices.css";
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { fetchData } from "../../fetching/Fetch";
|
||||
|
||||
const ManageServices = () => {
|
||||
const { tenantCode } = useParams();
|
||||
const [services, setServices] = useState([]);
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [selectedServiceId, setSelectedServiceId] = useState(-1);
|
||||
const [serviceNameInput, setServiceNameInput] = useState("");
|
||||
|
||||
|
||||
const getServices = () => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/tenants/${tenantCode}/services`,
|
||||
"GET"
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
setServices(res.data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addService = (serviceName) => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/tenants/${tenantCode}/services`,
|
||||
"POST",
|
||||
{ name: serviceName }
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getServices();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const editService = (serviceName) => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/tenants/${tenantCode}/services/${selectedServiceId}`,
|
||||
"PUT",
|
||||
{ name: serviceName }
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getServices();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteService = () => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/tenants/${tenantCode}/services/${selectedServiceId}`,
|
||||
"DELETE"
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getServices();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getServices();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="root-services">
|
||||
<h2>Manage Services</h2>
|
||||
<Table id="table-custom-services" variant="light" striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{services.map((service) => (
|
||||
<tr key={service.id}>
|
||||
<td>{service.id}</td>
|
||||
<td>{service.name}</td>
|
||||
<td>
|
||||
<Button
|
||||
id="button-edit"
|
||||
className="button-custom-services button-custom-blue-services"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setServiceNameInput(service.name);
|
||||
setShowEdit(true);
|
||||
setSelectedServiceId(service.id);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
id="button-delete"
|
||||
className="button-custom"
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
setSelectedServiceId(service.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Button
|
||||
id="button-add-services"
|
||||
className="button-custom-services button-custom-blue-services"
|
||||
variant="success"
|
||||
onClick={() => {
|
||||
setShowAdd(true);
|
||||
}}
|
||||
>
|
||||
Add Service
|
||||
</Button>
|
||||
<Modal show={showAdd}>
|
||||
<Modal.Header className="modal-custom-header-services">
|
||||
<Modal.Title>Add Service</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group
|
||||
className="mb-3"
|
||||
controlId="formBasicEmail"
|
||||
>
|
||||
<Form.Label>Service Name</Form.Label>
|
||||
<Form.Control
|
||||
value={serviceNameInput}
|
||||
onChange={(e) =>
|
||||
setServiceNameInput(e.target.value)
|
||||
}
|
||||
type="text"
|
||||
placeholder="Enter Service Name"
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setServiceNameInput("");
|
||||
setShowAdd(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="button-custom-blue-services"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if(serviceNameInput === "") {
|
||||
alert("Service name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
setShowAdd(false);
|
||||
addService(serviceNameInput);
|
||||
setServiceNameInput("");
|
||||
}}
|
||||
>
|
||||
Add Service
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showEdit}>
|
||||
<Modal.Header className="modal-custom-header">
|
||||
<Modal.Title>Edit Service</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group
|
||||
className="mb-3"
|
||||
controlId="formBasicEmail"
|
||||
>
|
||||
<Form.Label>Service Name</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter Service Name"
|
||||
value={serviceNameInput}
|
||||
onChange={(e) =>
|
||||
setServiceNameInput(e.target.value)
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setServiceNameInput("");
|
||||
setShowEdit(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="button-custom-blue-services"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if(serviceNameInput === "") {
|
||||
alert("Service name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
setShowEdit(false);
|
||||
editService(serviceNameInput);
|
||||
setServiceNameInput("");
|
||||
}}
|
||||
>
|
||||
Edit Service
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showDelete}>
|
||||
<Modal.Header className="modal-custom-header">
|
||||
<Modal.Title>Delete Service</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to delete this service?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setServiceNameInput("");
|
||||
setShowDelete(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setShowDelete(false);
|
||||
deleteService();
|
||||
}}
|
||||
>
|
||||
Delete Service
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageServices;
|
||||
@@ -0,0 +1,340 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Dropdown, DropdownButton, Form, Modal, Table } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
|
||||
const ManageStationScreen = () => {
|
||||
const [showServiceModal, setShowServiceModal] = useState(false);
|
||||
const [showDisplayModal, setShowDisplayModal] = useState(false);
|
||||
const [stations, setStations] = useState([]);
|
||||
const [selectedStation, setSelectedStation] = useState(null);
|
||||
const [selectedBranch, setSelectedBranch] = useState(null);
|
||||
const [branches, setBranches] = useState([]);
|
||||
const [selectedServices, setSelectedServices] = useState([]);
|
||||
const [availableServices, setAvailableServices] = useState([]);
|
||||
const [availableDisplays, setAvailableDisplays] = useState([]);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchBranches();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedBranch) {
|
||||
fetchStationsForBranch();
|
||||
fetchAvailableDisplays(selectedBranch);
|
||||
} else {
|
||||
setStations([]);
|
||||
}
|
||||
}, [selectedBranch]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedServices(selectedStation ? selectedStation.services : []);
|
||||
if (selectedStation) {
|
||||
fetchAvailableServices(selectedStation);
|
||||
}
|
||||
}, [selectedStation]);
|
||||
|
||||
function fetchBranches() {
|
||||
fetchData(`${ url }branches/${ tenantCode }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setBranches)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchStationsForBranch() {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ selectedBranch.id }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setStations)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchAvailableDisplays(branch) {
|
||||
fetchData(`${ url }displays/unassigned/${ tenantCode }/${ branch.id }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setAvailableDisplays)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchAssignableServices(station) {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ station.id }/services/assignable`)
|
||||
.then(response => response.data)
|
||||
.then(setAvailableServices)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function onEditServices(station) {
|
||||
fetchAssignableServices(station)
|
||||
setShowServiceModal(true)
|
||||
setSelectedStation(station)
|
||||
setSelectedServices(station.services)
|
||||
}
|
||||
|
||||
function fetchAvailableServices(station) {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ station.id }/services?assigned=false`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setAvailableServices)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
|
||||
function addService(service) {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ selectedStation.id }/services/${ service.id }`, 'PUT')
|
||||
.then(() => fetchAssignableServices(selectedStation))
|
||||
.then(fetchStationsForBranch)
|
||||
.catch(console.error)
|
||||
|
||||
setSelectedServices([...selectedServices, service]);
|
||||
}
|
||||
|
||||
function removeSelectedService(service) {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ selectedStation.id }/services/${ service.id }`, 'DELETE')
|
||||
.then(() => fetchAssignableServices(selectedStation))
|
||||
.then(fetchStationsForBranch)
|
||||
.catch(console.error)
|
||||
|
||||
const updatedServices = selectedServices.filter(selectedService => selectedService.id !== service.id)
|
||||
setSelectedServices(updatedServices)
|
||||
}
|
||||
|
||||
|
||||
// Ove dvije funkcije ispod isto treba ispraviti da se ne radi filtriranje na frontendu, ali nemam vise vremena.
|
||||
const addDisplayToStation = async (display) => {
|
||||
try {
|
||||
const response = await fetchData(`${ url }stations/${ tenantCode }/${ selectedStation.id }/displays/${ display.id }`, 'PUT');
|
||||
if (response.success) {
|
||||
const updatedStations = stations.map(station => {
|
||||
if (station.id === selectedStation.id) {
|
||||
if (station.display) {
|
||||
setAvailableDisplays(prevDisplays => [...prevDisplays, station.display]);
|
||||
}
|
||||
return {
|
||||
...station,
|
||||
display: display
|
||||
};
|
||||
}
|
||||
return station;
|
||||
});
|
||||
setStations(updatedStations);
|
||||
setAvailableDisplays(prevDisplays => prevDisplays.filter(d => d.id !== display.id));
|
||||
setSelectedStation(updatedStations.find(station => station.id === selectedStation.id));
|
||||
setShowDisplayModal(true);
|
||||
} else {
|
||||
console.error('Error adding display to station:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding display to station:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const removeDisplayFromStation = async () => {
|
||||
try {
|
||||
const response = await fetchData(`${ url }stations/${ tenantCode }/${ selectedStation.id }/displays/${ selectedStation.display.id }`, 'DELETE');
|
||||
if (response.success) {
|
||||
const updatedStations = stations.map(station => {
|
||||
if (station.id === selectedStation.id) {
|
||||
return {
|
||||
...station,
|
||||
display: null
|
||||
};
|
||||
}
|
||||
return station;
|
||||
});
|
||||
setStations(updatedStations);
|
||||
setAvailableDisplays(prevDisplays => [...prevDisplays, selectedStation.display]);
|
||||
setShowDisplayModal(false);
|
||||
} else {
|
||||
console.error('Error removing display from station:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing display from station:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setSelectedServices(selectedStation ? selectedStation.services : []);
|
||||
setShowServiceModal(false);
|
||||
setShowDisplayModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Teller Stations</h2>
|
||||
<DropdownButton title={ selectedBranch ? selectedBranch.name : 'Select Branch' }
|
||||
variant="btn btn-outline-secondary">
|
||||
{ branches.map((branch, index) => (
|
||||
<Dropdown.Item key={ index }
|
||||
onClick={ () => setSelectedBranch(branch) }>{ branch.name }</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
<div style={ { marginTop: '20px' } }>
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Station ID</th>
|
||||
<th>Station Name</th>
|
||||
<th>Services</th>
|
||||
<th>Service Action</th>
|
||||
<th>Displays</th>
|
||||
<th>Display Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ stations.map((station, index) => (
|
||||
<tr key={ index }>
|
||||
<td>{ station.id }</td>
|
||||
<td>{ station.name }</td>
|
||||
<td>
|
||||
{
|
||||
|
||||
station.services && station.services.length > 0 ? (
|
||||
station.services.map((service, serviceIndex) => (
|
||||
<span key={ serviceIndex } style={ { marginRight: '5px' } }>
|
||||
{ serviceIndex > 0 && ', ' }
|
||||
{ service.name }
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span>No services</span>
|
||||
) }
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ () => onEditServices(station) }
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</td>
|
||||
<td>
|
||||
{ station.display ? (
|
||||
<span>{ station.display.name }</span>
|
||||
) : (
|
||||
<span>No display</span>
|
||||
) }
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ () => {
|
||||
setShowDisplayModal(true);
|
||||
setSelectedStation(station);
|
||||
} }
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)) }
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
<Modal show={ showServiceModal } onHide={ handleCloseModal }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#334257', color: 'white' } }>
|
||||
<Modal.Title>Manage Services for Station</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p><strong>Teller Station:</strong> { selectedStation && selectedStation.name }</p>
|
||||
<div>
|
||||
<div style={ { marginBottom: '10px' } }>
|
||||
<strong>Selected Services:</strong>
|
||||
{ selectedServices.length > 0 ? (
|
||||
selectedServices.map((service, index) => (
|
||||
<span key={ index } className="badge bg-secondary m-1"
|
||||
style={ { display: 'inline-flex', alignItems: 'center' } }>
|
||||
{ service.name }
|
||||
<Button variant="danger" size="sm" style={ {
|
||||
marginLeft: '5px',
|
||||
padding: '2px 5px',
|
||||
backgroundColor: '#dc3545',
|
||||
borderColor: '#dc3545'
|
||||
} } onClick={ () => removeSelectedService(service) }>X</Button>
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span> No service selected</span>
|
||||
) }
|
||||
</div>
|
||||
<Form.Group controlId="formGroupService" className="mb-3">
|
||||
<DropdownButton title={ 'Select Service' } variant="btn btn-outline-secondary">
|
||||
{ availableServices.map((service, index) => {
|
||||
const isAssigned = selectedServices.some(selectedService => selectedService.id === service.id);
|
||||
if (!isAssigned) {
|
||||
return (
|
||||
<Dropdown.Item key={ index }
|
||||
onClick={ () => addService(service) }>{ service.name }</Dropdown.Item>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}) }
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
|
||||
{ errorMessage && <p className="text-danger">{ errorMessage }</p> }
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={ handleCloseModal }>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={ showDisplayModal } onHide={ handleCloseModal }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#334257', color: 'white' } }>
|
||||
<Modal.Title>Manage Display for Station</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p><strong>Teller Station:</strong> { selectedStation && selectedStation.name }</p>
|
||||
<div>
|
||||
<div style={ { marginBottom: '10px' } }>
|
||||
<strong>Selected Display:</strong>
|
||||
{ selectedStation && selectedStation.display ? (
|
||||
<span className="badge bg-secondary m-1" style={ {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center'
|
||||
} }> { selectedStation.display.name }
|
||||
<Button variant="danger" size="sm" style={ {
|
||||
marginLeft: '5px',
|
||||
padding: '2px 5px',
|
||||
backgroundColor: '#dc3545',
|
||||
borderColor: '#dc3545'
|
||||
} } onClick={ removeDisplayFromStation }>X</Button>
|
||||
</span>
|
||||
) : (
|
||||
<span> No display selected</span>
|
||||
) }
|
||||
</div>
|
||||
<Form>
|
||||
<Form.Group controlId="formGroupDisplay" className="mb-3">
|
||||
<DropdownButton
|
||||
title={ 'Select Display' }
|
||||
variant="btn btn-outline-secondary"
|
||||
>
|
||||
{ availableDisplays.map((display, index) => (
|
||||
<Dropdown.Item key={ index }
|
||||
onClick={ () => addDisplayToStation(display) }>{ display.name }</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
|
||||
|
||||
</Form.Group>
|
||||
</Form>
|
||||
{ errorMessage && <p className="text-danger">{ errorMessage }</p> }
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={ handleCloseModal }>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageStationScreen;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import {useContext} from "react";
|
||||
|
||||
export default function NotFound() {
|
||||
const navigate = useNavigate();
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
function handleHomeClick() {
|
||||
if (user) {
|
||||
navigate(`${user.tenantCode}/home`);
|
||||
} else {
|
||||
navigate('/login');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={ {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
gap: '10px'
|
||||
} }>
|
||||
<p>
|
||||
404 - The page does not exist.
|
||||
</p>
|
||||
<button style={ {
|
||||
width: 'fit-content',
|
||||
border: '1px solid blue',
|
||||
borderRadius: '5px',
|
||||
padding: '5px 30px',
|
||||
cursor: 'pointer'
|
||||
} }
|
||||
onClick={ handleHomeClick }>
|
||||
Go back home?
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color:ghostwhite;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from "react";
|
||||
import RegistrationForm from '../../components/RegistrationForm/RegistrationForm.jsx'
|
||||
import "./RegistrationScreen.css";
|
||||
|
||||
export default function RegistrationScreen(){
|
||||
return(
|
||||
<main>
|
||||
<RegistrationForm/>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Modal, Form, Dropdown, DropdownButton } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { SERVER_URL } from 'src/constants.js';
|
||||
import { fetchData } from '../../fetching/Fetch.js'; // Import fetchData function
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
const StationServiceScreen = () => {
|
||||
const location = useLocation();
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [deleteConfirmation, setDeleteConfirmation] = useState(false);
|
||||
const [stations, setStations] = useState([]);
|
||||
const [selectedStation, setSelectedStation] = useState(null);
|
||||
const [selectedServices, setSelectedServices] = useState([]);
|
||||
const [availableServices, setAvailableServices] = useState([]);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchStations() {
|
||||
try {
|
||||
const tenantCode = location.pathname.split('/')[1];
|
||||
const response = await fetchData(`${SERVER_URL}/api/v1/stations/${tenantCode}`, 'GET');
|
||||
if (response.success) {
|
||||
setStations(response.data);
|
||||
} else {
|
||||
console.error('Error fetching stations:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching stations:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchAvailableServices() {
|
||||
try {
|
||||
const tenantCode = location.pathname.split('/')[1];
|
||||
const response = await fetchData(`${SERVER_URL}/api/v1/tenants/${tenantCode}/services`, 'GET');
|
||||
if (response.success) {
|
||||
setAvailableServices(response.data);
|
||||
} else {
|
||||
console.error('Error fetching available services:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching available services:', error);
|
||||
}
|
||||
}
|
||||
|
||||
fetchStations();
|
||||
fetchAvailableServices();
|
||||
}, [location]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedServices(selectedStation ? selectedStation.services : []);
|
||||
}, [selectedStation]);
|
||||
|
||||
const confirmDeleteStation = () => {
|
||||
// Implement station deletion logic here
|
||||
};
|
||||
|
||||
const saveServiceToStation = async (service) => {
|
||||
try {
|
||||
const tenantCode = location.pathname.split('/')[1];
|
||||
const response = await fetchData(`${SERVER_URL}/api/v1/stations/${tenantCode}/${selectedStation.id}/services/${service.id}`, 'PUT');
|
||||
if (response.success) {
|
||||
console.log('Service added to station successfully');
|
||||
|
||||
// Ažurirajte lokalno stanje stanicama kako biste odmah prikazali dodani servis
|
||||
const updatedStations = stations.map(station => {
|
||||
if (station.id === selectedStation.id) {
|
||||
return {
|
||||
...station,
|
||||
services: [...station.services, service]
|
||||
};
|
||||
}
|
||||
return station;
|
||||
});
|
||||
setStations(updatedStations);
|
||||
} else {
|
||||
console.error('Error adding service to station:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding service to station:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const removeServiceFromStation = async (serviceId) => {
|
||||
try {
|
||||
const tenantCode = location.pathname.split('/')[1];
|
||||
const response = await fetchData(`${SERVER_URL}/api/v1/stations/${tenantCode}/${selectedStation.id}/services/${serviceId}`, 'DELETE');
|
||||
if (response.success) {
|
||||
console.log('Service removed from station successfully');
|
||||
|
||||
// Ažurirajte lokalno stanje stanicama kako biste odmah prikazali uklonjeni servis
|
||||
const updatedStations = stations.map(station => {
|
||||
if (station.id === selectedStation.id) {
|
||||
return {
|
||||
...station,
|
||||
services: station.services.filter(service => service.id !== serviceId)
|
||||
};
|
||||
}
|
||||
return station;
|
||||
});
|
||||
setStations(updatedStations);
|
||||
} else {
|
||||
console.error('Error removing service from station:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing service from station:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const removeSelectedService = (serviceId) => {
|
||||
removeServiceFromStation(serviceId);
|
||||
const updatedServices = selectedServices.filter(service => service.id !== serviceId);
|
||||
setSelectedServices(updatedServices);
|
||||
};
|
||||
|
||||
const addService = (service) => {
|
||||
saveServiceToStation(service);
|
||||
setSelectedServices([...selectedServices, service]);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setSelectedServices(selectedStation ? selectedStation.services : []);
|
||||
setShowModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<h2>Teller Stations</h2>
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Station ID</th>
|
||||
<th>Station Name</th>
|
||||
<th>Services</th>
|
||||
<th>Service Action</th>
|
||||
<th>Displays</th>
|
||||
<th>Display Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stations.map((station, index) => (
|
||||
<tr key={index}>
|
||||
<td>{station.id}</td>
|
||||
<td>{station.name}</td>
|
||||
<td>
|
||||
{station.services && station.services.length > 0 ? (
|
||||
station.services.map((service, serviceIndex) => (
|
||||
<span key={serviceIndex} style={{marginRight: '5px'}}>
|
||||
{serviceIndex > 0 && ', '}
|
||||
{service.name}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span>No services</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Button variant="primary"
|
||||
style={{backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8'}}
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
setSelectedStation(station);
|
||||
}}>Edit</Button>
|
||||
</td>
|
||||
<td>
|
||||
{station.services && station.services.length > 0 ? (
|
||||
station.services.map((service, serviceIndex) => (
|
||||
<span key={serviceIndex} style={{marginRight: '5px'}}>
|
||||
{serviceIndex > 0 && ', '}
|
||||
{service.name}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span>No displays</span>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<Button variant="primary"
|
||||
style={{backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8'}}
|
||||
onClick={() => {
|
||||
setShowModal(true);
|
||||
setSelectedStation(station);
|
||||
}}>Edit</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Modal show={showModal} onHide={handleCloseModal}>
|
||||
<Modal.Header closeButton style={{backgroundColor: '#334257', color: 'white'}}>
|
||||
<Modal.Title>Add Service to Station</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p><strong>Teller Station:</strong> {selectedStation && selectedStation.name}</p>
|
||||
<div>
|
||||
<div style={{marginBottom: '10px'}}>
|
||||
<strong>Selected Services:</strong>
|
||||
{selectedServices.map((service, index) => (
|
||||
<span key={index} className="badge bg-secondary m-1" style={{ display: 'inline-flex', alignItems: 'center' }}>
|
||||
{service.name}
|
||||
<Button variant="danger" size="sm" style={{ marginLeft: '5px', padding: '2px 5px', backgroundColor: '#dc3545', borderColor: '#dc3545' }} onClick={() => removeSelectedService(service.id)}>X</Button>
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<Form>
|
||||
<Form.Group controlId="formGroupService" className="mb-3">
|
||||
<DropdownButton
|
||||
title={'Select Service'}
|
||||
variant="btn btn-outline-secondary"
|
||||
>
|
||||
{availableServices.map((service, index) => (
|
||||
<Dropdown.Item key={index} onClick={() => addService(service)}>{service.name}</Dropdown.Item>
|
||||
))}
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
{errorMessage && <p className="text-danger">{errorMessage}</p>}
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleCloseModal}>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={deleteConfirmation} onHide={() => setDeleteConfirmation(false)}>
|
||||
<Modal.Header closeButton style={{ backgroundColor: '#334257', color: 'white' }}>
|
||||
<Modal.Title>Confirmation</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
Are you sure you want to delete this station?
|
||||
</Modal.Body>
|
||||
<Modal.Footer style={{ backgroundColor: '#334257', color: 'white' }}>
|
||||
<Button variant="secondary" onClick={() => setDeleteConfirmation(false)}>Cancel</Button>
|
||||
<Button variant="danger" onClick={confirmDeleteStation}>Delete</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StationServiceScreen;
|
||||
@@ -0,0 +1,231 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Modal, Form } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const styles = {
|
||||
primaryButton: {
|
||||
backgroundColor: "var(--light-blue)",
|
||||
borderColor: "var(--light-blue)",
|
||||
},
|
||||
infoButton: {
|
||||
backgroundColor: "var(--light-blue)",
|
||||
color: 'white',
|
||||
borderColor: "var(--light-blue)",
|
||||
},
|
||||
modalHeader: {
|
||||
backgroundColor: "var(--blue)",
|
||||
color: 'white',
|
||||
},
|
||||
};
|
||||
|
||||
const UserManageScreen = () => {
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [userEmail, setUserEmail] = useState('');
|
||||
const [userPassword, setUserPassword] = useState('');
|
||||
const [selectedUserIndex, setSelectedUserIndex] = useState(null);
|
||||
const [token, setToken] = useState('');
|
||||
const [emailError, setEmailError] = useState('');
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (storedToken) {
|
||||
setToken(storedToken);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchUsers();
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const requestBody = JSON.stringify({
|
||||
roleName: 'ROLE_USER'
|
||||
});
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUsers(data);
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const validateEmail = (email) => {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
};
|
||||
|
||||
const handleAddUser = async () => {
|
||||
const requestBody = {
|
||||
email: userEmail,
|
||||
password: userPassword,
|
||||
roleName: 'ROLE_USER'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setUsers([...users, data]);
|
||||
setShowModal(false);
|
||||
setUserEmail('');
|
||||
setUserPassword('');
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditUser = async () => {
|
||||
if (!validateEmail(userEmail)) {
|
||||
setEmailError('Invalid email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setEmailError('');
|
||||
|
||||
try {
|
||||
const updatedUser = {
|
||||
email: userEmail
|
||||
};
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${users[selectedUserIndex].id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: JSON.stringify(updatedUser),
|
||||
});
|
||||
if (response.ok) {
|
||||
const updatedUsers = [...users];
|
||||
updatedUsers[selectedUserIndex].email = userEmail;
|
||||
setUsers(updatedUsers);
|
||||
setShowModal(false);
|
||||
setUserEmail('');
|
||||
setUserPassword('');
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (userId) => {
|
||||
try {
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const updatedUsers = users.filter(user => user.id !== userId);
|
||||
setUsers(updatedUsers);
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = (index) => {
|
||||
const user = users[index];
|
||||
setSelectedUserIndex(index);
|
||||
setUserEmail(user.email);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Manage Users</h2>
|
||||
<Button variant="primary" style={styles.primaryButton} className="mb-3" onClick={() => { setShowModal(true); setSelectedUserIndex(null); }}>Add User</Button>
|
||||
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{users.map((user, index) => (
|
||||
<tr key={index}>
|
||||
<td>{user.id}</td>
|
||||
<td>{user.email}</td>
|
||||
<td>
|
||||
<Button variant="info" style={styles.infoButton} onClick={() => handleEditClick(index)}>Edit</Button>{' '}
|
||||
<Button variant="danger" onClick={() => handleDeleteUser(user.id)}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Modal show={showModal} onHide={() => { setShowModal(false); setSelectedUserIndex(null); }}>
|
||||
<Modal.Header closeButton style={styles.modalHeader}>
|
||||
<Modal.Title>{selectedUserIndex !== null ? 'EDIT USER' : 'ADD USER'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group controlId="formUserEmail" className="mb-3">
|
||||
<Form.Label>Email</Form.Label>
|
||||
<Form.Control type="email" placeholder="Enter user email" value={userEmail} onChange={(e) => setUserEmail(e.target.value)} />
|
||||
{emailError && <div style={{ color: 'red' }}>{emailError}</div>}
|
||||
</Form.Group>
|
||||
{selectedUserIndex === null && (
|
||||
<Form.Group controlId="formUserPassword" className="mb-3">
|
||||
<Form.Label>Password</Form.Label>
|
||||
<Form.Control type="password" placeholder="Enter user password" value={userPassword} onChange={(e) => setUserPassword(e.target.value)} />
|
||||
{passwordError && <div style={{ color: 'red' }}>{passwordError}</div>}
|
||||
</Form.Group>
|
||||
)}
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => { setShowModal(false); setSelectedUserIndex(null); }}>Close</Button>
|
||||
{selectedUserIndex !== null ?
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleEditUser}>Save Changes</Button> :
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleAddUser}>Add User</Button>
|
||||
}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserManageScreen;
|
||||
@@ -0,0 +1,192 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Table, Dropdown, DropdownButton, Button } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { useInterval } from '../../hooks/hooks.jsx';
|
||||
import { TimePicker } from '../../components/TimePicker/TimePicker.jsx'
|
||||
import { formatDate, getDifference, getToday, todayWithTime } from '../../utils/DateUtils.js';
|
||||
import { addSimpleParamToUrl, addSortToUrl, parseSortFromUrl } from '../../utils/QueryParamsService.js';
|
||||
import { SortContextProvider } from '../../context/SortContext.jsx';
|
||||
import { SortableHeader } from '../../components/Table/SortableHeader.jsx';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
|
||||
export default function ViewBranchQueues() {
|
||||
const [selectedBranch, setSelectedBranch] = useState(null);
|
||||
const [selectedService, setSelectedService] = useState(null);
|
||||
const [branches, setBranches] = useState([]);
|
||||
const [services, setServices] = useState([]);
|
||||
const [queue, setQueue] = useState([]);
|
||||
const { tenantCode } = useParams();
|
||||
const [ startTime, setStartTime ] = useState({ hour: 0, minutes: 0});
|
||||
const [ endTime, setEndTime ] = useState({ hour: 23, minutes: 59 });
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(`${ url }branches/${ tenantCode }`, 'GET')
|
||||
.then(response => setBranches(response.data))
|
||||
.catch(error => console.error('Error fetching branches:', error));
|
||||
}, [])
|
||||
|
||||
useInterval(() => {
|
||||
if (selectedBranch) {
|
||||
fetchQueuesForBranch(selectedBranch, selectedService);
|
||||
}
|
||||
}, 10000, [selectedBranch, selectedService])
|
||||
|
||||
async function fetchQueuesForBranch(branch, service) {
|
||||
try {
|
||||
let baseUrl = `${ url }branches/${ tenantCode }/${ branch.id }/queue`;
|
||||
|
||||
const sort = parseSortFromUrl(window.location.href);
|
||||
if (sort) {
|
||||
baseUrl = addSortToUrl(sort, baseUrl);
|
||||
}
|
||||
|
||||
if (service) {
|
||||
baseUrl = addSimpleParamToUrl('serviceId', service.id, baseUrl);
|
||||
}
|
||||
|
||||
const startInstant = todayWithTime(startTime.hour, startTime.minutes);
|
||||
const endInstant = todayWithTime(endTime.hour, endTime.minutes);
|
||||
|
||||
baseUrl = addSimpleParamToUrl('createdAfter', startInstant.toISOString(), baseUrl);
|
||||
baseUrl = addSimpleParamToUrl('createdBefore', endInstant.toISOString(), baseUrl);
|
||||
|
||||
const response = await fetchData(baseUrl, 'GET');
|
||||
|
||||
if (response.success) {
|
||||
setQueue(response.data);
|
||||
} else {
|
||||
console.error('Error fetching queue:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching queue:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchServicesForBranch(branch) {
|
||||
try {
|
||||
const response = await fetchData(`${ url }branches/${ tenantCode }/${ branch.id }/services`, 'GET');
|
||||
if (response.success) {
|
||||
setServices(response.data);
|
||||
} else {
|
||||
console.error('Error fetching services:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching services:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function onServiceChange(service) {
|
||||
setSelectedService(service);
|
||||
await fetchQueuesForBranch(selectedBranch, service);
|
||||
}
|
||||
|
||||
async function onBranchChange(branch) {
|
||||
setSelectedService(null);
|
||||
setSelectedBranch(branch);
|
||||
await fetchQueuesForBranch(branch, null);
|
||||
await fetchServicesForBranch(branch);
|
||||
}
|
||||
|
||||
function onStartTimeChange(time) {
|
||||
setStartTime(time);
|
||||
}
|
||||
|
||||
function onEndTimeChange(time) {
|
||||
setEndTime(time);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Queue</h2>
|
||||
<DropdownButton title={ selectedBranch ? selectedBranch.name : 'Select Branch' }
|
||||
variant="btn btn-outline-secondary">
|
||||
{ branches.map((branch, index) => (
|
||||
<Dropdown.Item key={ index }
|
||||
onClick={ () => onBranchChange(branch) }>
|
||||
{ branch.name }
|
||||
</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
|
||||
<div className="mx-auto d-inline">
|
||||
<DropdownButton style={ { marginTop: '20px' } }
|
||||
title={ selectedService ? selectedService.name : 'Select Service' }
|
||||
variant="btn btn-outline-secondary">
|
||||
{ services.map((service, index) => (
|
||||
<Dropdown.Item key={ index }
|
||||
onClick={ () => onServiceChange(service) }>
|
||||
{ service.name }
|
||||
</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
</div>
|
||||
|
||||
<div className="d-flex justify-content-end gap-3 me-5">
|
||||
<TimePicker title="Start time"
|
||||
onChange={ onStartTimeChange }
|
||||
defaultHour="0"
|
||||
defaultMinute="0" />
|
||||
<TimePicker title="End time"
|
||||
onChange={ onEndTimeChange }
|
||||
defaultHour="23"
|
||||
defaultMinute="59" />
|
||||
<Button onClick={ () => fetchQueuesForBranch(selectedBranch, selectedService) }
|
||||
style={{
|
||||
height: 'fit-content',
|
||||
alignSelf: 'end',
|
||||
backgroundColor: 'var(--light-blue)',
|
||||
padding: '6px 20px'
|
||||
}}>
|
||||
Filter
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div style={ { marginTop: '20px' } }>
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<SortContextProvider onSort={ () => fetchQueuesForBranch(selectedBranch, selectedService) }>
|
||||
<tr>
|
||||
<SortableHeader columnName="number">
|
||||
Ticket Number
|
||||
</SortableHeader>
|
||||
|
||||
<SortableHeader columnName="serviceId">
|
||||
Service
|
||||
</SortableHeader>
|
||||
|
||||
<SortableHeader columnName="createdAt">
|
||||
Created At
|
||||
</SortableHeader>
|
||||
|
||||
<SortableHeader columnName="createdAt">
|
||||
Elapsed Time
|
||||
</SortableHeader>
|
||||
</tr>
|
||||
</SortContextProvider>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{ queue.map((ticket, index) => {
|
||||
const createdAt = new Date(ticket.createdAt);
|
||||
const currentTime = getToday();
|
||||
const elapsedTime = getDifference(currentTime, createdAt, 'minutes');
|
||||
|
||||
return (
|
||||
<tr key={ `${ index }-${ ticket.id }` }>
|
||||
<td>{ ticket.number }</td>
|
||||
<td>{ ticket.service.name }</td>
|
||||
<td>{ formatDate(ticket.createdAt) }</td>
|
||||
<td>{ elapsedTime } minutes</td>
|
||||
</tr>
|
||||
);
|
||||
}) }
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
function parseDate(date) {
|
||||
return new Date(Date.parse(date))
|
||||
}
|
||||
|
||||
function format(date, options) {
|
||||
return date && date.toLocaleString('en-GB', { ...options })
|
||||
}
|
||||
|
||||
function getDate(date) {
|
||||
return date instanceof Date ? date : parseDate(date);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param date - either a string, or a Date object to be formatted
|
||||
* @returns {*} - a string representing the date in the format: 14 Apr 2023, 13:13 (for example)
|
||||
*/
|
||||
export function formatDate(date) {
|
||||
return format(getDate(date), {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the time from a date
|
||||
* @param date - either a string, or a Date object to be formatted
|
||||
* @returns {*} - a string representation of the date in the format: 19:31 (for example)
|
||||
*/
|
||||
export function formatTime(date) {
|
||||
return format(getDate(date), {
|
||||
hour: 'numeric',
|
||||
minute: '2-digit'
|
||||
})
|
||||
}
|
||||
|
||||
export function getToday() {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
export function todayWithTime(hour, minute) {
|
||||
const today = getToday();
|
||||
today.setHours(hour, minute);
|
||||
|
||||
return today;
|
||||
}
|
||||
|
||||
export function getDifference(date1, date2, unit = 'minutes') {
|
||||
const UNIT_FACTOR = {
|
||||
seconds: 1,
|
||||
minutes: 60,
|
||||
hours: 60 * 60
|
||||
}
|
||||
|
||||
return Math.floor(Math.abs(date1 - date2) / (1000 * UNIT_FACTOR[unit]));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Used for appending simple (of primitive types - string, int etc.) query params to url string
|
||||
* @param name{string} - name of the query parameter
|
||||
* @param value{string} - value of the query parameter
|
||||
* @param url{string} - url to which we are appending the query parameter
|
||||
* @returns {string} - url with the query parameter appended
|
||||
*/
|
||||
export function addSimpleParamToUrl(name, value, url) {
|
||||
if (url.includes('?')) {
|
||||
// There are already url parameters present - we should add ours with '&'
|
||||
return `${url}&${name}=${value}`;
|
||||
} else {
|
||||
// There are no params before ours, we can add it with '?'
|
||||
return `${url}?${name}=${value}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param params - an array of objects in format { name: paramName, value: paramValue }. For example [{ name: 'sort', value: 'id' }]
|
||||
* @param url{string} - url to which we are appending the query parameters
|
||||
* @returns {string} - url with the query parameter appended
|
||||
*/
|
||||
export function addArrayToUrl(params, url) {
|
||||
return params.reduce((prev, item, index) => {
|
||||
if (index === 0) {
|
||||
return `${prev}?${item.name}=${item.value}`;
|
||||
} else {
|
||||
return `${prev}&${item.name}=${item.value}`;
|
||||
}
|
||||
}, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param sort{string} - sort object represented as a string in format name,direction, for example id,asc
|
||||
* @param url{string} - url to which we are appending the sort parameter
|
||||
* @returns {string} - url with the sort param appended
|
||||
*/
|
||||
export function addSortToUrl(sort, url) {
|
||||
return addSimpleParamToUrl('sort', sort, url);
|
||||
}
|
||||
|
||||
export function parseSortFromUrl(url) {
|
||||
const searchParams = url.split('?')[1];
|
||||
|
||||
if (searchParams) {
|
||||
const sortParam = searchParams.split('&').find(param => param.split('=')[0] === 'sort');
|
||||
return decodeURIComponent(sortParam.split('=')[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* @param classNames{} - an array of css classnames to concatenate. Even null/undefined are allowed (they will simply not be serialized into the final css class)
|
||||
* @returns {string} - resulting css class, conforming to css standard
|
||||
*/
|
||||
export function createClassName(classNames) {
|
||||
return classNames.reduce((prev, className) => {
|
||||
if (!className || className.trim() === 0) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return prev + (prev.length > 0 ? ' ' : '') + className.trim()
|
||||
}, '');
|
||||
}
|
||||
|
||||
export function lastPathPart(path) {
|
||||
if (path) {
|
||||
const splitPath = path.split('/')
|
||||
return splitPath[splitPath.length - 1]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM node:21-alpine AS build
|
||||
WORKDIR /teller-app
|
||||
COPY package*.json .
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "run", "preview"]
|
||||
@@ -0,0 +1,7 @@
|
||||
## Build instructions
|
||||
|
||||
- cd **teller-app**
|
||||
- npm **install**
|
||||
- npm **run dev**
|
||||
|
||||
#### **NOTE:** requires Node 21
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BBQMS Teller App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+4493
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "teller-app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --port 3000",
|
||||
"build": "vite build",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^5.3.3",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^2.10.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.22.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.64",
|
||||
"@types/react-dom": "^18.2.21",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.34.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"vite": "^5.1.6"
|
||||
}
|
||||
}
|
||||
Generated
+3373
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import StationIntroPage from './pages/StationIntroPage/StationIntroPage.jsx';
|
||||
import ShowQueuesForTellerPage from './pages/ShowQueuesForTellerPage/ShowQueuesForTellerPage';
|
||||
import Header from './components/Header/Header.jsx';
|
||||
import CurrentTicketPage from './pages/CurrentTicketPage/CurrentTicketPage.jsx';
|
||||
|
||||
export default function App() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route exact path='/' element={<StationIntroPage />} />
|
||||
<Route exact path="/teller-queue/:stationId" element={<ShowQueuesForTellerPage />} />
|
||||
<Route path="/display/:stationId" element={<CurrentTicketPage/>} />
|
||||
</Routes>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
export default function ExampleButton({ text, onClick }) {
|
||||
return (
|
||||
<button onClick={() => onClick()}>
|
||||
{ text }
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.centered {
|
||||
text-align: center;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import React from "react";
|
||||
|
||||
import './ExampleComponent.css';
|
||||
|
||||
export default function ExampleComponent() {
|
||||
return (
|
||||
<div className="centered">This is a component</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
header.main-header {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin: 0 0 3% -2%;
|
||||
background-color: #d3e2f8;
|
||||
width: 101vw;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-top: 7px;
|
||||
margin-right: auto;
|
||||
margin-left: 2%;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import './Header.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<header className="main-header">
|
||||
<h2 className="header-logo" onClick={() => navigate(`/`)}>BBQMS</h2>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export const SERVER_URL = 'http://localhost:8080';
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Koristiti ovu funkciju za fetchanje u buducnosti kad god je to moguce.
|
||||
*/
|
||||
export async function fetchData(url, method, body) {
|
||||
const headers = new Headers();
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers.append('Authorization', `Bearer ${ token }`);
|
||||
}
|
||||
|
||||
headers.append('Content-Type', 'application/json');
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method || 'GET',
|
||||
headers: headers,
|
||||
body: body ? JSON.stringify(body) : null
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const data = res.ok && res.body ? await res.json() : null;
|
||||
|
||||
if (res.ok) {
|
||||
//na svaki ispravan rezultat treba da dobijemo novi token da refreshamo stari
|
||||
const newToken = res.headers.get('Auth-Token');
|
||||
if (newToken) {
|
||||
localStorage.setItem('token', newToken);
|
||||
}
|
||||
}
|
||||
|
||||
return { data: data, success: res.ok };
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* A hook for using the native JS interval API. The interval is released after the user component dismounts.
|
||||
* @param callback function to be executed
|
||||
* @param period interval between executions of the callback function. The first execution happens instantly.
|
||||
*/
|
||||
export function useInterval(callback, period) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => callback(), period)
|
||||
return () => clearInterval(interval);
|
||||
}, [ period ]);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'General Sans', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0 20px;
|
||||
background-color: ghostwhite;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* ovdje definisite konstante boje i sl. koje cete koristiti na vise mjesta */
|
||||
--blue: #334257;
|
||||
--light-blue: #548CA8;
|
||||
--dark-blue: #476072;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom/client'
|
||||
import App from './App.jsx'
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
|
||||
import './index.css'
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,3 @@
|
||||
.message{
|
||||
font-size: 1000%;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useInterval } from '../../hooks/hooks.jsx';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import '../CurrentTicketPage/CurrentTicketPage.css';
|
||||
|
||||
export default function CurrentTicketPage() {
|
||||
const [currentTicket, setCurrentTicket] = useState('');
|
||||
const { stationId } = useParams();
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentTicket();
|
||||
}, [stationId]);
|
||||
|
||||
useInterval(() => fetchCurrentTicket(), 3000);
|
||||
|
||||
const fetchCurrentTicket = async () => {
|
||||
try {
|
||||
const response = await fetchData(`${SERVER_URL}/api/v1/teller/current-ticket/${stationId}`, 'GET');
|
||||
if (response.success) {
|
||||
if (typeof response.data === 'object' && response.data.message) {
|
||||
setCurrentTicket(response.data.message);
|
||||
} else {
|
||||
setCurrentTicket(response.data.number);
|
||||
}
|
||||
} else {
|
||||
setCurrentTicket('There is no current ticket for this station');
|
||||
console.error('Error fetching current ticket:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
setCurrentTicket('There is no current ticket for this station');
|
||||
console.log("Error fetching current ticket:", error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<h1 className="message">{currentTicket}</h1>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.center {
|
||||
text-align: center;
|
||||
margin-top: 2%;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { useState } from "react";
|
||||
import ExampleComponent from "../../components/ExampleComponent/ExampleComponent";
|
||||
import ExampleButton from "../../components/ExampleButton/ExampleButton";
|
||||
|
||||
import './Home.css'
|
||||
import { SERVER_URL } from "../../constants";
|
||||
|
||||
export default function Home() {
|
||||
const [message, setMessage] = useState('');
|
||||
|
||||
async function getMessageFromBackend() {
|
||||
const response = await fetch(SERVER_URL + '/api/v1/teller');
|
||||
const body = await response.json();
|
||||
|
||||
setMessage('Got from backend: ' + body.text);
|
||||
}
|
||||
|
||||
return (
|
||||
<main>
|
||||
<ExampleComponent />
|
||||
<div className="center">
|
||||
<ExampleButton text={ "Click to get message from backend" } onClick={ getMessageFromBackend }/>
|
||||
<p>{ message }</p>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { useInterval } from '../../hooks/hooks.jsx';
|
||||
import { fetchData } from '../../fetching/Fetch.js'
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
|
||||
const styles = {
|
||||
stationRow: {
|
||||
backgroundColor: "#ADD8E6",
|
||||
}
|
||||
};
|
||||
|
||||
export default function ShowQueuesForTellerPage() {
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
const [ tickets, setTickets ] = useState([]);
|
||||
const [ usedUndo, setUsedUndo ] = useState(false)
|
||||
const [ timedOut, setTimedOut ] = useState(false)
|
||||
|
||||
const { stationId} = useParams()
|
||||
|
||||
useEffect(() => {
|
||||
fetchTicketsForTellerStation();
|
||||
}, [ stationId ]);
|
||||
|
||||
useInterval(fetchTicketsForTellerStation, 10000)
|
||||
|
||||
function fetchTicketsForTellerStation() {
|
||||
fetchData(`${ url }stations/${ stationId }/tickets`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setTickets)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function advanceQueue() {
|
||||
fetchData(`${ url }teller/advance-queue/${ stationId }`, 'POST')
|
||||
.then(fetchTicketsForTellerStation)
|
||||
.then(() => setUsedUndo(false))
|
||||
.then(timeOutTeller)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function undoQueue() {
|
||||
fetchData(`${ url }teller/undo-queue/${ stationId }`, 'POST')
|
||||
.then(fetchTicketsForTellerStation)
|
||||
.then(() => setUsedUndo(true))
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function timeOutTeller() {
|
||||
setTimedOut(true)
|
||||
setTimeout(() => setTimedOut(false), 5000)
|
||||
// Can't advance more often than every 5 seconds.
|
||||
}
|
||||
|
||||
const hasAssignedTicket = tickets.some(ticket => ticket.station?.id == stationId)
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<h1>Queues</h1>
|
||||
<div className="d-flex gap-2 justify-content-center">
|
||||
<Button
|
||||
variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ advanceQueue }
|
||||
disabled={ timedOut }
|
||||
>
|
||||
Advance Queue
|
||||
</Button>
|
||||
<Button variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ undoQueue }
|
||||
disabled={ usedUndo || !hasAssignedTicket }>
|
||||
Undo
|
||||
</Button>
|
||||
<Button variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ fetchTicketsForTellerStation }>
|
||||
Refresh
|
||||
</Button>
|
||||
<Link to={`/display/${stationId}`} target="_blank" rel="noopener noreferrer">
|
||||
<Button
|
||||
variant="secondary"
|
||||
>
|
||||
Current Ticket
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
<div style={ { marginTop: '20px' } }>
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ticket number</th>
|
||||
<th>Service</th>
|
||||
<th>Creation at</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tickets.map((ticket, index) => {
|
||||
const createdAt = new Date(ticket.createdAt);
|
||||
return (
|
||||
<tr key={index}>
|
||||
<td style={ticket.station?.id == stationId ? styles.stationRow : {}}>{ticket.number}</td>
|
||||
<td>{ticket.service.name}</td>
|
||||
<td>{createdAt.toLocaleString()}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Dropdown, Button } from 'react-bootstrap';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const StationIntroPage = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [branches, setBranches] = useState([]);
|
||||
const [selectedBranch, setSelectedBranch] = useState(null);
|
||||
const [stations, setStations] = useState([]);
|
||||
const [selectedStation, setSelectedStation] = useState(null);
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(`${ url }branches/DFLT`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setBranches)
|
||||
.catch(console.error)
|
||||
}, []);
|
||||
|
||||
function handleBranchSelect(branch) {
|
||||
setSelectedBranch(branch)
|
||||
fetchData(`${ url }stations/DFLT/${ branch.id }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setStations)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function handleStationSelect(station) {
|
||||
navigate(`/teller-queue/${ station.id }`)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<div className="border p-3" style={{ maxWidth: '500px', margin: '0 auto' }}>
|
||||
<h1 className="mb-4">Select Branch and Station</h1>
|
||||
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="primary" id="dropdown-branch" style={{ width: '100%' }}>
|
||||
{selectedBranch ? selectedBranch.name : 'Select Branch'}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu style={{ width: '100%' }}>
|
||||
{branches.map(branch => (
|
||||
<Dropdown.Item key={branch.id} onClick={() => handleBranchSelect(branch)}>
|
||||
{branch.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
|
||||
{selectedBranch && (
|
||||
<div className="mt-3">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="primary" id="dropdown-station" style={{ width: '100%' }}>
|
||||
{selectedStation ? selectedStation.name : 'Select Station'}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu style={{ width: '100%' }}>
|
||||
{stations.map(station => (
|
||||
<Dropdown.Item key={station.id} onClick={() => handleStationSelect(station)}>
|
||||
{station.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StationIntroPage;
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
})
|
||||
Reference in New Issue
Block a user