DONE: first init

This commit is contained in:
ISMAIL MASSERAN
2026-07-20 16:10:22 +08:00
commit 3189e3a1e3
272 changed files with 48853 additions and 0 deletions
+21
View File
@@ -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 },
],
},
}
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+10
View File
@@ -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"]
+7
View File
@@ -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

+14
View File
@@ -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>
+4803
View File
File diff suppressed because it is too large Load Diff
+36
View File
@@ -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"
}
}
+3645
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
.background-color-app{
background-color: whitesmoke;
}
+200
View File
@@ -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 } />
)
}
+6
View File
@@ -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();
+35
View File
@@ -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 };
}
+14
View File
@@ -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) };
}
+22
View File
@@ -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;
}
+17
View File
@@ -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>
);
}
+59
View File
@@ -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]);
}
}
+20
View File
@@ -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]
}
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
+21
View File
@@ -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 },
],
},
}
+24
View File
@@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
+10
View File
@@ -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"]
+7
View File
@@ -0,0 +1,7 @@
## Build instructions
- cd **teller-app**
- npm **install**
- npm **run dev**
#### **NOTE:** requires Node 21
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>BBQMS Teller App</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+4493
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -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"
}
}
+3373
View File
File diff suppressed because it is too large Load Diff
+20
View File
@@ -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>
);
}
+1
View File
@@ -0,0 +1 @@
export const SERVER_URL = 'http://localhost:8080';
+35
View File
@@ -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 };
}
+13
View File
@@ -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 ]);
}
+22
View File
@@ -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;
}
+14
View File
@@ -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%;
}
+28
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})