DONE: first init
@@ -0,0 +1,35 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
|
||||
# Native
|
||||
*.orig.*
|
||||
*.jks
|
||||
*.p8
|
||||
*.p12
|
||||
*.key
|
||||
*.mobileprovision
|
||||
|
||||
# Metro
|
||||
.metro-health-check*
|
||||
|
||||
# debug
|
||||
npm-debug.*
|
||||
yarn-debug.*
|
||||
yarn-error.*
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
@@ -0,0 +1,12 @@
|
||||
# Build instructions
|
||||
```
|
||||
cd mobile-app
|
||||
```
|
||||
```
|
||||
npm install
|
||||
```
|
||||
```
|
||||
npx expo start
|
||||
```
|
||||
### - open Expo Go application
|
||||
### - scan QR code displayed in terminal
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "mobile-app",
|
||||
"slug": "mobile-app",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/icon.png",
|
||||
"userInterfaceStyle": "light",
|
||||
"splash": {
|
||||
"image": "./assets/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"assetBundlePatterns": [
|
||||
"**/*"
|
||||
],
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"permissions": [
|
||||
"READ_EXTERNAL_STORAGE",
|
||||
"WRITE_EXTERNAL_STORAGE"
|
||||
],
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"package": "com.emacaga.mobileapp"
|
||||
},
|
||||
"web": {
|
||||
"favicon": "./assets/favicon.png"
|
||||
},
|
||||
"extra": {
|
||||
"eas": {
|
||||
"projectId": "2a75751f-3867-4465-8c1d-6832a25c2b44"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 46 KiB |
@@ -0,0 +1,6 @@
|
||||
module.exports = function(api) {
|
||||
api.cache(true);
|
||||
return {
|
||||
presets: ['babel-preset-expo'],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"build": {
|
||||
"preview": {
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
}
|
||||
},
|
||||
"preview2": {
|
||||
"android": {
|
||||
"gradleCommand": ":app:assembleRelease"
|
||||
}
|
||||
},
|
||||
"preview3": {
|
||||
"developmentClient": true
|
||||
},
|
||||
"preview4": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "mobile-app",
|
||||
"version": "1.0.0",
|
||||
"main": "./src/App.tsx",
|
||||
"scripts": {
|
||||
"ts:check": "tsc",
|
||||
"start": "expo start",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/metro-runtime": "~3.1.3",
|
||||
"@react-native-async-storage/async-storage": "^1.23.1",
|
||||
"@react-native-community/masked-view": "^0.1.11",
|
||||
"@react-navigation/bottom-tabs": "^6.5.20",
|
||||
"@react-navigation/native": "^6.1.17",
|
||||
"@react-navigation/stack": "^6.3.29",
|
||||
"@testing-library/react": "^14.2.2",
|
||||
"@testing-library/react-native": "^12.4.4",
|
||||
"buffer": "^6.0.3",
|
||||
"expo": "~50.0.14",
|
||||
"expo-file-system": "^16.0.9",
|
||||
"expo-font": "^11.10.3",
|
||||
"expo-sharing": "^11.10.0",
|
||||
"expo-splash-screen": "^0.26.4",
|
||||
"expo-status-bar": "~1.11.1",
|
||||
"jest-fetch-mock": "^3.0.3",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-native": "0.73.6",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "~2.14.0",
|
||||
"react-native-reanimated": "^3.6.2",
|
||||
"react-native-safe-area-context": "^4.8.2",
|
||||
"react-native-screens": "^3.29.0",
|
||||
"react-native-web": "~0.19.6",
|
||||
"react-navigation": "^5.0.0",
|
||||
"rn-fetch-blob": "^0.12.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.0",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "~18.2.45",
|
||||
"jest": "^29.7.0",
|
||||
"typescript": "^5.4.3"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { registerRootComponent } from "expo";
|
||||
import AppNavigator from "./navigation/AppNavigator";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
|
||||
export default function App() {
|
||||
AsyncStorage.clear();
|
||||
return AppNavigator();
|
||||
}
|
||||
|
||||
registerRootComponent(App);
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { View, Modal, Text, Button, StyleSheet, Dimensions } from 'react-native';
|
||||
import { Colors } from '../constants/colors';
|
||||
import { generateTicket } from '../services/fetchData';
|
||||
import { getExpoToken } from '../utils/tokenUtils';
|
||||
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
const windowHeight = Dimensions.get('window').height;
|
||||
|
||||
export default function ({ visible, onClose, number }: { visible: any, onClose: any, number: number}) {
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(visible);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setModalVisible(visible);
|
||||
if (visible) {
|
||||
const timer = setTimeout(() => {
|
||||
setModalVisible(false);
|
||||
onClose();
|
||||
}, 5000);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
if(number != 0 )
|
||||
return (
|
||||
<Modal
|
||||
animationType="slide"
|
||||
transparent={true}
|
||||
visible={modalVisible}
|
||||
onRequestClose={() => {
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<View style={styles.modalContainer}>
|
||||
<View style={styles.modalContent}>
|
||||
<Text style={styles.message}>Vaš broj je:</Text>
|
||||
<View style={styles.numberContainer}>
|
||||
<Text style={styles.number}>{number}</Text>
|
||||
</View>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Button title="Dismiss" onPress={onClose} color={Colors.ACCENT} />
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
modalContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.5)', // Semi-transparent background
|
||||
},
|
||||
modalContent: {
|
||||
backgroundColor: 'white',
|
||||
padding: windowWidth * 0.05, // Responsive padding
|
||||
borderRadius: windowWidth * 0.05, // Responsive border radius
|
||||
width: windowWidth * 0.8, // Responsive width
|
||||
alignItems: 'center', // Center content horizontally
|
||||
},
|
||||
message: {
|
||||
fontSize: windowWidth * 0.05, // Responsive font size
|
||||
marginBottom: windowHeight * 0.02, // Responsive margin
|
||||
},
|
||||
numberContainer: {
|
||||
marginBottom: windowHeight * 0.04, // Responsive margin
|
||||
},
|
||||
number: {
|
||||
fontSize: windowWidth * 0.12, // Responsive font size
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
buttonContainer: {
|
||||
marginTop: windowHeight * 0.02, // Responsive margin
|
||||
alignSelf: 'flex-end', // Align button to the right
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { View } from "react-native";
|
||||
import React from "react";
|
||||
import LottieView from "lottie-react-native";
|
||||
|
||||
export default function LoadingAnimation() {
|
||||
return (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<LottieView
|
||||
source={require("../../assets/animations/loading.json")}
|
||||
autoPlay
|
||||
loop
|
||||
style={{ width: 200, height: 200 }}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import React from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
SafeAreaView,
|
||||
Dimensions,
|
||||
Animated,
|
||||
} from "react-native";
|
||||
import { Colors } from "../constants/colors";
|
||||
|
||||
interface WelcomeMessageProps {
|
||||
name: string;
|
||||
welcome: string;
|
||||
font: string;
|
||||
}
|
||||
|
||||
export default function WelcomeMessage(params: WelcomeMessageProps) {
|
||||
const screenWidth = Dimensions.get("window").width;
|
||||
const fontSize = screenWidth * 0.09;
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<Animated.Text style={[styles.title, { fontSize }]}>
|
||||
{params.name}
|
||||
</Animated.Text>
|
||||
<View style={styles.borderContainer}>
|
||||
<Text style={[styles.message, { fontFamily: params.font }]}>
|
||||
{params.welcome}
|
||||
</Text>
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: "center",
|
||||
},
|
||||
title: {
|
||||
color: Colors.PRIMARY,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 30,
|
||||
},
|
||||
borderContainer: {
|
||||
borderWidth: 1,
|
||||
padding: 10,
|
||||
width: "90%",
|
||||
alignItems: "center",
|
||||
borderColor: Colors.ACCENT,
|
||||
borderRadius: 10,
|
||||
},
|
||||
message: {
|
||||
textAlign: "center",
|
||||
fontSize: 40,
|
||||
color: Colors.PRIMARY,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
//export const SERVER_URL = 'http://localhost:8080';
|
||||
//export const SERVER_URL = 'http://192.168.1.5:8080';
|
||||
export const SERVER_URL = 'http://localhost:8080';
|
||||
@@ -0,0 +1,13 @@
|
||||
export const Assets = {
|
||||
DEFAULT_LOGO: require("../../assets/images/default_logo.jpg"),
|
||||
ARIAL: require("../../assets/fonts/arial.ttf"),
|
||||
HELVETICA: require("../../assets/fonts/helvetica.ttf"),
|
||||
TIMES_NEW_ROMAN: require("../../assets/fonts/times-new-roman.ttf"),
|
||||
FUTURA: require("../../assets/fonts/futura.ttf"),
|
||||
MONTSERRAT: require("../../assets/fonts/montserrat.ttf"),
|
||||
ROCKWELL: require("../../assets/fonts/rockwell.ttf"),
|
||||
VERDANA: require("../../assets/fonts/verdana.ttf"),
|
||||
CALIBRI: require("../../assets/fonts/calibri.ttf"),
|
||||
BODONI: require("../../assets/fonts/bodoni.ttf"),
|
||||
COMIC_SANS_MS: require("../../assets/fonts/comic-sans-ms.ttf"),
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export const Colors = {
|
||||
PRIMARY : "#334257",
|
||||
SECONDARY : "#476072",
|
||||
ACCENT : "#548CA8",
|
||||
BACKGROUND : "#EEEEEE",
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export const Dialogs = {
|
||||
BUTTON: {
|
||||
OK: "OK",
|
||||
CANCEL: "Cancel",
|
||||
SUBMIT: "Submit",
|
||||
},
|
||||
ERROR: {
|
||||
INVALID_CODE: "Invalid Code",
|
||||
INVALID_CODE_DESC: "The code you entered is invalid. Please try again.",
|
||||
},
|
||||
PROMPT: {
|
||||
ENTER_CODE: "Enter your company code here:",
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,14 @@
|
||||
export const Fonts = {
|
||||
DEFAULT_FONT: 'Arial',
|
||||
ARIAL : 'Arial',
|
||||
COMIC_SANS_MS : 'Comic Sans MS',
|
||||
HELVETICA : 'Helvetica',
|
||||
TIMES_NEW_ROMAN : 'Times New Roman',
|
||||
FUTURA : 'Futura',
|
||||
MONTSERRAT : 'Montserrat',
|
||||
ROCKWELL : 'Rockwell',
|
||||
VERDANA : 'Verdana',
|
||||
CALIBRI : 'Calibri',
|
||||
BODONI: 'Bodoni',
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export const Screens = {
|
||||
WELCOME : "WelcomeScreen",
|
||||
CODE_ENTER : "CodeEnterScreen",
|
||||
BRANCH_PICK : "BranchPickScreen",
|
||||
ASSIGNED_NUMBER:"AssignedNumberAlert",
|
||||
TICKET_INFO:"TicketInfoScreen",
|
||||
TICKET_LIST:"TicketListScreen",
|
||||
BOTTOM_NAV:"BottomNavigator"
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import React from "react";
|
||||
import { NavigationContainer } from "@react-navigation/native";
|
||||
import { createStackNavigator } from "@react-navigation/stack";
|
||||
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import CodeEnterScreen from "../screens/CodeEnterScreen";
|
||||
import WelcomeScreen from "../screens/WelcomeScreen";
|
||||
import BranchPickScreen from "../screens/BranchPickScreen"
|
||||
import TicketListScreen from '../screens/TicketListScreen';
|
||||
import TicketInfoScreen from '../screens/TicketInfoScreen'
|
||||
import { Screens } from "../constants/screens";
|
||||
import AssignedNumberAlert from "../components/AssignedNumberAlert";
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
|
||||
const Stack = createStackNavigator();
|
||||
const Tab = createBottomTabNavigator();
|
||||
|
||||
const BottomNavigator = ({ route }) => {
|
||||
|
||||
const { details, services, branchID } = route.params;
|
||||
return (
|
||||
<Tab.Navigator screenOptions={{headerShown: false }}>
|
||||
<Tab.Screen
|
||||
name="Home"
|
||||
component={WelcomeScreen}
|
||||
initialParams={{ details, services , branchID }}
|
||||
options={{
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="home-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Tab.Screen
|
||||
name="Moji tiketi"
|
||||
component={TicketListScreen}
|
||||
options={{
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<Ionicons name="list-outline" size={size} color={color} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Tab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
const AppNavigator = () => {
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator
|
||||
initialRouteName={Screens.CODE_ENTER}
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
>
|
||||
<Stack.Screen
|
||||
name={Screens.CODE_ENTER}
|
||||
component={CodeEnterScreen}
|
||||
/>
|
||||
<Stack.Screen name={Screens.BRANCH_PICK} component={BranchPickScreen} />
|
||||
<Stack.Screen name={Screens.WELCOME} component={WelcomeScreen} />
|
||||
<Stack.Screen name={Screens.ASSIGNED_NUMBER} component={AssignedNumberAlert} />
|
||||
<Stack.Screen name={Screens.TICKET_INFO} component={TicketInfoScreen} />
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppNavigator;
|
||||
@@ -0,0 +1,137 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
Dimensions,
|
||||
} from "react-native";
|
||||
import { Screens } from "../constants/screens";
|
||||
import { getBranchServices } from "../services/fetchData";
|
||||
import { Colors } from "../constants/colors";
|
||||
import { Fonts } from "../constants/fonts";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { useFonts } from "expo-font";
|
||||
import { Assets } from "../constants/assets";
|
||||
|
||||
const windowWidth = Dimensions.get("window").width;
|
||||
const windowHeight = Dimensions.get("window").height;
|
||||
|
||||
export default function BranchPickScreen({
|
||||
route,
|
||||
navigation,
|
||||
}: {
|
||||
route: any;
|
||||
navigation: any;
|
||||
}) {
|
||||
const { branches } = route.params;
|
||||
|
||||
let [fontsLoaded] = useFonts({
|
||||
[Fonts.ARIAL]: Assets.ARIAL,
|
||||
[Fonts.TIMES_NEW_ROMAN]: Assets.TIMES_NEW_ROMAN,
|
||||
[Fonts.VERDANA]: Assets.VERDANA,
|
||||
[Fonts.HELVETICA]: Assets.HELVETICA,
|
||||
[Fonts.MONTSERRAT]: Assets.MONTSERRAT,
|
||||
[Fonts.CALIBRI]: Assets.CALIBRI,
|
||||
[Fonts.FUTURA]: Assets.FUTURA,
|
||||
[Fonts.BODONI]: Assets.BODONI,
|
||||
[Fonts.ROCKWELL]: Assets.ROCKWELL,
|
||||
[Fonts.COMIC_SANS_MS]: Assets.COMIC_SANS_MS,
|
||||
});
|
||||
|
||||
const renderItem = ({ item }: { item: any }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.branchItem}
|
||||
onPress={() => handlePress(item.id)}
|
||||
>
|
||||
<Text style={styles.branchName}>{item.name}</Text>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons
|
||||
name="chevron-forward-circle-outline"
|
||||
size={windowWidth * 0.1}
|
||||
color={Colors.ACCENT}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
async function handlePress(id: any) {
|
||||
getBranchServices(route.params.code, id)
|
||||
.then(function (services) {
|
||||
navigation.navigate(Screens.WELCOME, {
|
||||
details: route.params.details,
|
||||
services: services,
|
||||
branchID: id,
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
});
|
||||
}
|
||||
if (!fontsLoaded) return <Text>Loading...</Text>;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={[styles.title]}>Poslovnice</Text>
|
||||
<FlatList
|
||||
data={branches}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={styles.flatListContainer}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.BACKGROUND,
|
||||
justifyContent: "center",
|
||||
paddingTop: windowHeight * 0.1,
|
||||
},
|
||||
title: {
|
||||
fontSize: windowWidth * 0.1,
|
||||
fontWeight: "bold",
|
||||
marginBottom: windowHeight * 0.03,
|
||||
marginLeft: windowWidth * 0.05,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
},
|
||||
flatListContainer: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: windowWidth * 0.05,
|
||||
paddingBottom: windowHeight * 0.05,
|
||||
},
|
||||
branchItem: {
|
||||
paddingHorizontal: windowWidth * 0.05,
|
||||
paddingVertical: windowHeight * 0.06, // Adjust the padding to make the items taller
|
||||
marginBottom: windowHeight * 0.02,
|
||||
borderRadius: windowWidth * 0.05,
|
||||
backgroundColor: "#fff",
|
||||
shadowColor: Colors.ACCENT,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
branchContent: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
branchName: {
|
||||
flex: 1,
|
||||
fontSize: windowWidth * 0.08,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
textAlign: "center",
|
||||
},
|
||||
icon: {
|
||||
marginLeft: windowWidth * 0.02,
|
||||
},
|
||||
iconContainer: {
|
||||
alignItems: "center",
|
||||
marginTop: windowHeight * 0.02, // Adjust the margin to position the icon below the branch name
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useState } from "react";
|
||||
import {
|
||||
SafeAreaView,
|
||||
StyleSheet,
|
||||
TextInput,
|
||||
Pressable,
|
||||
Text,
|
||||
Alert,
|
||||
ActivityIndicator,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { registerRootComponent } from "expo";
|
||||
import { getCompanyBranches, getCompanyDetails } from "../services/fetchData";
|
||||
import { Dialogs } from "../constants/dialogs";
|
||||
import { Colors } from "../constants/colors";
|
||||
import { Screens } from "../constants/screens";
|
||||
import { Dimensions } from 'react-native';
|
||||
|
||||
const screenWidth = Dimensions.get('window').width;
|
||||
const buttonWidth = screenWidth * 0.5;
|
||||
|
||||
export default function CodeEnterScreen({ navigation }: { navigation: any }) {
|
||||
const [text, onChangeText] = React.useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
||||
function displayInvalidCodeAlert() {
|
||||
Alert.alert(
|
||||
Dialogs.ERROR.INVALID_CODE,
|
||||
Dialogs.ERROR.INVALID_CODE_DESC,
|
||||
[{ text: Dialogs.BUTTON.OK }],
|
||||
{ cancelable: false }
|
||||
);
|
||||
}
|
||||
|
||||
async function handlePress() {
|
||||
setIsLoading(true);
|
||||
// await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
const code = text;
|
||||
getCompanyDetails(code)
|
||||
.then(function (details) {
|
||||
setIsLoading(false);
|
||||
getCompanyBranches(code)
|
||||
.then(function (branches){
|
||||
navigation.navigate(Screens.BRANCH_PICK, {
|
||||
details: details, branches: branches, code
|
||||
});})
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
setIsLoading(false);
|
||||
displayInvalidCodeAlert();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.area}>
|
||||
{isLoading && <ActivityIndicator style={styles.loader} size={75} />}
|
||||
{isLoading && <View style={styles.loadingArea}></View>}
|
||||
<TextInput
|
||||
style={styles.input}
|
||||
onChangeText={(text) => onChangeText(text)}
|
||||
value={text}
|
||||
placeholder={Dialogs.PROMPT.ENTER_CODE}
|
||||
/>
|
||||
<Pressable
|
||||
style={({ pressed }) => [
|
||||
styles.button,
|
||||
pressed ? styles.buttonPressed : null,
|
||||
]}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<Text style={styles.text}>{Dialogs.BUTTON.SUBMIT}</Text>
|
||||
</Pressable>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
loadingArea: {
|
||||
position: "absolute",
|
||||
zIndex: 2,
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
backgroundColor: "rgba(255, 255, 255, 0.75)",
|
||||
},
|
||||
loader: {
|
||||
position: "absolute",
|
||||
zIndex: 100,
|
||||
},
|
||||
input: {
|
||||
height: 40,
|
||||
margin: 12,
|
||||
borderWidth: 1,
|
||||
padding: 10,
|
||||
textAlign: "center",
|
||||
width: 220,
|
||||
},
|
||||
area: {
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
flex: 1,
|
||||
},
|
||||
button: {
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
paddingVertical: 12,
|
||||
paddingHorizontal: 32,
|
||||
borderRadius: 4,
|
||||
elevation: 3,
|
||||
width: buttonWidth, // Adjusted dynamically based on screen width
|
||||
backgroundColor: Colors.ACCENT,
|
||||
},
|
||||
buttonPressed: {
|
||||
backgroundColor: Colors.PRIMARY,
|
||||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
lineHeight: 21,
|
||||
fontWeight: "bold",
|
||||
letterSpacing: 0.25,
|
||||
color: "white",
|
||||
},
|
||||
});
|
||||
|
||||
registerRootComponent(({ navigation }) => <CodeEnterScreen navigation={navigation} />);
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, StyleSheet, Dimensions } from 'react-native';
|
||||
import { Fonts } from '../constants/fonts';
|
||||
import { Colors } from '../constants/colors';
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
const windowHeight = Dimensions.get('window').height;
|
||||
|
||||
export default function TicketInfoScreen({ route }: {route: any}) {
|
||||
|
||||
const ticket = route.params.ticket
|
||||
console.log(ticket)
|
||||
useEffect(() => {
|
||||
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Ticket Details</Text>
|
||||
{ticket ? (
|
||||
<View style={styles.detailsContainer}>
|
||||
<Text style={styles.detailText}>Naziv: {ticket.name}</Text>
|
||||
<Text style={styles.detailText}>Generisano: {new Date(ticket.date).toLocaleString()}</Text>
|
||||
<Text style={styles.detailText}>Podići na šalterima:</Text>
|
||||
{ticket.stations.map((station: string, index: number) => (
|
||||
<Text key={index} style={styles.stationText}>{"- "+ station}</Text>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<Text>Loading...</Text>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.BACKGROUND,
|
||||
justifyContent: 'flex-start',
|
||||
paddingHorizontal: windowWidth * 0.05,
|
||||
paddingTop: windowHeight * 0.2,
|
||||
},
|
||||
title: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: windowHeight * 0.03,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
alignSelf: 'center',
|
||||
},
|
||||
detailsContainer: {
|
||||
marginTop: windowHeight * 0.03,
|
||||
},
|
||||
detailText: {
|
||||
fontSize: 20,
|
||||
marginBottom: 10,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
},
|
||||
stationText: {
|
||||
fontSize: 20,
|
||||
marginBottom: 5,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
marginLeft: 10,
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,154 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { View, Text, FlatList, TouchableOpacity, StyleSheet, Dimensions, ActivityIndicator } from 'react-native';
|
||||
import { Screens } from '../constants/screens';
|
||||
import { getTickets } from '../services/fetchData';
|
||||
import { Colors } from '../constants/colors';
|
||||
import { Fonts } from '../constants/fonts';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { getExpoToken } from '../utils/tokenUtils';
|
||||
const windowWidth = Dimensions.get('window').width;
|
||||
const windowHeight = Dimensions.get('window').height;
|
||||
|
||||
export default function TicketListScreen({ navigation }: { navigation: any }){
|
||||
|
||||
const [tickets, setTickets] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchTickets = async () => {
|
||||
try {
|
||||
const fetchedTickets = await getTickets(getExpoToken());
|
||||
setTickets(fetchedTickets);
|
||||
} catch (error) {
|
||||
console.error('Error fetching tickets:', error);
|
||||
}
|
||||
};
|
||||
fetchTickets();
|
||||
|
||||
const intervalId = setInterval(fetchTickets, 10000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
|
||||
const renderItem = ({ item }: { item: any }) => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={styles.ticketItem}
|
||||
onPress={() => handlePress(item.id)}
|
||||
>
|
||||
<Text style={styles.ticketName}>{item.name}</Text>
|
||||
<View style={styles.stationContainer}>
|
||||
<Text style={styles.stationTitle}>Šalteri</Text>
|
||||
{item.stations.map((station: string, index: number) => (
|
||||
<Text key={index} style={styles.stationList}>{station}</Text>
|
||||
))}
|
||||
</View>
|
||||
<View style={styles.iconContainer}>
|
||||
<Ionicons name="information-circle-outline" size={windowWidth * 0.1} color={Colors.ACCENT} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
async function handlePress(id: any) {
|
||||
|
||||
let ticket = tickets.find(item => item.id == id)
|
||||
navigation.navigate(Screens.TICKET_INFO, {
|
||||
ticket:ticket
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
if (tickets.length === 0) {
|
||||
return (
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="large" color={Colors.PRIMARY} />
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Tiketi</Text>
|
||||
<FlatList
|
||||
data={tickets}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={styles.flatListContainer}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.BACKGROUND,
|
||||
justifyContent: 'center',
|
||||
paddingTop: windowHeight * 0.1,
|
||||
},
|
||||
title: {
|
||||
fontSize: windowWidth * 0.1,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: windowHeight * 0.03,
|
||||
marginLeft: windowWidth * 0.05,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
},
|
||||
flatListContainer: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: windowWidth * 0.05,
|
||||
paddingBottom: windowHeight * 0.05,
|
||||
},
|
||||
ticketItem: {
|
||||
paddingHorizontal: windowWidth * 0.05,
|
||||
paddingVertical: windowHeight * 0.06,
|
||||
marginBottom: windowHeight * 0.02,
|
||||
borderRadius: windowWidth * 0.05,
|
||||
backgroundColor: '#fff',
|
||||
shadowColor: Colors.ACCENT,
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
},
|
||||
ticketContent: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
ticketName: {
|
||||
flex: 1,
|
||||
fontSize: windowWidth * 0.08,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
textAlign: 'center',
|
||||
},
|
||||
icon: {
|
||||
marginLeft: windowWidth * 0.02,
|
||||
},
|
||||
iconContainer: {
|
||||
alignItems: 'center',
|
||||
marginTop: windowHeight * 0.02,
|
||||
},
|
||||
loadingContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
stationList: {
|
||||
fontSize: windowWidth * 0.05,
|
||||
color: 'gray',
|
||||
marginTop: 5,
|
||||
},
|
||||
stationTitle: {
|
||||
fontSize: windowWidth * 0.06,
|
||||
fontWeight: 'bold',
|
||||
marginBottom: 5,
|
||||
},
|
||||
stationContainer: {
|
||||
marginTop: 30,
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,256 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
Animated,
|
||||
Text,
|
||||
FlatList,
|
||||
TouchableOpacity,
|
||||
Platform,
|
||||
Alert,
|
||||
} from "react-native";
|
||||
import WelcomeMessage from "../components/WelcomeMessage";
|
||||
import { useFonts } from "expo-font";
|
||||
const { Fonts } = require("../constants/fonts");
|
||||
const { Assets } = require("../constants/assets");
|
||||
const { Colors } = require("../constants/colors");
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import AssignedNumberAlert from "../components/AssignedNumberAlert";
|
||||
//import * as Notifications from "expo-notifications";
|
||||
//import * as Device from "expo-device";
|
||||
import { getExpoToken, setExpoToken } from "../utils/tokenUtils";
|
||||
import { generateTicket } from "../services/fetchData";
|
||||
|
||||
export default function WelcomeScreen({ route }: { route: any }) {
|
||||
const { details } = route.params;
|
||||
let { services } = route.params;
|
||||
let { branchID } = route.params;
|
||||
|
||||
let { name, welcomeMessage, font, logoUrl } = details;
|
||||
|
||||
/*
|
||||
Notifications.setNotificationHandler({
|
||||
handleNotification: async () => ({
|
||||
shouldShowAlert: true,
|
||||
shouldPlaySound: true,
|
||||
shouldSetBadge: false,
|
||||
}),
|
||||
});
|
||||
*/
|
||||
|
||||
let [fontsLoaded] = useFonts({
|
||||
[Fonts.ARIAL]: Assets.ARIAL,
|
||||
[Fonts.TIMES_NEW_ROMAN]: Assets.TIMES_NEW_ROMAN,
|
||||
[Fonts.VERDANA]: Assets.VERDANA,
|
||||
[Fonts.HELVETICA]: Assets.HELVETICA,
|
||||
[Fonts.MONTSERRAT]: Assets.MONTSERRAT,
|
||||
[Fonts.CALIBRI]: Assets.CALIBRI,
|
||||
[Fonts.FUTURA]: Assets.FUTURA,
|
||||
[Fonts.BODONI]: Assets.BODONI,
|
||||
[Fonts.ROCKWELL]: Assets.ROCKWELL,
|
||||
[Fonts.COMIC_SANS_MS]: Assets.COMIC_SANS_MS,
|
||||
});
|
||||
|
||||
const fadeAnim = React.useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
Animated.timing(fadeAnim, {
|
||||
toValue: 1,
|
||||
duration: 2500,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [fadeAnim]);
|
||||
|
||||
/*
|
||||
useEffect(() => {
|
||||
getToken();
|
||||
}, []);
|
||||
|
||||
async function getToken() {
|
||||
try {
|
||||
await registerForPushNotificationsAsync().then((token) => {
|
||||
if (token != undefined) setExpoToken(token);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch data in TasksScreen:", error);
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [ticket, setTicket] = useState(0);
|
||||
|
||||
const openModal = () => {
|
||||
setModalVisible(true);
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
setModalVisible(false);
|
||||
};
|
||||
|
||||
const renderItem = ({ item }: { item: any }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.branchItem}
|
||||
onPress={() => handlePress(item.id)}
|
||||
>
|
||||
<View style={styles.branchInfoContainer}>
|
||||
<Ionicons
|
||||
name="information-circle"
|
||||
size={24}
|
||||
color={Colors.ACCENT}
|
||||
style={styles.icon}
|
||||
/>
|
||||
<Text style={styles.branchName}>{item.name}</Text>
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={24}
|
||||
color={Colors.ACCENT}
|
||||
style={styles.icon}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
async function handlePress(serviceID: any) {
|
||||
const assignTicket = async () => {
|
||||
try {
|
||||
const fetchedTicket = await generateTicket(
|
||||
getExpoToken(),
|
||||
branchID,
|
||||
serviceID
|
||||
);
|
||||
setTicket(fetchedTicket.number);
|
||||
} catch (error) {
|
||||
console.error("Error fetching tickets:", error);
|
||||
}
|
||||
};
|
||||
|
||||
assignTicket();
|
||||
openModal();
|
||||
}
|
||||
|
||||
if (!fontsLoaded) return <Text>Loading...</Text>;
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.logoContainer}>
|
||||
<Animated.Image
|
||||
style={[styles.logo, { opacity: fadeAnim }]}
|
||||
source={{ uri: logoUrl }}
|
||||
/>
|
||||
</View>
|
||||
<WelcomeMessage name={name} font={font} welcome={welcomeMessage} />
|
||||
<FlatList
|
||||
data={services}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
contentContainerStyle={styles.flatListContainer}
|
||||
/>
|
||||
<AssignedNumberAlert
|
||||
visible={modalVisible}
|
||||
number={ticket}
|
||||
onClose={closeModal}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
async function registerForPushNotificationsAsync() {
|
||||
let token;
|
||||
|
||||
if (Platform.OS === "android") {
|
||||
await Notifications.setNotificationChannelAsync("default", {
|
||||
name: "default",
|
||||
importance: Notifications.AndroidImportance.MAX,
|
||||
vibrationPattern: [0, 250, 250, 250],
|
||||
lightColor: "#FF231F7C",
|
||||
});
|
||||
}
|
||||
|
||||
if (Device.isDevice) {
|
||||
const { status: existingStatus } =
|
||||
await Notifications.getPermissionsAsync();
|
||||
let finalStatus = existingStatus;
|
||||
if (existingStatus !== "granted") {
|
||||
const { status } = await Notifications.requestPermissionsAsync();
|
||||
finalStatus = status;
|
||||
}
|
||||
if (finalStatus !== "granted") {
|
||||
alert("Failed to get push token for push notification!");
|
||||
return;
|
||||
}
|
||||
|
||||
token = (await Notifications.getExpoPushTokenAsync()).data;
|
||||
} else {
|
||||
alert("Must use physical device for Push Notifications");
|
||||
}
|
||||
return token;
|
||||
}
|
||||
*/
|
||||
|
||||
export const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: Colors.BACKGROUND,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
titleContainer: {
|
||||
alignSelf: "flex-start",
|
||||
width: "100%",
|
||||
},
|
||||
logoContainer: {
|
||||
marginBottom: 20, // Add margin to separate logo from title
|
||||
fontFamily: Fonts.ARIAL,
|
||||
color: Colors.TEXT_PRIMARY,
|
||||
},
|
||||
logo: {
|
||||
marginTop: 80, // Add margin to separate logo from title
|
||||
width: "40%", // Adjust the logo size to fit better on all screens
|
||||
height: undefined, // Allow height to adjust automatically based on width
|
||||
aspectRatio: 1, // Maintain aspect ratio
|
||||
resizeMode: "contain",
|
||||
},
|
||||
title: {
|
||||
fontSize: 42,
|
||||
textAlign: "center",
|
||||
marginTop: 20,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
color: Colors.TEXT_PRIMARY,
|
||||
},
|
||||
flatListContainer: {
|
||||
flexGrow: 1,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 10, // Add padding to the top of the list to separate from title
|
||||
},
|
||||
branchItem: {
|
||||
marginTop: 10, // Add margin between items
|
||||
paddingVertical: 15,
|
||||
paddingHorizontal: 20,
|
||||
borderRadius: 10,
|
||||
borderColor: Colors.ACCENT,
|
||||
borderWidth: 1,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: {
|
||||
width: 0,
|
||||
height: 2,
|
||||
},
|
||||
shadowOpacity: 0.25,
|
||||
shadowRadius: 3.84,
|
||||
elevation: 5,
|
||||
width: "100%", // Make items take full width of the screen
|
||||
},
|
||||
branchInfoContainer: {
|
||||
flexDirection: "row", // Arrange items horizontally
|
||||
alignItems: "center", // Center items vertically
|
||||
},
|
||||
branchName: {
|
||||
fontSize: 30,
|
||||
fontFamily: Fonts.ARIAL,
|
||||
color: Colors.TEXT_PRIMARY,
|
||||
marginLeft: 10, // Add some space between text and icon
|
||||
},
|
||||
icon: {
|
||||
marginRight: 10, // Add some space between icon and text
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,323 @@
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { Buffer } from "buffer";
|
||||
import * as Sharing from "expo-sharing";
|
||||
import { Platform, PermissionsAndroid } from "react-native";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
|
||||
const { SERVER_URL } = require("../constants/api");
|
||||
const { Dialogs } = require("../constants/dialogs");
|
||||
const { Fonts } = require("../constants/fonts");
|
||||
const { Assets } = require("../constants/assets");
|
||||
const { validateCodeFormat } = require("../utils/validation");
|
||||
|
||||
interface CompanyData {
|
||||
name: string;
|
||||
welcomeMessage: string;
|
||||
font: string;
|
||||
logoUrl: string;
|
||||
}
|
||||
|
||||
interface BranchData {
|
||||
id: string;
|
||||
name: string;
|
||||
tellerStations: Array<Int32Array>;
|
||||
}
|
||||
|
||||
interface ServiceData {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface TicketData {
|
||||
id: string;
|
||||
name: string;
|
||||
date: Date;
|
||||
number: number;
|
||||
stations: Array<String>;
|
||||
}
|
||||
|
||||
async function getCompanyDetails(code: string) {
|
||||
if (!validateCodeFormat(code)) throw new Error(Dialogs.ERROR.INVALID_CODE);
|
||||
let details: CompanyData = await fetch(
|
||||
`${SERVER_URL}/api/v1/tenants/${code}`,
|
||||
{ method: "GET" }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error(Dialogs.ERROR.INVALID_CODE);
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
const companyData: CompanyData = {
|
||||
name: data.name,
|
||||
welcomeMessage: data.welcomeMessage,
|
||||
font: data.font,
|
||||
logoUrl: data.logo.base64Logo,
|
||||
};
|
||||
if (!companyData.font) companyData.font = Fonts.DEFAULT_FONT;
|
||||
if (!companyData.logoUrl) companyData.logoUrl = Assets.DEFAULT_LOGO;
|
||||
return companyData;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
throw error;
|
||||
});
|
||||
return details;
|
||||
}
|
||||
|
||||
async function getCompanyBranches(code: any) {
|
||||
const branchList: BranchData[] = [];
|
||||
let branches: BranchData[] = await fetch(
|
||||
`${SERVER_URL}/api/v1/branches/${code}`,
|
||||
{ method: "GET" }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error(Dialogs.ERROR.INVALID_CODE);
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
data.forEach((branch: any) => {
|
||||
const tellerList: Int32Array[] = [];
|
||||
branch.tellerStations.forEach((teller: any) => {
|
||||
tellerList.push(teller);
|
||||
});
|
||||
|
||||
const branchData: BranchData = {
|
||||
id: branch.id,
|
||||
name: branch.name,
|
||||
tellerStations: tellerList,
|
||||
};
|
||||
|
||||
branchList.push(branchData);
|
||||
});
|
||||
|
||||
return branchList;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
throw error;
|
||||
});
|
||||
return branches;
|
||||
}
|
||||
|
||||
async function getBranchServices(code: string, id: string) {
|
||||
const serviceList: ServiceData[] = [];
|
||||
let services: ServiceData[] = await fetch(
|
||||
`${SERVER_URL}/api/v1/branches/${code}/${id}/services`,
|
||||
{ method: "GET" }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error(Dialogs.ERROR.INVALID_CODE);
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
data.forEach((service: any) => {
|
||||
const serviceData: ServiceData = {
|
||||
id: service.id,
|
||||
name: service.name,
|
||||
};
|
||||
serviceList.push(serviceData);
|
||||
});
|
||||
|
||||
return serviceList;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
throw error;
|
||||
});
|
||||
return services;
|
||||
}
|
||||
|
||||
/*
|
||||
async function getTickets(token: string) {
|
||||
const ticketList: TicketData[] = [];
|
||||
let tickets: TicketData[] = await fetch(
|
||||
`${SERVER_URL}/tickets/devices/${token}`,
|
||||
{ method: "GET" }
|
||||
)
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error(Dialogs.ERROR.INVALID_CODE);
|
||||
}
|
||||
})
|
||||
.then((data) => {
|
||||
let ticket = data;
|
||||
|
||||
const stationList: String[] = [];
|
||||
ticket.stations.forEach((station: any) => {
|
||||
stationList.push(station.name);
|
||||
});
|
||||
|
||||
const ticketData: TicketData = {
|
||||
id: ticket.ticket.id,
|
||||
name: ticket.ticket.branch.name,
|
||||
date: new Date(ticket.ticket.createdAt),
|
||||
stations: stationList,
|
||||
number: ticket.ticket.number,
|
||||
};
|
||||
ticketList.push(ticketData);
|
||||
|
||||
return ticketList;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
throw error;
|
||||
});
|
||||
return tickets;
|
||||
}
|
||||
*/
|
||||
|
||||
async function generateTicket(
|
||||
token: string,
|
||||
branchId: number,
|
||||
serviceId: number
|
||||
) {
|
||||
let tickets: TicketData = await fetch(`${SERVER_URL}/api/v1/tickets`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
branchId: `${branchId}`,
|
||||
serviceId: `${serviceId}`,
|
||||
deviceToken: token,
|
||||
}),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
return response.json();
|
||||
} else {
|
||||
throw new Error(Dialogs.ERROR.INVALID_CODE);
|
||||
}
|
||||
})
|
||||
.then(async (data) => {
|
||||
await printTicket(data.ticket.id);
|
||||
console.log(data.ticket.id);
|
||||
return data.ticket;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error:", error);
|
||||
throw error;
|
||||
});
|
||||
return tickets;
|
||||
}
|
||||
|
||||
function printTicket(ticketId: number) {
|
||||
console.log("Printing ticket...");
|
||||
fetch(`${SERVER_URL}/api/v1/tickets/${ticketId}/print`, {
|
||||
method: "GET",
|
||||
})
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch PDF: ${response.status} - ${response.statusText}`
|
||||
);
|
||||
}
|
||||
return response.arrayBuffer();
|
||||
})
|
||||
.then((pdfByteArray) => {
|
||||
const filename = `ticket_${ticketId}`;
|
||||
return savePdfToFile(pdfByteArray, filename);
|
||||
})
|
||||
.then((filePath) => {
|
||||
if (filePath) {
|
||||
console.log("PDF saved successfully at: ", filePath);
|
||||
} else {
|
||||
console.log("Failed to save PDF.");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error printing ticket: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
async function savePdfToFile(pdfByteArray: ArrayBuffer, filename: string) {
|
||||
try {
|
||||
const folderPath = await FileSystem.documentDirectory + "pdfs/";
|
||||
await FileSystem.makeDirectoryAsync(folderPath, { intermediates: true });
|
||||
const filePath = folderPath + filename + ".pdf";
|
||||
const buffer = Buffer.from(pdfByteArray);
|
||||
const base64String = buffer.toString("base64");
|
||||
await FileSystem.writeAsStringAsync(filePath, base64String, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
saveFile(filePath, filename, "application/pdf");
|
||||
return filePath;
|
||||
} catch (error) {
|
||||
console.error("Error saving PDF: ", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveFile(uri: string, filename: string, mimetype: string) {
|
||||
// await AsyncStorage.removeItem("permission");
|
||||
if (Platform.OS === "android") {
|
||||
const permission = await AsyncStorage.getItem("permission");
|
||||
console.log("PERMISSION : " + permission);
|
||||
if (!permission) {
|
||||
// Check if permission is already granted
|
||||
//let x = await Permissions.askAsync(Permissions.MANAGE_EXTERNAL_STORAGE);
|
||||
const permissions =
|
||||
await FileSystem.StorageAccessFramework.requestDirectoryPermissionsAsync();
|
||||
console.log(permissions);
|
||||
if (permissions.granted)
|
||||
await AsyncStorage.setItem("permission", permissions.directoryUri);
|
||||
if (permissions.granted) {
|
||||
const base64 = await FileSystem.readAsStringAsync(uri, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
|
||||
await FileSystem.StorageAccessFramework.createFileAsync(
|
||||
permissions.directoryUri,
|
||||
filename,
|
||||
mimetype
|
||||
)
|
||||
.then(async (uri) => {
|
||||
await FileSystem.writeAsStringAsync(uri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
} else {
|
||||
Sharing.shareAsync(uri);
|
||||
}
|
||||
} else {
|
||||
// Permission already granted
|
||||
const base64 = await FileSystem.readAsStringAsync(uri, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
|
||||
if (permission !== null)
|
||||
await FileSystem.StorageAccessFramework.createFileAsync(
|
||||
permission,
|
||||
filename,
|
||||
mimetype
|
||||
)
|
||||
.then(async (uri) => {
|
||||
await FileSystem.writeAsStringAsync(uri, base64, {
|
||||
encoding: FileSystem.EncodingType.Base64,
|
||||
});
|
||||
})
|
||||
.catch((e) => console.log(e));
|
||||
}
|
||||
} else {
|
||||
Sharing.shareAsync(uri);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
getCompanyDetails,
|
||||
getBranchServices,
|
||||
getCompanyBranches,
|
||||
generateTicket,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
const { validateCodeFormat } = require("../utils/validation");
|
||||
const { Dialogs } = require("../constants/dialogs");
|
||||
|
||||
describe('Code validation tests', () => {
|
||||
|
||||
test('Valid and invalid code formats', () => {
|
||||
// Valid code formats
|
||||
expect(validateCodeFormat('ABCD')).toBe(true);
|
||||
expect(validateCodeFormat('WXYZ')).toBe(true);
|
||||
expect(validateCodeFormat('1234')).toBe(true);
|
||||
// Invalid code formats
|
||||
expect(validateCodeFormat('ABCD1')).toBe(false);
|
||||
expect(validateCodeFormat('ABCD!')).toBe(false);
|
||||
expect(validateCodeFormat('ABC')).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
let apiToken = '';
|
||||
|
||||
export function setExpoToken(token: string) {
|
||||
apiToken = token;
|
||||
}
|
||||
|
||||
export function getExpoToken() {
|
||||
return apiToken;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
function validateCodeFormat(code: string): boolean {
|
||||
return /^[A-Z0-9]{4}$/.test(code);
|
||||
}
|
||||
|
||||
export { validateCodeFormat };
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: { browser: true, es2020: true },
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:react/recommended',
|
||||
'plugin:react/jsx-runtime',
|
||||
'plugin:react-hooks/recommended',
|
||||
],
|
||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
||||
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||
settings: { react: { version: '18.2' } },
|
||||
plugins: ['react-refresh'],
|
||||
rules: {
|
||||
'react/jsx-no-target-blank': 'off',
|
||||
'react-refresh/only-export-components': [
|
||||
'warn',
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -0,0 +1,10 @@
|
||||
FROM node:21-alpine AS build
|
||||
WORKDIR /admin-app
|
||||
COPY package*.json .
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 5001
|
||||
CMD ["npm", "run", "preview"]
|
||||
@@ -0,0 +1,7 @@
|
||||
## Build instructions
|
||||
|
||||
- cd **admin-app**
|
||||
- npm **install**
|
||||
- npm **run dev**
|
||||
|
||||
#### **NOTE:** requires Node 21
|
||||
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link href="https://api.fontshare.com/v2/css?f[]=general-sans@200,300,400,500,600,700&display=swap" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BBQMS Admin App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.background-color-app{
|
||||
background-color: whitesmoke;
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import Header from './components/Header/Header.jsx';
|
||||
import { SERVER_URL } from './constants.js';
|
||||
import { UserContext } from './context/UserContext.jsx';
|
||||
import { fetchData } from './fetching/Fetch.js';
|
||||
import AuthGuard from './components/AuthGuard/AuthGuard';
|
||||
import LoginScreen from './pages/LoginScreen/LoginScreen';
|
||||
import CompanyInfoUpdate from './pages/CompanyInfoUpdate/CompanyInfoUpdate';
|
||||
import HomePage from './pages/HomePage/HomePage';
|
||||
import NotFound from './pages/NotFound/NotFound.jsx';
|
||||
import CanAccess from './components/CanAccess/CanAccess';
|
||||
import HomePageCard from './components/HomePageCard/HomePageCard';
|
||||
import AdminProfile from './pages/AdminProfile/AdminProfile';
|
||||
import LoginAuth from './components/LoginAuth/LoginAuth';
|
||||
import ManageAdmins from './pages/AdminManagingScreen/AdminManagingScreen';
|
||||
import ManageServices from './pages/ManageServices/ManageServices';
|
||||
import ManageBranches from './pages/ManageBranchesScreen/ManageBranchesScreen';
|
||||
import ManageGroups from './pages/ManageGroupsScreen/ManageGroupsScreen';
|
||||
import ManageStations from './pages/ManageStationScreen/ManageStationScreen';
|
||||
import ManageDisplays from './pages/ManageDisplays/ManageDisplays';
|
||||
import ManageUsers from './pages/UserManagingScreen/UserManagingScreen';
|
||||
import ViewQueues from './pages/ViewBranchQueues/ViewBranchQueues';
|
||||
import { ROLES } from './constants.js';
|
||||
|
||||
export default function App() {
|
||||
const [user, setUser] = useState();
|
||||
/*
|
||||
Kada se logiramo, ako vec postoji token u localStorage, provjerimo da li je validan (nije istekao)
|
||||
Ako je validan, ulogujemo usera, ako nije ocistimo storage od starih podataka
|
||||
*/
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
const url = `${ SERVER_URL }/api/v1/auth`;
|
||||
fetchData(url, 'GET')
|
||||
.then(({ data, success }) => {
|
||||
if (success) {
|
||||
setUser(JSON.parse(localStorage.getItem('userData')));
|
||||
} else {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('userData');
|
||||
}
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserContext.Provider value={ { user, setUser } }>
|
||||
<Header />
|
||||
<Routes>
|
||||
<Route exact path="/:tenantCode/manage/displays" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageDisplays />
|
||||
</AuthGuard> } />
|
||||
<Route exact path="/:tenantCode/manage/stations" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageStations />
|
||||
</AuthGuard> } />
|
||||
<Route exact path="/:tenantCode/manage/groups" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageGroups />
|
||||
</AuthGuard> } />
|
||||
|
||||
<Route exact path="/:tenantCode/manage/branches" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageBranches />
|
||||
</AuthGuard> } />
|
||||
|
||||
<Route exact path="/:tenantCode/companydetails"
|
||||
element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<CompanyInfoUpdate />
|
||||
</AuthGuard>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/:tenantCode/manage/services" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageServices />
|
||||
</AuthGuard> }
|
||||
/>
|
||||
<Route exact path="/:tenantCode/manage/users" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ManageUsers />
|
||||
</AuthGuard>
|
||||
} />
|
||||
<Route exact path="/login" element={ <LoginScreen /> } />
|
||||
<Route exact path="/:tenantCode/manage/admins" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN] }>
|
||||
<ManageAdmins />
|
||||
</AuthGuard> } />
|
||||
<Route exact path="/profile" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<AdminProfile />
|
||||
</AuthGuard> } />
|
||||
<Route exact path="/" element={ <LoginScreen /> } />
|
||||
<Route exact path="/loginauth"
|
||||
element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<LoginAuth />
|
||||
</AuthGuard>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/:tenantCode/queues" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<ViewQueues />
|
||||
</AuthGuard> } />
|
||||
|
||||
<Route exact path="/:tenantCode/home" element={
|
||||
<AuthGuard roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<HomePage></HomePage>
|
||||
<CanAccess roles={ [ROLES.ROLE_SUPER_ADMIN, ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
<>
|
||||
{ user && (
|
||||
<>
|
||||
<HomePageCard
|
||||
title="Manage displays"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/displays` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage groups"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/groups` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage branches"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/branches` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage services"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/services` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage teller stations"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/stations` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="Manage company details"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/companydetails` }
|
||||
/>
|
||||
<HomePageCard
|
||||
title="View queues"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/queues` }
|
||||
/>
|
||||
</>
|
||||
) }
|
||||
</>
|
||||
</CanAccess>
|
||||
|
||||
<CanAccess roles={ [ROLES.ROLE_SUPER_ADMIN] }>
|
||||
<>
|
||||
{ user && (
|
||||
<>
|
||||
<HomePageCard title="Manage administrators"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/admins` }></HomePageCard>
|
||||
<HomePageCard
|
||||
title="Manage users"
|
||||
backgroundColor="var(--light-blue)"
|
||||
buttonColor="var(--dark-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/users` }
|
||||
/>
|
||||
</>
|
||||
) }
|
||||
</>
|
||||
</CanAccess>
|
||||
|
||||
<CanAccess roles={ [ROLES.ROLE_BRANCH_ADMIN] }>
|
||||
{ user && (
|
||||
<HomePageCard
|
||||
title="Manage users"
|
||||
backgroundColor="var(--dark-blue)"
|
||||
buttonColor="var(--light-blue)"
|
||||
url={ `/${ user.tenantCode }/manage/users` }
|
||||
/>
|
||||
) }
|
||||
</CanAccess>
|
||||
</AuthGuard>
|
||||
} />
|
||||
<Route path="*" element={ <NotFound /> } />
|
||||
</Routes>
|
||||
</UserContext.Provider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export default function AuthGuard({ children, roles }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
const storedUserData = localStorage.getItem('userData');
|
||||
const user = storedUserData ? JSON.parse(storedUserData) : null;
|
||||
|
||||
if (!user) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasNecessaryRole = roles.some(role => user.roles.find(userRole => userRole === role));
|
||||
|
||||
if (!hasNecessaryRole) {
|
||||
navigate('/');
|
||||
return;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ children }
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { useContext } from 'react';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
|
||||
export default function CanAccess( {children, roles} ){
|
||||
|
||||
const {user, setUser} = useContext(UserContext);
|
||||
const hasRole = user && user.roles && user.roles.some(userRole => roles.find(role => role === userRole));
|
||||
return hasRole ? children : null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
header.main-header {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
margin: 0 0 0 -2%;
|
||||
background-color: #d3e2f8;
|
||||
width: 101vw;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-right: auto;
|
||||
margin-left: 2%;
|
||||
}
|
||||
|
||||
.header-logo-png {
|
||||
width: 200px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.header-logout-btn {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
justify-content: center; /* Center content horizontally */
|
||||
align-items: center;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
box-shadow: 3px 8px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 40px;
|
||||
width: 130px;
|
||||
font-size: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.header-logout-btn:hover {
|
||||
background-color: var(--light-blue);
|
||||
transition-duration: 300ms;
|
||||
}
|
||||
|
||||
.header-profile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
margin-left: 20px;
|
||||
margin-right: 2%;
|
||||
margin-bottom: 0.5%;
|
||||
|
||||
}
|
||||
|
||||
.header-profile-png {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useContext } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { lastPathPart } from '../../utils/StringUtils.js';
|
||||
import profileImage from '../../../assets/profile-user.png'
|
||||
|
||||
import './Header.css';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
|
||||
export default function Header() {
|
||||
const navigate = useNavigate();
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('userData');
|
||||
localStorage.removeItem('token');
|
||||
setUser(null);
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
const path = window.location.pathname;
|
||||
const showBackButton = !!user
|
||||
&& '/' !== path
|
||||
&& '/login' !== path
|
||||
&& 'home' !== lastPathPart(window.location.href);
|
||||
|
||||
const goHome = () => {
|
||||
if (user?.tenantCode) {
|
||||
navigate(`/${user.tenantCode}/home`);
|
||||
} else {
|
||||
navigate('/');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="main-header">
|
||||
<h2 className="header-logo" onClick={ goHome }>BBQMS</h2>
|
||||
|
||||
<div className="header-logout">
|
||||
{ !!user && (
|
||||
<button className="header-logout-btn" onClick={ handleLogout }>
|
||||
Logout
|
||||
</button>
|
||||
) }
|
||||
</div>
|
||||
|
||||
<div className="header-profile" onClick={ () => navigate('/profile') }>
|
||||
<img src={ profileImage } className="header-profile-png" alt="Profile image" />
|
||||
</div>
|
||||
</header>
|
||||
{ showBackButton && (
|
||||
<Button variant="secondary"
|
||||
className="mt-2 px-4"
|
||||
onClick={ goHome }>
|
||||
Back
|
||||
</Button>
|
||||
) }
|
||||
</>
|
||||
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
.button-hp{
|
||||
height: 80%;
|
||||
display: inline-block;
|
||||
margin-top: 50%;
|
||||
box-shadow: 3px 6px 10px 0 rgba(0,0,0,0.2);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.card-hp{
|
||||
width: 14%;
|
||||
border-radius: 10px;
|
||||
float: left;
|
||||
margin-right: 4%;
|
||||
margin-left: 6%;
|
||||
margin-top: 6%;
|
||||
}
|
||||
.card-title-hp{
|
||||
text-align: center;
|
||||
color: #334257;
|
||||
height: 10%;
|
||||
max-height: 10%;
|
||||
}
|
||||
.card-text-hp{
|
||||
text-align: center;
|
||||
color: #334257;
|
||||
}
|
||||
.button-container-hp{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import './HomePageCard.css'
|
||||
|
||||
export default function HomePageCard( {title, backgroundColor, buttonColor, url} ) {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div>
|
||||
<div className="card card-hp" style={{border: '1px solid #334257', backgroundColor: backgroundColor, height: '200px'}}>
|
||||
<div className="card-body">
|
||||
<h5 className="card-title card-title-hp" style={{color: 'white'}}>{title}</h5>
|
||||
<p className="card-text card-text-hp" style={{color: 'white'}}></p>
|
||||
<div className="button-container-hp">
|
||||
<button className="btn btn-primary button-hp"
|
||||
style={{backgroundColor: buttonColor}} onClick ={ () => navigate(url) } >Open</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import './HomePageCard.css'
|
||||
|
||||
export default function HomePageCardLight() {
|
||||
return (
|
||||
<div>
|
||||
<div className="card card-hp" style={{border: '1px solid #334257', backgroundColor: '#548CA8'}}>
|
||||
<div className="card-body">
|
||||
<h5 className="card-title card-title-hp" style={{color: 'white'}}>Card title</h5>
|
||||
<p className="card-text card-text-hp" style={{color: 'white'}}>Some quick example text to build on
|
||||
the card title and make up the bulk of
|
||||
the card's content.</p>
|
||||
<div className="button-container-hp">
|
||||
<a href="#" className="btn btn-primary button-hp"
|
||||
style={{backgroundColor: '#476072', border: '1px solid #548CA8'}}>Go somewhere</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
#auth-container *{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#auth-container{
|
||||
font-family: "Poppins", sans-serif;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
#authForm {
|
||||
border: 1px solid black;
|
||||
padding: 40px;
|
||||
width: 1000px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputs > * {
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.button-auth {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 24px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.button-auth:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import React, { useEffect, useRef, useReducer, useContext } from "react";
|
||||
import "./LoginAuth.css";
|
||||
import { SERVER_URL } from "../../constants.js";
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
function doSubmit(submittedValues) {
|
||||
console.log(`Submitted: ${submittedValues.join("")}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function clampIndex(index) {
|
||||
if (index > 6) {
|
||||
return 6;
|
||||
} else if (index < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "INPUT":
|
||||
return {
|
||||
...state,
|
||||
inputValues: [
|
||||
...state.inputValues.slice(0, action.payload.index),
|
||||
action.payload.value,
|
||||
...state.inputValues.slice(action.payload.index + 1)
|
||||
],
|
||||
focusedIndex: clampIndex(state.focusedIndex + 1)
|
||||
};
|
||||
|
||||
case "BACK":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: clampIndex(state.focusedIndex - 1)
|
||||
};
|
||||
|
||||
case "PASTE":
|
||||
return {
|
||||
...state,
|
||||
inputValues: state.inputValues.map(
|
||||
(_, index) => action.payload.pastedValue[index] || ""
|
||||
)
|
||||
};
|
||||
|
||||
case "FOCUS":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: action.payload.focusedIndex
|
||||
};
|
||||
|
||||
case "VERIFY":
|
||||
return {
|
||||
...state,
|
||||
status: "pending"
|
||||
};
|
||||
|
||||
case "VERIFY_SUCCESS":
|
||||
return {
|
||||
...state,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
case "RESET_INPUTS":
|
||||
return {
|
||||
...state,
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
default:
|
||||
throw new Error("unknown action");
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
export default function LoginAuth() {
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer(
|
||||
reducer,
|
||||
initialState
|
||||
);
|
||||
|
||||
function handleInput(index, value) {
|
||||
dispatch({ type: "INPUT", payload: { index, value } });
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
dispatch({ type: "BACK" });
|
||||
}
|
||||
|
||||
function handlePaste(pastedValue) {
|
||||
dispatch({ type: "PASTE", payload: { pastedValue } });
|
||||
|
||||
if (pastedValue.length === 6) {
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(pastedValue.split("")).then(() =>
|
||||
dispatch({ type: "VERIFY_SUCCESS" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(focusedIndex) {
|
||||
dispatch({ type: "FOCUS", payload: { focusedIndex } });
|
||||
}
|
||||
|
||||
async function handleSubmit(e) {
|
||||
|
||||
//e.preventDefault();
|
||||
|
||||
dispatch({ type: "VERIFY" });
|
||||
|
||||
try {
|
||||
const storedUserData = localStorage.getItem('userData');
|
||||
const userData = storedUserData ? JSON.parse(storedUserData) : null;
|
||||
if (!userData) {
|
||||
throw new Error("Email not found in localStorage");
|
||||
}
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/auth/tfa`;
|
||||
const { data, success } = await fetchData(url, 'POST', {
|
||||
code: inputValues.join(""),
|
||||
email: userData.email
|
||||
});
|
||||
|
||||
if (success) {
|
||||
localStorage.setItem('token', data.token);
|
||||
setUser(data.userData);
|
||||
navigate(`/${ data.userData.tenantCode }/home`);
|
||||
} else {
|
||||
throw new Error("Code could not be verified. It is incorrect or expired.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Code could not be verified. It is incorrect or expired.");
|
||||
resetInputs();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function resetInputs() {
|
||||
dispatch({ type: "RESET_INPUTS" });
|
||||
}
|
||||
let navigate = useNavigate();
|
||||
function routeChange() {
|
||||
handleSubmit({});
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="auth-container">
|
||||
<form id="authForm" onSubmit={handleSubmit}>
|
||||
<div className="inputs">
|
||||
{inputValues.map((value, index) => {
|
||||
return (
|
||||
<Input
|
||||
key={index}
|
||||
index={index}
|
||||
value={value}
|
||||
onChange={handleInput}
|
||||
onBackspace={handleBack}
|
||||
onPaste={handlePaste}
|
||||
isFocused={index === focusedIndex}
|
||||
onFocus={handleFocus}
|
||||
isDisabled={status === "pending"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button className="button-auth" disabled={status === "pending"} onClick={routeChange}>
|
||||
{status === "pending" ? "VERIFYING..." : "VERIFY"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Input({
|
||||
index,
|
||||
value,
|
||||
onChange,
|
||||
onPaste,
|
||||
onBackspace,
|
||||
isFocused,
|
||||
onFocus,
|
||||
isDisabled
|
||||
}) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (ref.current !== document.activeElement && isFocused) {
|
||||
ref.current.focus();
|
||||
}
|
||||
});
|
||||
}, [isFocused]);
|
||||
|
||||
function handleChange(e) {
|
||||
onChange(index, e.target.value);
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
onPaste(e.clipboardData.getData("text"));
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === "Backspace") {
|
||||
onBackspace();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(e) {
|
||||
e.target.setSelectionRange(0, 1);
|
||||
onFocus(index);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength="1"
|
||||
onFocus={handleFocus}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
#login-form {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="text"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
#registration{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import { GoogleLogin } from '@react-oauth/google';
|
||||
import React, { useState } from "react";
|
||||
import validator from "validator";
|
||||
import "./LoginForm.css";
|
||||
import LoginAuth from "../LoginAuth/LoginAuth";
|
||||
import { Route, Routes, useNavigate, Link } from "react-router-dom";
|
||||
import { SERVER_URL } from "../../constants.js";
|
||||
|
||||
const LoginForm = () => {
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
const [isSubmitted, setIsSubmitted] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
async function handleGoogleLogin(credentialResponse) {
|
||||
const response = await fetch(`${SERVER_URL}/api/v1/auth/login/oauth2/google`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
googleToken: credentialResponse.credential
|
||||
})
|
||||
});
|
||||
if (!response.ok) {
|
||||
setError("Error while trying to log in with Google.");
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
localStorage.setItem('userData', JSON.stringify(data));
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
console.log("username: " + username);
|
||||
console.log("password: " + password);
|
||||
|
||||
if (!username.trim()) {
|
||||
setError("Username is required");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.isEmail(username) && !validator.isMobilePhone(username, "any")) {
|
||||
setError("Invalid username format. Please enter a valid email or phone number.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
setError("Password is required.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${SERVER_URL}/api/v1/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: username,
|
||||
password: password
|
||||
}),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
localStorage.setItem('userData', JSON.stringify(data));
|
||||
|
||||
if (response.ok) {
|
||||
setIsSubmitted(true);
|
||||
navigate('/');
|
||||
} else if (response.status === 403) {
|
||||
setError("Your credentials are incorrect.");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setError("An error occurred. Please try again.");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsernameChange = (event) => {
|
||||
setUsername(event.target.value);
|
||||
setError("");
|
||||
};
|
||||
|
||||
const handlePasswordChange = (event) => {
|
||||
setPassword(event.target.value);
|
||||
setError("");
|
||||
};
|
||||
|
||||
if (isSubmitted) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='/' element={<LoginAuth />} />
|
||||
</Routes>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="login-form">
|
||||
<h1>LOGIN</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Email or phone number:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={username}
|
||||
onChange={handleUsernameChange}
|
||||
/>
|
||||
{error && (error.includes("Username") || error.includes("Invalid")) && <p className="error">{error}</p>}
|
||||
{error && (error.includes("credentials")) && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
{error && error.includes("Password") && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<input type="submit" value="Submit" />
|
||||
<p id="registration">
|
||||
Not registered? <Link to="/registration">Create an account</Link>
|
||||
</p>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
@@ -0,0 +1,66 @@
|
||||
.auth-container-reg *{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.auth-container-reg{
|
||||
font-family: "Poppins", sans-serif;
|
||||
min-height: 100vh;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
|
||||
.authForm-reg {
|
||||
border: 1px solid black;
|
||||
padding: 0px 40px 40px 40px;
|
||||
width: 1000px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.QRCodeContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.QRCodeContainer h1 {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.inputs-reg {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.inputs-reg > * {
|
||||
border: 1px solid black;
|
||||
width: 100%;
|
||||
padding: 40px;
|
||||
text-align: center;
|
||||
font-size: 40px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
padding: 24px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
@@ -0,0 +1,236 @@
|
||||
import React, { useEffect, useRef, useReducer, useState } from "react";
|
||||
import "./RegistrationAuth.css";
|
||||
import { SERVER_URL } from "../../constants";
|
||||
|
||||
function doSubmit(submittedValues) {
|
||||
console.log(`Submitted: ${submittedValues.join("")}`);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, 1500);
|
||||
});
|
||||
}
|
||||
|
||||
function clampIndex(index) {
|
||||
if (index > 6) {
|
||||
return 6;
|
||||
} else if (index < 0) {
|
||||
return 0;
|
||||
} else {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
function reducer(state, action) {
|
||||
switch (action.type) {
|
||||
case "INPUT":
|
||||
return {
|
||||
...state,
|
||||
inputValues: [
|
||||
...state.inputValues.slice(0, action.payload.index),
|
||||
action.payload.value,
|
||||
...state.inputValues.slice(action.payload.index + 1)
|
||||
],
|
||||
focusedIndex: clampIndex(state.focusedIndex + 1)
|
||||
};
|
||||
|
||||
case "BACK":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: clampIndex(state.focusedIndex - 1)
|
||||
};
|
||||
|
||||
case "PASTE":
|
||||
return {
|
||||
...state,
|
||||
inputValues: state.inputValues.map(
|
||||
(_, index) => action.payload.pastedValue[index] || ""
|
||||
)
|
||||
};
|
||||
|
||||
case "FOCUS":
|
||||
return {
|
||||
...state,
|
||||
focusedIndex: action.payload.focusedIndex
|
||||
};
|
||||
|
||||
case "VERIFY":
|
||||
return {
|
||||
...state,
|
||||
status: "pending"
|
||||
};
|
||||
|
||||
case "VERIFY_SUCCESS":
|
||||
return {
|
||||
...state,
|
||||
status: "idle"
|
||||
};
|
||||
case "RESET_INPUTS":
|
||||
return {
|
||||
...state,
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
default:
|
||||
throw new Error("unknown action");
|
||||
}
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
inputValues: Array(6).fill(""),
|
||||
focusedIndex: 0,
|
||||
status: "idle"
|
||||
};
|
||||
|
||||
export default function RefistrationAuth( {qrCode, email} ) {
|
||||
const [{ inputValues, focusedIndex, status }, dispatch] = useReducer(
|
||||
reducer,
|
||||
initialState
|
||||
);
|
||||
|
||||
function handleInput(index, value) {
|
||||
dispatch({ type: "INPUT", payload: { index, value } });
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
dispatch({ type: "BACK" });
|
||||
}
|
||||
|
||||
function handlePaste(pastedValue) {
|
||||
dispatch({ type: "PASTE", payload: { pastedValue } });
|
||||
|
||||
if (pastedValue.length === 6) {
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(pastedValue.split("")).then(() =>
|
||||
dispatch({ type: "VERIFY_SUCCESS" })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(focusedIndex) {
|
||||
dispatch({ type: "FOCUS", payload: { focusedIndex } });
|
||||
}
|
||||
|
||||
function handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch({ type: "VERIFY" });
|
||||
doSubmit(inputValues).then(() => dispatch({ type: "VERIFY_SUCCESS" }));
|
||||
}
|
||||
|
||||
const checkInputValues = async () =>{
|
||||
const enteredValues = inputValues.join("");
|
||||
const requestBody = {
|
||||
code: enteredValues,
|
||||
email: email
|
||||
}
|
||||
try{
|
||||
const response = await fetch(SERVER_URL + '/api/v1/auth/tfa', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
if (response.ok) {
|
||||
dispatch({ type: "VERIFY_SUCCESS" });
|
||||
} else {
|
||||
throw new Error("Code could not be verified. It is incorrect or expired.");
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Code could not be verified. It is incorrect.");
|
||||
resetInputs();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function resetInputs() {
|
||||
dispatch({ type: "RESET_INPUTS" });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="auth-container-reg">
|
||||
<form className="authForm-reg" onSubmit={handleSubmit}>
|
||||
<div className="QRCodeContainer">
|
||||
<h1>Scan QR code</h1>
|
||||
<img src={qrCode} height='300px'></img>
|
||||
</div>
|
||||
<div className="inputs-reg">
|
||||
{inputValues.map((value, index) => {
|
||||
return (
|
||||
<Input
|
||||
key={index}
|
||||
index={index}
|
||||
value={value}
|
||||
onChange={handleInput}
|
||||
onBackspace={handleBack}
|
||||
onPaste={handlePaste}
|
||||
isFocused={index === focusedIndex}
|
||||
onFocus={handleFocus}
|
||||
isDisabled={status === "pending"}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<button onClick={checkInputValues} disabled={status === "pending"}>
|
||||
{status === "pending" ? "VERIFYING..." : "VERIFY"}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Input({
|
||||
index,
|
||||
value,
|
||||
onChange,
|
||||
onPaste,
|
||||
onBackspace,
|
||||
isFocused,
|
||||
onFocus,
|
||||
isDisabled
|
||||
}) {
|
||||
const ref = useRef();
|
||||
useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
if (ref.current !== document.activeElement && isFocused) {
|
||||
ref.current.focus();
|
||||
}
|
||||
});
|
||||
}, [isFocused]);
|
||||
|
||||
function handleChange(e) {
|
||||
onChange(index, e.target.value);
|
||||
}
|
||||
|
||||
function handlePaste(e) {
|
||||
onPaste(e.clipboardData.getData("text"));
|
||||
}
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.key === "Backspace") {
|
||||
onBackspace();
|
||||
}
|
||||
}
|
||||
|
||||
function handleFocus(e) {
|
||||
e.target.setSelectionRange(0, 1);
|
||||
onFocus(index);
|
||||
}
|
||||
|
||||
return (
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onPaste={handlePaste}
|
||||
onKeyDown={handleKeyDown}
|
||||
maxLength="1"
|
||||
onFocus={handleFocus}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
.login-form-reg {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.login-form-reg form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="text"],
|
||||
.login-form-reg form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.login-form-reg form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.login{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.error-reg{
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Route, Routes, useNavigate, Link } from "react-router-dom";
|
||||
import { useFormik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
import "./RegistrationForm.css";
|
||||
import RegistrationAuth from '../RegistrationAuth/RegistrationAuth';
|
||||
import { SERVER_URL } from '../../constants';
|
||||
|
||||
const userSchema = yup.object().shape({
|
||||
email: yup.string().email("Please enter a valid email").required("Email is required"),
|
||||
password: yup.string().min(4).max(10).required("Password is required")
|
||||
});
|
||||
|
||||
export default function RegistrationForm() {
|
||||
const navigate = useNavigate();
|
||||
const [nextPage, setNextPage] = useState(false);
|
||||
const [qrCode, setQrCode] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
validationSchema: userSchema,
|
||||
onSubmit: async (values, { setFieldError }) => {
|
||||
try {
|
||||
const isValid = await userSchema.isValid(values);
|
||||
if (isValid) {
|
||||
const response = await fetch(SERVER_URL + '/api/v1/auth/register', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
email: values.email,
|
||||
password: values.password,
|
||||
phone_number: "00000"
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const body = await response.json();
|
||||
const responseBody = await fetch(SERVER_URL + `/api/v1/auth/tfa?email=${body.email}`);
|
||||
const responseData = await responseBody.json();
|
||||
setQrCode(responseData.qrCode);
|
||||
setEmail(body.email);
|
||||
setNextPage(true);
|
||||
} else if (response.status === 400) {
|
||||
setFieldError('email', 'Email already in use');
|
||||
} else {
|
||||
console.error('Failed to register:', response.statusText);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (nextPage) {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='*' element={<RegistrationAuth qrCode={qrCode} email={email} />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="login-form-reg">
|
||||
<h1>REGISTER ACCOUNT</h1>
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<label htmlFor="email">Email or phone number:</label>
|
||||
<input
|
||||
id="email"
|
||||
name="email"
|
||||
type="text"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.email}
|
||||
/>
|
||||
{formik.touched.email && formik.errors.email ? (
|
||||
<p className="error-reg">{formik.errors.email}</p>
|
||||
) : null}
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
value={formik.values.password}
|
||||
/>
|
||||
{formik.touched.password && formik.errors.password ? (
|
||||
<p className="error-reg">{formik.errors.password}</p>
|
||||
) : null}
|
||||
<input type="submit" value="Create new account"/>
|
||||
<p className="login">
|
||||
Already have an account? <Link to="/login">Login</Link>
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { useSortContext } from '../../context/SortContext.jsx';
|
||||
import { useSortSearchState } from '../../hooks/sortParamsHooks.jsx';
|
||||
|
||||
export function SortableHeader({ columnName, children }) {
|
||||
const { sort, updateSort } = useSortSearchState(columnName);
|
||||
const sortContext = useSortContext();
|
||||
|
||||
function handleSort() {
|
||||
const newSort = updateSort();
|
||||
sortContext.onSort(newSort);
|
||||
}
|
||||
|
||||
return (
|
||||
<th style={{ cursor: 'pointer' }}
|
||||
onClick={ handleSort }>
|
||||
{ children }
|
||||
</th>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useState } from 'react';
|
||||
import { createClassName } from '../../utils/StringUtils.js'
|
||||
|
||||
export function TimePicker({ title, onChange, className, defaultHour, defaultMinute }) {
|
||||
const [ hours, setHours ] = useState(defaultHour ?? '');
|
||||
const [ minutes, setMinutes ] = useState(defaultMinute ?? '');
|
||||
|
||||
function handleChange(hours, minutes) {
|
||||
onChange({
|
||||
hour: hours && hours !== '' ? parseInt(hours) : defaultHour ?? 0,
|
||||
minutes: minutes && minutes !== '' ? parseInt(minutes) : defaultMinute ?? 0
|
||||
});
|
||||
}
|
||||
|
||||
function handleHourChange(value) {
|
||||
setHours(value);
|
||||
handleChange(value, minutes);
|
||||
}
|
||||
|
||||
function handleMinuteChange(value) {
|
||||
setMinutes(value);
|
||||
handleChange(hours, value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={ createClassName([ 'd-inline-flex flex-column gap-1', className ]) }>
|
||||
<div>{ title }</div>
|
||||
<div className="d-inline-flex gap-2 align-items-center">
|
||||
<TimePart value={ hours }
|
||||
placeholder="HH"
|
||||
min={ 0 }
|
||||
max={ 23 }
|
||||
onChange={ handleHourChange } />
|
||||
:
|
||||
<TimePart value={ minutes }
|
||||
placeholder="MM"
|
||||
min={ 0 }
|
||||
max={ 59 }
|
||||
onChange={ handleMinuteChange } />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TimePart({ value, placeholder, onChange, min, max }) {
|
||||
function handleChange(newValueString) {
|
||||
if (newValueString !== '') {
|
||||
if (isNaN(newValueString)) {
|
||||
onChange('');
|
||||
return;
|
||||
}
|
||||
|
||||
const newValue = parseInt(newValueString);
|
||||
|
||||
if (newValue <= max && newValue >= min) {
|
||||
onChange(newValue)
|
||||
}
|
||||
} else {
|
||||
onChange('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<input className="border py-1 px-2 text-center rounded-1"
|
||||
value={ value && value !== '' && parseInt(value) }
|
||||
placeholder={ placeholder }
|
||||
onChange={ e => handleChange(e.target.value) }
|
||||
size={ 3 }
|
||||
min={ min }
|
||||
max={ max } />
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const SERVER_URL = 'http://localhost:8080';
|
||||
|
||||
export const ROLES = {
|
||||
ROLE_SUPER_ADMIN : "ROLE_SUPER_ADMIN",
|
||||
ROLE_BRANCH_ADMIN : "ROLE_BRANCH_ADMIN"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
const SortContext = createContext({ onSort: () => {} });
|
||||
|
||||
export function useSortContext() {
|
||||
return useContext(SortContext);
|
||||
}
|
||||
|
||||
export function SortContextProvider({ onSort, children }) {
|
||||
return (
|
||||
<SortContext.Provider value={{ onSort }}>
|
||||
{ children }
|
||||
</SortContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const UserContext = createContext();
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
Koristiti ovu funkciju za fetchanje u buducnosti kad god je to moguce.
|
||||
*/
|
||||
export async function fetchData(url, method, body) {
|
||||
const headers = new Headers();
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (token) {
|
||||
headers.append('Authorization', `Bearer ${ token }`);
|
||||
}
|
||||
|
||||
headers.append('Content-Type', 'application/json');
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: method || 'GET',
|
||||
headers: headers,
|
||||
body: body ? JSON.stringify(body) : null
|
||||
});
|
||||
|
||||
if (!res) {
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
const data = res.ok && res.body ? await res.json() : null;
|
||||
|
||||
if (res.ok) {
|
||||
//na svaki ispravan rezultat treba da dobijemo novi token da refreshamo stari
|
||||
const newToken = res.headers.get('Auth-Token');
|
||||
if (newToken) {
|
||||
localStorage.setItem('token', newToken);
|
||||
}
|
||||
}
|
||||
|
||||
return { data: data, success: res.ok };
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* A hook for using the native JS interval API. The interval is released after the user component dismounts.
|
||||
* @param callback function to be executed
|
||||
* @param period interval between executions of the callback function. The first execution happens instantly.
|
||||
* @param deps {array} optional array of dependencies
|
||||
*/
|
||||
export function useInterval(callback, period, deps) {
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => callback(), period)
|
||||
return () => clearInterval(interval);
|
||||
}, [ period, ...deps ]);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export function useSortSearchState(column) {
|
||||
const [ search, setSearch ] = useSearchParams();
|
||||
|
||||
function serializeToUrl(column, direction) {
|
||||
return `${column},${direction}`;
|
||||
}
|
||||
|
||||
function deserializeFromUrl(sortQueryParam) {
|
||||
if (sortQueryParam) {
|
||||
const sortParts = sortQueryParam.split(',')
|
||||
return {
|
||||
column: sortParts[0],
|
||||
direction: sortParts[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function searchContainsColumn(column) {
|
||||
const sortParam = getCurrentSort();
|
||||
|
||||
if (sortParam) {
|
||||
return deserializeFromUrl(sortParam).column === column;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function changeSortDirection() {
|
||||
const sortParam = getCurrentSort();
|
||||
|
||||
if (sortParam) {
|
||||
const { column, direction } = deserializeFromUrl(sortParam);
|
||||
|
||||
if (direction === 'asc') {
|
||||
setSortDirection(column, 'desc');
|
||||
} else if (direction === 'desc') {
|
||||
removeSort()
|
||||
} else {
|
||||
setSortDirection(column, 'asc');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setSortDirection(column, direction = 'asc') {
|
||||
search.set('sort', serializeToUrl(column, direction));
|
||||
setSearch(prev => search);
|
||||
}
|
||||
|
||||
function getCurrentSort() {
|
||||
return search.get('sort');
|
||||
}
|
||||
|
||||
function removeSort() {
|
||||
search.delete('sort');
|
||||
setSearch(prev => search)
|
||||
}
|
||||
|
||||
function updateSort(column) {
|
||||
if (searchContainsColumn(column)) {
|
||||
changeSortDirection();
|
||||
} else {
|
||||
setSortDirection(column, 'asc');
|
||||
}
|
||||
return getCurrentSort();
|
||||
}
|
||||
|
||||
return { sort: getCurrentSort(), updateSort: () => updateSort(column) };
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'General Sans', sans-serif;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
padding: 0 20px;
|
||||
background-color: ghostwhite;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* ovdje definisite konstante boje i sl. koje cete koristiti na vise mjesta */
|
||||
--blue: #334257;
|
||||
--light-blue: #548CA8;
|
||||
--dark-blue: #476072;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { GoogleOAuthProvider } from "@react-oauth/google";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import App from "./App";
|
||||
//import ManageServices from "./pages/ManageServices/ManageServices";
|
||||
import "./index.css";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||||
<React.StrictMode>
|
||||
<GoogleOAuthProvider clientId="776973117081-smp0drfulvkjk8s55ifr2i7k3uklpr04.apps.googleusercontent.com">
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</GoogleOAuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
@@ -0,0 +1,232 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Modal, Form } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
const styles = {
|
||||
primaryButton: {
|
||||
backgroundColor: "var(--light-blue)",
|
||||
borderColor: "var(--light-blue)",
|
||||
},
|
||||
infoButton: {
|
||||
backgroundColor: "var(--light-blue)",
|
||||
color: 'white',
|
||||
borderColor: "var(--light-blue)",
|
||||
},
|
||||
modalHeader: {
|
||||
backgroundColor: "var(--blue)",
|
||||
color: 'white',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const AdminManageScreen = () => {
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [admins, setAdmins] = useState([]);
|
||||
const [adminEmail, setAdminEmail] = useState('');
|
||||
const [adminPassword, setAdminPassword] = useState('');
|
||||
const [selectedAdminIndex, setSelectedAdminIndex] = useState(null);
|
||||
const [token, setToken] = useState('');
|
||||
const [emailError, setEmailError] = useState('');
|
||||
const [passwordError, setPasswordError] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const storedToken = localStorage.getItem('token');
|
||||
if (storedToken) {
|
||||
setToken(storedToken);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
fetchAdmins();
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const fetchAdmins = async () => {
|
||||
try {
|
||||
const requestBody = JSON.stringify({
|
||||
roleName: 'ROLE_BRANCH_ADMIN'
|
||||
});
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAdmins(data);
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const validateEmail = (email) => {
|
||||
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return re.test(String(email).toLowerCase());
|
||||
};
|
||||
|
||||
const handleAddAdmin = async () => {
|
||||
const requestBody = {
|
||||
email: adminEmail,
|
||||
password: adminPassword,
|
||||
roleName: 'ROLE_BRANCH_ADMIN'
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setAdmins([...admins, data]);
|
||||
setShowModal(false);
|
||||
setAdminEmail('');
|
||||
setAdminPassword('');
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditAdmin = async () => {
|
||||
if (!validateEmail(adminEmail)) {
|
||||
setEmailError('Invalid email address');
|
||||
return;
|
||||
}
|
||||
|
||||
setEmailError('');
|
||||
|
||||
try {
|
||||
const updatedAdmin = {
|
||||
email: adminEmail
|
||||
};
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${admins[selectedAdminIndex].id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
},
|
||||
body: JSON.stringify(updatedAdmin),
|
||||
});
|
||||
if (response.ok) {
|
||||
const updatedAdmins = [...admins];
|
||||
updatedAdmins[selectedAdminIndex].email = adminEmail;
|
||||
setAdmins(updatedAdmins);
|
||||
setShowModal(false);
|
||||
setAdminEmail('');
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteAdmin = async (userId) => {
|
||||
try {
|
||||
const response = await fetch(`${ SERVER_URL }/api/v1/admin/${tenantCode}/user/${userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token
|
||||
}
|
||||
});
|
||||
if (response.ok) {
|
||||
const updatedAdmins = admins.filter(admin => admin.id !== userId);
|
||||
setAdmins(updatedAdmins);
|
||||
} else {
|
||||
console.error('Unsuccessful API call');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error while making API call:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditClick = (index) => {
|
||||
const admin = admins[index];
|
||||
setSelectedAdminIndex(index);
|
||||
setAdminEmail(admin.email);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center mt-5">
|
||||
<h2>Manage Administrators</h2>
|
||||
<Button variant="primary" style={styles.primaryButton} className="mb-3" onClick={() => { setShowModal(true); setSelectedAdminIndex(null); }}>Add Admin</Button>
|
||||
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Email</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{admins.map((admin, index) => (
|
||||
<tr key={index}>
|
||||
<td>{admin.id}</td>
|
||||
<td>{admin.email}</td>
|
||||
<td>
|
||||
<Button variant="info" style={styles.infoButton} onClick={() => handleEditClick(index)}>Edit</Button>{' '}
|
||||
<Button variant="danger" onClick={() => handleDeleteAdmin(admin.id)}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Modal show={showModal} onHide={() => { setShowModal(false); setSelectedAdminIndex(null); }}>
|
||||
<Modal.Header closeButton style={styles.modalHeader}>
|
||||
<Modal.Title>{selectedAdminIndex !== null ? 'EDIT ADMIN' : 'ADD ADMIN'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group controlId="formAdminEmail" className="mb-3">
|
||||
<Form.Label>Email</Form.Label>
|
||||
<Form.Control type="email" placeholder="Enter admin email" value={adminEmail} onChange={(e) => setAdminEmail(e.target.value)} />
|
||||
{emailError && <div style={{ color: 'red' }}>{emailError}</div>}
|
||||
</Form.Group>
|
||||
{selectedAdminIndex === null && (
|
||||
<Form.Group controlId="formAdminPassword" className="mb-3">
|
||||
<Form.Label>Password</Form.Label>
|
||||
<Form.Control type="password" placeholder="Enter admin password" value={adminPassword} onChange={(e) => setAdminPassword(e.target.value)} />
|
||||
{passwordError && <div style={{ color: 'red' }}>{passwordError}</div>}
|
||||
</Form.Group>
|
||||
)}
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => { setShowModal(false); setSelectedAdminIndex(null); }}>Close</Button>
|
||||
{selectedAdminIndex !== null ?
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleEditAdmin}>Save Changes</Button> :
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleAddAdmin}>Add Admin</Button>
|
||||
}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminManageScreen;
|
||||
@@ -0,0 +1,45 @@
|
||||
#account-settings{
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
#check-box{
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
#check-box, label{
|
||||
margin-bottom: 20px;
|
||||
padding-right: 10px;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
background-color: #548CA8;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#QR-code input[type="submit"][disabled] {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#QR-code img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
display: block;
|
||||
margin-left: 0;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { SERVER_URL } from '../../constants';
|
||||
import "./AdminProfile.css"
|
||||
|
||||
export default function AdminProfile(){
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isQRCodeEnabled, setIsQRCodeEnabled] = useState(false);
|
||||
const [qrCodeSrc, setQrCodeSrc] = useState('');
|
||||
const [userData, setUserData] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const storedUserData = localStorage.getItem('userData');
|
||||
setUserData(JSON.parse(storedUserData));
|
||||
|
||||
const storedIsTfa = localStorage.getItem('isTfa');
|
||||
let isTfa = JSON.parse(storedIsTfa);
|
||||
|
||||
setIsChecked(isTfa);
|
||||
setIsQRCodeEnabled(isTfa);
|
||||
}, []);
|
||||
|
||||
const handleSaveChanges = async () =>{
|
||||
const url = `${ SERVER_URL }/api/v1/auth/tfa`;
|
||||
const { data, success } = await fetchData(url, 'PUT', {
|
||||
isTfa: isChecked
|
||||
});
|
||||
localStorage.setItem('isTfa', isChecked);
|
||||
setIsQRCodeEnabled(isChecked);
|
||||
if(!isChecked){
|
||||
setQrCodeSrc('');
|
||||
}
|
||||
if(success){
|
||||
let message = 'Success: Your changes have been successfully submitted.';
|
||||
if(isChecked){
|
||||
message = message + '\nPlease scan QR code.';
|
||||
}
|
||||
alert(message);
|
||||
}else{
|
||||
alert('An error occurred. Please try again.');
|
||||
}
|
||||
}
|
||||
|
||||
const handleCheckBoxChange = () =>{
|
||||
setIsChecked(!isChecked);
|
||||
}
|
||||
|
||||
const handleGenerateQRCode = () =>{
|
||||
if(isQRCodeEnabled){
|
||||
const url = `${ SERVER_URL }/api/v1/auth/tfa?email=${userData.email}`;
|
||||
fetchData(url, 'GET')
|
||||
.then(({ data, success }) => {
|
||||
if (success) {
|
||||
setQrCodeSrc(data.qrCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="account-settings">
|
||||
<h1>Account settings</h1>
|
||||
<div id="check-box">
|
||||
<form>
|
||||
<label htmlFor="2fa">Use two-factor authentication:</label>
|
||||
<input type="checkbox" id="2fa" name="2fa" checked={isChecked} onChange={handleCheckBoxChange}></input>
|
||||
</form>
|
||||
</div>
|
||||
<div id="QR-code">
|
||||
<input type="submit" value="Generate QR code" disabled={!isQRCodeEnabled} onClick={handleGenerateQRCode}/>
|
||||
<img src={qrCodeSrc}></img>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Save changes" onClick={handleSaveChanges}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
.company-info-update-wrapper {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.heading2 {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-bottom: 2px;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.form-container-comp {
|
||||
width: 100%;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
background-color: #ffff;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.form-group-comp {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.labels-comp {
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
color: #666362;
|
||||
font-family: "Poppins", sans-serif;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.inputs-comp[type="text"],
|
||||
.inputs-comp[type="file"],
|
||||
textarea,
|
||||
select {
|
||||
width: 80%;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
text-indent: 5px;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.inputs-comp[type="file"] {
|
||||
width: 80%;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
margin-top: 0.03%;
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
margin: 10px auto 0;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.left-column .labels-comp {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.right-column .form-group-comp {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.right-column .labels-comp {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/*
|
||||
?????
|
||||
*/
|
||||
.right-column .inputs-comp,
|
||||
.right-column textarea,
|
||||
.right-column select {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
|
||||
button[type="submit"]:hover {
|
||||
background-color: #476072;
|
||||
/
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
}
|
||||
|
||||
.welcome-message {
|
||||
width: 40%;
|
||||
height: 100px;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
max-height: 200px;
|
||||
border: solid 1px black;
|
||||
}
|
||||
|
||||
.welcome-message > p {
|
||||
padding: 4px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.company-info-submit-btn {
|
||||
display: flex;
|
||||
margin: 10px auto;
|
||||
background-color: var(--blue);
|
||||
padding: 10px 40px;
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
width: 12%;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,153 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import './CompanyInfoUpdate.css';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
|
||||
export default function CompanyInfoUpdate() {
|
||||
const navigate = useNavigate();
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const [name, setName] = useState('');
|
||||
const [hqAddress, setHQAddress] = useState('');
|
||||
const [welcomeMessage, setWelcomeMessage] = useState('');
|
||||
const [font, setFont] = useState('Arial');
|
||||
const [file, setFile] = useState('');
|
||||
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
useEffect(() => {
|
||||
// Na pocetku popunimo polja sa vec postojecim podacima
|
||||
const url = `${ SERVER_URL }/api/v1/tenants/${ tenantCode }`;
|
||||
|
||||
fetchData(url, 'GET')
|
||||
.then(({ data, success }) => {
|
||||
if (success) {
|
||||
setName(data.name);
|
||||
setHQAddress(data.hqAddress);
|
||||
setWelcomeMessage(data.welcomeMessage);
|
||||
setFont(data.font);
|
||||
setFile(data.logo.base64Logo);
|
||||
}
|
||||
});
|
||||
|
||||
}, []);
|
||||
|
||||
function handleChange(e) {
|
||||
const file = e.target.files[0];
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = () => {
|
||||
setFile(reader.result);
|
||||
};
|
||||
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
|
||||
async function submitForm() {
|
||||
try {
|
||||
const url = `${ SERVER_URL }/api/v1/tenants/${ tenantCode }`;
|
||||
const { data, success } = await fetchData(url, 'PUT', {
|
||||
name: name,
|
||||
hqAddress: hqAddress,
|
||||
font: font,
|
||||
welcomeMessage: welcomeMessage,
|
||||
logo: file
|
||||
});
|
||||
|
||||
if (success) {
|
||||
alert('Success: Your changes have been successfully submitted.');
|
||||
} else {
|
||||
alert(`Error while trying to save your changes. Please try again.`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error: An error occurred while submitting your changes.');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="company-info-update-wrapper">
|
||||
<div className="heading2">
|
||||
<h2>COMPANY DETAILS</h2>
|
||||
</div>
|
||||
<div className="form-container-comp">
|
||||
<div className="left-column">
|
||||
<form>
|
||||
<div className="form-group-comp">
|
||||
<label className="labels-comp" htmlFor="name" id="Name">Name</label>
|
||||
<input className="inputs-comp"
|
||||
type="text"
|
||||
id="name"
|
||||
value={ name }
|
||||
onChange={ (e) => setName(e.target.value) }
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group-comp">
|
||||
<label className="labels-comp" htmlFor="logo" id="Logo">Logo</label>
|
||||
<input className="inputs-comp"
|
||||
type="file"
|
||||
onChange={ handleChange }
|
||||
accept="image/*"
|
||||
required
|
||||
/>
|
||||
{ file && <img className="logo" src={ file } alt="Uploaded Logo" /> }
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="right-column">
|
||||
<form>
|
||||
<div className="form-group-comp">
|
||||
<label className="labels-comp" htmlFor="hqAddress">HQ Address</label>
|
||||
<input className="inputs-comp"
|
||||
type="text"
|
||||
id="hqAddress"
|
||||
value={ hqAddress }
|
||||
onChange={ (e) => setHQAddress(e.target.value) }
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group-comp">
|
||||
<label className="labels-comp" htmlFor="welcomeMessage">Welcome Message</label>
|
||||
<input className="inputs-comp"
|
||||
type="text"
|
||||
id="welcomeMessage"
|
||||
value={ welcomeMessage }
|
||||
onChange={ (e) => setWelcomeMessage(e.target.value) }
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="welcome-message">
|
||||
<p style={ { fontFamily: font } }>{ welcomeMessage }</p>
|
||||
</div>
|
||||
|
||||
<div className="fontSelect">
|
||||
<label className="labels-comp" htmlFor="font">Font</label>
|
||||
<select
|
||||
className="form-select"
|
||||
id="font"
|
||||
value={ font }
|
||||
onChange={ (e) => setFont(e.target.value) }
|
||||
required
|
||||
>
|
||||
<option value="Arial">Arial</option>
|
||||
<option value="Times New Roman">Times New Roman</option>
|
||||
<option value="Verdana">Verdana</option>
|
||||
<option value="Helvetica">Helvetica</option>
|
||||
<option value="Montserrat">Montserrat</option>
|
||||
<option value="Calibri">Calibri</option>
|
||||
<option value="Futura">Futura</option>
|
||||
<option value="Bodoni">Bodoni</option>
|
||||
<option value="Rockwell">Rockwell</option>
|
||||
<option value="Comic Sans MS">Comic Sans MS</option>
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" onClick={ submitForm } className="company-info-submit-btn">Submit</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
.h1-hp{
|
||||
text-align: center;
|
||||
margin-top: 1%;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
|
||||
import './HomePage.css'
|
||||
export default function HomePage() {
|
||||
|
||||
return (
|
||||
<main className="background-hp">
|
||||
<h1 className="h1-hp">Dashboard</h1>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
#login-form {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="text"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#registration {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#login-form {
|
||||
position: relative;
|
||||
width: 420px;
|
||||
max-width: 100%;
|
||||
margin: 150px auto 50px;
|
||||
background-color: #334257;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form h1 {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
padding: 20px 0;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
}
|
||||
|
||||
#login-form form {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-radius: 10px;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 14px;
|
||||
color: gray;
|
||||
font-family: "Poppins", sans-serif;
|
||||
}
|
||||
|
||||
#login-form form input[type="text"],
|
||||
#login-form form input[type="password"] {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid lightgray;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"] {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 12px;
|
||||
background-color: #548CA8;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
transition: background-color 0.3s ease;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#login-form form input[type="submit"]:hover {
|
||||
background-color: #334257;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
#registration {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: red;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
bottom: -20px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import validator from 'validator';
|
||||
import './LoginScreen.css';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
|
||||
export default function LoginScreen() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
if (!username.trim()) {
|
||||
setError('Username is required');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validator.isEmail(username) && !validator.isMobilePhone(username, 'any')) {
|
||||
setError('Invalid username format. Please enter a valid email or phone number.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password.trim()) {
|
||||
setError('Password is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = `${SERVER_URL}/api/v1/auth/login`;
|
||||
const { data, success } = await fetchData(url, 'POST', {
|
||||
email: username,
|
||||
password: password
|
||||
});
|
||||
|
||||
if (success) {
|
||||
if (data.userData == undefined) {
|
||||
localStorage.setItem('userData', JSON.stringify(data));
|
||||
} else {
|
||||
localStorage.setItem('userData', JSON.stringify(data.userData));
|
||||
localStorage.setItem('token', data.token);
|
||||
setUser(data.userData);
|
||||
}
|
||||
setIsSubmitted(true);
|
||||
|
||||
if (data.token) {
|
||||
localStorage.setItem('isTfa', false);
|
||||
navigate(`/${data.userData.tenantCode}/home`);
|
||||
} else {
|
||||
localStorage.setItem('isTfa', true);
|
||||
navigate('/loginauth');
|
||||
}
|
||||
} else {
|
||||
setError('Your credentials are incorrect.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setError('An error occurred. Please try again.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUsernameChange = (event) => {
|
||||
setUsername(event.target.value);
|
||||
setError('');
|
||||
};
|
||||
|
||||
const handlePasswordChange = (event) => {
|
||||
setPassword(event.target.value);
|
||||
setError('');
|
||||
};
|
||||
/*
|
||||
if (isSubmitted) {
|
||||
//navigate('/loginAuth');
|
||||
navigate('/companydetails');
|
||||
}*/
|
||||
|
||||
return (
|
||||
<div id="login-form">
|
||||
<h1>LOGIN</h1>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="form-group">
|
||||
<label htmlFor="username">Email or phone number:</label>
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
name="username"
|
||||
value={username}
|
||||
onChange={handleUsernameChange}
|
||||
/>
|
||||
{error && (error.includes('Username') || error.includes('Invalid')) &&
|
||||
<p className="error">{error}</p>}
|
||||
{error && (error.includes('credentials')) && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label htmlFor="password">Password:</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
value={password}
|
||||
onChange={handlePasswordChange}
|
||||
/>
|
||||
{error && error.includes('Password') && <p className="error">{error}</p>}
|
||||
</div>
|
||||
<input type="submit" value="Submit" />
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'center'
|
||||
}}>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,302 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Modal, Form, ListGroup } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
|
||||
const styles = {
|
||||
primaryButton: {
|
||||
backgroundColor: '#548CA8',
|
||||
borderColor: '#548CA8',
|
||||
},
|
||||
infoButton: {
|
||||
backgroundColor: '#548CA8',
|
||||
color: 'white',
|
||||
borderColor: '#548CA8',
|
||||
},
|
||||
modalHeader: {
|
||||
backgroundColor: '#334257',
|
||||
color: 'white',
|
||||
},
|
||||
};
|
||||
|
||||
const ManageBranchesScreen = () => {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [manageBranches, setManageBranches] = useState([]);
|
||||
const [branchName, setBranchName] = useState('');
|
||||
const [selectedBranchIndex, setSelectedBranchIndex] = useState(null);
|
||||
const [tellerStations, setTellerStations] = useState([]);
|
||||
const [newStationName, setNewStationName] = useState('');
|
||||
const [deleteConfirmation, setDeleteConfirmation] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const url = `${SERVER_URL}/api/v1/branches/${tenantCode}`;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchBranches = async () => {
|
||||
try {
|
||||
const response = await fetchData(url, 'GET');
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
setManageBranches(response.data);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to fetch branches.');
|
||||
}
|
||||
};
|
||||
fetchBranches();
|
||||
}, [SERVER_URL, url]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showModal) {
|
||||
setBranchName('');
|
||||
setTellerStations([]);
|
||||
setSelectedBranchIndex(null);
|
||||
setErrorMessage('');
|
||||
}
|
||||
}, [showModal]);
|
||||
|
||||
const handleEditClick = (index) => {
|
||||
const branch = manageBranches[index];
|
||||
setSelectedBranchIndex(index);
|
||||
setBranchName(branch.name);
|
||||
setTellerStations(branch.tellerStations ? [...branch.tellerStations] : []);
|
||||
setShowModal(true);
|
||||
};
|
||||
|
||||
const handleAddBranch = async () => {
|
||||
if (branchName.trim() === '' || tellerStations.length === 0) {
|
||||
setErrorMessage('Please fill out all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const newBranch = {
|
||||
name: branchName,
|
||||
tellerStations: tellerStations.map(station => station.name)
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchData(url, 'POST', newBranch);
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
setManageBranches([...manageBranches, response.data]);
|
||||
setShowModal(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to create branch.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddTellerStation = () => {
|
||||
if (newStationName.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStation = { id: tellerStations.length + 1, name: newStationName };
|
||||
setTellerStations([...tellerStations, newStation]);
|
||||
setNewStationName('');
|
||||
};
|
||||
|
||||
const handleEditTellerStation = async () => {
|
||||
if (newStationName.trim() === '' || selectedBranchIndex === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const branchId = manageBranches[selectedBranchIndex].id;
|
||||
const newStation = { name: newStationName };
|
||||
|
||||
try {
|
||||
const response = await fetchData(`${url}/${branchId}/stations`, 'POST', newStation);
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const updatedStations = [...tellerStations, response.data];
|
||||
setTellerStations(updatedStations);
|
||||
setNewStationName('');
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to add teller station.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveTellerStation = (index) => {
|
||||
const updatedStations = [...tellerStations];
|
||||
updatedStations.splice(index, 1);
|
||||
setTellerStations(updatedStations);
|
||||
};
|
||||
|
||||
const handleEditRemoveTellerStation = async (index) => {
|
||||
const stationToRemove = tellerStations[index];
|
||||
const branchId = manageBranches[selectedBranchIndex].id;
|
||||
const stationId = stationToRemove.id;
|
||||
|
||||
try {
|
||||
const response = await fetchData(`${url}/${branchId}/stations/${stationId}`, 'DELETE');
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
|
||||
const updatedStations = [...tellerStations];
|
||||
updatedStations.splice(index, 1);
|
||||
setTellerStations(updatedStations);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to remove teller station.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteBranch = (index) => {
|
||||
setDeleteConfirmation(true);
|
||||
setSelectedBranchIndex(index);
|
||||
};
|
||||
|
||||
const confirmDeleteBranch = async () => {
|
||||
const branchId = manageBranches[selectedBranchIndex].id;
|
||||
const urlToDelete = `${url}/${branchId}`;
|
||||
|
||||
try {
|
||||
const response = await fetchData(urlToDelete, 'DELETE');
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const updatedBranches = [...manageBranches];
|
||||
updatedBranches.splice(selectedBranchIndex, 1);
|
||||
setManageBranches(updatedBranches);
|
||||
setDeleteConfirmation(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to delete branch.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditBranch = async () => {
|
||||
if (branchName.trim() === '' || tellerStations.length === 0 || selectedBranchIndex === null) {
|
||||
setErrorMessage('Please fill out all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const branchId = manageBranches[selectedBranchIndex].id;
|
||||
const updatedBranch = {
|
||||
name: branchName
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetchData(`${url}/${branchId}`, 'PUT', updatedBranch);
|
||||
if (!response.success) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
const updatedBranches = [...manageBranches];
|
||||
updatedBranches[selectedBranchIndex] = response.data;
|
||||
setManageBranches(updatedBranches);
|
||||
setShowModal(false);
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
setErrorMessage('Failed to update branch.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Manage Branches</h2>
|
||||
<Button variant="primary" style={styles.primaryButton} className="mb-3" onClick={() => { setShowModal(true); setSelectedBranchIndex(null); }}>Add Branch</Button>
|
||||
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Teller Stations</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{manageBranches.map((branch, index) => (
|
||||
<tr key={index}>
|
||||
<td>{branch.id}</td>
|
||||
<td>{branch.name}</td>
|
||||
<td>{branch.tellerStations ? branch.tellerStations.map(station => station.name).join(', ') : '-'}</td>
|
||||
<td>
|
||||
<Button variant="info" style={styles.infoButton} onClick={() => handleEditClick(index)}>Edit</Button>{' '}
|
||||
<Button variant="danger" onClick={() => handleDeleteBranch(index)}>Delete</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Modal show={showModal} onHide={() => { setShowModal(false); setSelectedBranchIndex(null); }}>
|
||||
<Modal.Header closeButton style={styles.modalHeader}>
|
||||
<Modal.Title>{selectedBranchIndex !== null ? 'EDIT BRANCH' : 'ADD BRANCH'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group controlId="formBranchName" className="mb-3">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control type="text" placeholder="Enter branch name" value={branchName} onChange={(e) => setBranchName(e.target.value)} />
|
||||
</Form.Group>
|
||||
<Form.Group controlId="formNewStation" className="mb-3">
|
||||
<Form.Label>New Teller Station</Form.Label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Form.Control type="text" placeholder="Enter new station name" value={newStationName} onChange={(e) => setNewStationName(e.target.value)} />
|
||||
<Button variant="primary" onClick={selectedBranchIndex !== null ? handleEditTellerStation : handleAddTellerStation} style={{ backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8', marginLeft: '10px' }}>Add</Button>
|
||||
</div>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="formTellerStations" className="mb-3">
|
||||
<Form.Label>Teller Stations</Form.Label>
|
||||
<ListGroup>
|
||||
{tellerStations.map((station, index) => (
|
||||
<ListGroup.Item key={index}>
|
||||
{station.name}{' '}
|
||||
{selectedBranchIndex !== null ?
|
||||
<Button variant="danger" size="sm" onClick={() => handleEditRemoveTellerStation(index)}>X</Button> :
|
||||
<Button variant="danger" size="sm" onClick={() => handleRemoveTellerStation(index)}>X</Button>
|
||||
}
|
||||
</ListGroup.Item>
|
||||
))}
|
||||
</ListGroup>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => { setShowModal(false); setSelectedBranchIndex(null); }}>Close</Button>
|
||||
{selectedBranchIndex !== null ?
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleEditBranch}>Save Changes</Button> :
|
||||
<Button variant="primary" style={styles.primaryButton} onClick={handleAddBranch}>Add Branch</Button>
|
||||
}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={deleteConfirmation} onHide={() => setDeleteConfirmation(false)}>
|
||||
<Modal.Header closeButton style={{ backgroundColor: '#334257', color: 'white' }}>
|
||||
<Modal.Title>CONFIRMATION</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to delete this branch?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setDeleteConfirmation(false)}>Cancel</Button>
|
||||
<Button variant="danger" onClick={confirmDeleteBranch}>Delete</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={errorMessage !== ''} onHide={() => setErrorMessage('')} backdrop="static" keyboard={false}>
|
||||
<Modal.Header closeButton style={{ backgroundColor: '#dc3545', color: 'white' }}>
|
||||
<Modal.Title>ERROR</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>{errorMessage}</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => setErrorMessage('')}>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageBranchesScreen;
|
||||
@@ -0,0 +1,39 @@
|
||||
#root-displays {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#table-custom-displays {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-custom-displays {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.button-custom-blue-displays {
|
||||
background-color: var(--light-blue);
|
||||
}
|
||||
|
||||
.button-custom-blue-displays:hover {
|
||||
background-color: #476072;
|
||||
}
|
||||
|
||||
.button-custom-blue-displays:active {
|
||||
background-color: var(--blue) !important;
|
||||
}
|
||||
|
||||
#button-add-displays {
|
||||
width: 100%;
|
||||
max-width: 175px;
|
||||
}
|
||||
|
||||
.modal-custom-header-displays {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import Table from "react-bootstrap/Table";
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
import Form from "react-bootstrap/Form";
|
||||
import "./ManageDisplays.css";
|
||||
import { fetchData } from "../../fetching/Fetch";
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
|
||||
const ManageDisplays = () => {
|
||||
const { tenantCode } = useParams();
|
||||
const [displays, setDisplays] = useState([]);
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [selectedDisplayId, setSelectedDisplayId] = useState(-1);
|
||||
const [displayNameInput, setDisplayNameInput] = useState("");
|
||||
const [selectedBranchId, setSelectedBranchId] = useState(-1);
|
||||
const [branches, setBranches] = useState([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchData(`${SERVER_URL}/api/v1/branches/${tenantCode}`, "GET")
|
||||
.then((res) => {
|
||||
if (res.success) {
|
||||
setBranches(res.data);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching branches:", error);
|
||||
});
|
||||
}, [tenantCode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (branches.length !== 0) {
|
||||
setSelectedBranchId(branches[0].id);
|
||||
}
|
||||
}, [branches]);
|
||||
|
||||
const getDisplays = () => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/displays/${tenantCode}`,
|
||||
"GET"
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
setDisplays(res.data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addDisplay = (displayName, selectedBranchId) => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/displays/${tenantCode}`,
|
||||
"POST",
|
||||
{ name: displayName, branchId: selectedBranchId}
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getDisplays();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const editDisplay = (displayName) => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/displays/${tenantCode}/${selectedDisplayId}`,
|
||||
"PUT",
|
||||
{ name: displayName}
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getDisplays();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteDisplay = () => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/displays/${tenantCode}/${selectedDisplayId}`,
|
||||
"DELETE"
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getDisplays();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getDisplays();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="root-displays">
|
||||
<h2>Manage Displays</h2>
|
||||
<Table id="table-custom-displays" variant="light" striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Branch</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displays.map((display) => (
|
||||
<tr key={display.id}>
|
||||
<td>{display.id}</td>
|
||||
<td>{display.name}</td>
|
||||
<td>{display.branch.name}</td>
|
||||
<td>
|
||||
<Button
|
||||
id="button-edit"
|
||||
className="button-custom-displays button-custom-blue-displays"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setDisplayNameInput(display.name);
|
||||
setShowEdit(true);
|
||||
setSelectedDisplayId(display.id);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
id="button-delete"
|
||||
className="button-custom"
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
setSelectedDisplayId(display.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Button
|
||||
id="button-add-displays"
|
||||
className="button-custom-displays button-custom-blue-displays"
|
||||
variant="success"
|
||||
onClick={() => {
|
||||
setShowAdd(true);
|
||||
}}
|
||||
>
|
||||
Add Display
|
||||
</Button>
|
||||
<Modal show={showAdd}>
|
||||
<Modal.Header className="modal-custom-header-displays">
|
||||
<Modal.Title>Add Display</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group className="mb-3" controlId="formBasicEmail">
|
||||
<Form.Label>Display Name</Form.Label>
|
||||
<Form.Control
|
||||
value={displayNameInput}
|
||||
onChange={(e) => setDisplayNameInput(e.target.value)}
|
||||
type="text"
|
||||
placeholder="Enter Display Name"
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group className="mb-3" controlId="formBasicBranch">
|
||||
<Form.Label>Branch</Form.Label>
|
||||
<Form.Select value = {selectedBranchId} onChange = {(b) => {
|
||||
setSelectedBranchId(b.target.value);
|
||||
}}>
|
||||
{branches.map(branch => (
|
||||
<option key={branch.id} value={branch.id}>{branch.name}</option>
|
||||
))}
|
||||
</Form.Select>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDisplayNameInput("");
|
||||
setShowAdd(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="button-custom-blue-displays"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (displayNameInput === "") {
|
||||
alert("Display name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
setShowAdd(false);
|
||||
addDisplay(displayNameInput, selectedBranchId);
|
||||
setDisplayNameInput("");
|
||||
}}
|
||||
>
|
||||
Add Display
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showEdit}>
|
||||
<Modal.Header className="modal-custom-header">
|
||||
<Modal.Title>Edit Display</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group className="mb-3" controlId="formBasicEmail">
|
||||
<Form.Label>Display Name</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter Display Name"
|
||||
value={displayNameInput}
|
||||
onChange={(e) => setDisplayNameInput(e.target.value)}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDisplayNameInput("");
|
||||
setShowEdit(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="button-custom-blue-displays"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if (displayNameInput === "") {
|
||||
alert("Display name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
setShowEdit(false);
|
||||
editDisplay(displayNameInput, selectedBranchId);
|
||||
setDisplayNameInput("");
|
||||
}}
|
||||
>
|
||||
Edit Display
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showDelete}>
|
||||
<Modal.Header className="modal-custom-header">
|
||||
<Modal.Title>Delete Display</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to delete this display?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setDisplayNameInput("");
|
||||
setShowDelete(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setShowDelete(false);
|
||||
deleteDisplay();
|
||||
}}
|
||||
>
|
||||
Delete Display
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageDisplays;
|
||||
@@ -0,0 +1,345 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Modal, Form, Dropdown, DropdownButton } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
|
||||
export default function ManageGroupsScreen() {
|
||||
const [showModal, setShowModal] = useState(false);
|
||||
const [deleteConfirmation, setDeleteConfirmation] = useState(false);
|
||||
const [groups, setGroups] = useState([]);
|
||||
const [groupName, setGroupName] = useState('');
|
||||
const [selectedBranches, setSelectedBranches] = useState([]);
|
||||
const [selectedServices, setSelectedServices] = useState([]);
|
||||
const [availableBranches, setAvailableBranches] = useState([]);
|
||||
const [availableServices, setAvailableServices] = useState([]);
|
||||
const [selectedGroup, setSelectedGroup] = useState();
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchGroups();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!showModal) {
|
||||
// Reset modal state when it closes
|
||||
setGroupName('')
|
||||
setSelectedGroup(undefined)
|
||||
setSelectedBranches([])
|
||||
setSelectedServices([])
|
||||
setAvailableBranches([])
|
||||
setAvailableServices([])
|
||||
setErrorMessage('')
|
||||
}
|
||||
}, [showModal]);
|
||||
|
||||
const isValid = groupName.trim() !== '' && selectedBranches.length !== 0 && selectedServices.length !== 0
|
||||
|
||||
function fetchAvailableBranches(group) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ group.id }/assignable/branch`)
|
||||
.then(response => response.data)
|
||||
.then(setAvailableBranches)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchAvailableServices(group) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ group.id }/assignable/service`)
|
||||
.then(response => response.data)
|
||||
.then(setAvailableServices)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchGroups() {
|
||||
fetchData(`${ url }groups/${ tenantCode }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setGroups)
|
||||
}
|
||||
|
||||
function handleAddGroup() {
|
||||
if (!isValid) {
|
||||
setErrorMessage('Please fill out all fields.');
|
||||
return;
|
||||
}
|
||||
|
||||
const groupData = {
|
||||
name: groupName,
|
||||
branchIds: selectedBranches.map(branch => branch.id),
|
||||
serviceIds: selectedServices.map(service => service.id)
|
||||
};
|
||||
|
||||
fetchData(`${ url }groups/${ tenantCode }`, 'POST', groupData)
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
|
||||
setShowModal(false)
|
||||
}
|
||||
|
||||
function handleEditGroup() {
|
||||
if (!isValid) {
|
||||
setErrorMessage('Please fill out all fields.');
|
||||
return
|
||||
}
|
||||
|
||||
const updatedGroup = {
|
||||
...selectedGroup,
|
||||
name: groupName,
|
||||
branches: selectedBranches,
|
||||
services: selectedServices
|
||||
};
|
||||
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup?.id }`, 'PUT', updatedGroup)
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
|
||||
setShowModal(false)
|
||||
}
|
||||
|
||||
function handleDeleteGroup(group) {
|
||||
setSelectedGroup(group)
|
||||
setDeleteConfirmation(true);
|
||||
}
|
||||
|
||||
function confirmDeleteGroup() {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup?.id }`, 'DELETE')
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
|
||||
setDeleteConfirmation(false)
|
||||
setSelectedGroup(undefined)
|
||||
}
|
||||
|
||||
function handleEditClick(group) {
|
||||
setSelectedGroup(group)
|
||||
setGroupName(group.name)
|
||||
|
||||
setSelectedBranches(group.branches)
|
||||
setSelectedServices(group.services)
|
||||
|
||||
fetchAvailableBranches(group)
|
||||
fetchAvailableServices(group)
|
||||
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
function handleBranchSelection(branchId) {
|
||||
const branchToAdd = availableBranches.find(branch => branch.id == branchId)
|
||||
setSelectedBranches([ ...selectedBranches, branchToAdd ])
|
||||
|
||||
if (selectedGroup) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup.id }/branches/${ branchId }`, 'PUT')
|
||||
.then(() => fetchAvailableBranches(selectedGroup))
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
} else {
|
||||
const updatedAvailable = availableBranches.filter(branch => branch.id != branchId)
|
||||
setAvailableBranches(updatedAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveBranch(branch) {
|
||||
const updatedBranches = selectedBranches.filter(selectedBranch => selectedBranch.id !== branch.id);
|
||||
setSelectedBranches(updatedBranches);
|
||||
|
||||
if (selectedGroup) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup?.id }/branches/${ branch.id }`, 'DELETE')
|
||||
.then(() => fetchAvailableBranches(selectedGroup))
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
} else {
|
||||
setAvailableBranches([ ...availableBranches, branch ])
|
||||
}
|
||||
}
|
||||
|
||||
function handleServiceSelection(serviceId) {
|
||||
const serviceToAdd = availableServices.find(service => service.id == serviceId)
|
||||
setSelectedServices([ ...selectedServices, serviceToAdd ])
|
||||
|
||||
if (selectedGroup) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup.id }/services/${ serviceId }`, 'PUT')
|
||||
.then(() => fetchAvailableServices(selectedGroup))
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
} else {
|
||||
const updatedAvailable = availableServices.filter(service => service.id != serviceId)
|
||||
setAvailableServices(updatedAvailable)
|
||||
}
|
||||
}
|
||||
|
||||
function handleRemoveService(service) {
|
||||
const updatedServices = selectedServices.filter(selectedService => selectedService.id !== service.id);
|
||||
setSelectedServices(updatedServices);
|
||||
|
||||
if (selectedGroup) {
|
||||
fetchData(`${ url }groups/${ tenantCode }/${ selectedGroup?.id }/services/${ service.id }`, 'DELETE')
|
||||
.then(() => fetchAvailableServices(selectedGroup))
|
||||
.then(fetchGroups)
|
||||
.catch(console.error)
|
||||
} else {
|
||||
setAvailableServices([ ...availableServices, service ])
|
||||
}
|
||||
}
|
||||
|
||||
function onStartAddGroup() {
|
||||
setSelectedBranches([])
|
||||
setSelectedServices([])
|
||||
|
||||
fetchData(`${ url }branches/${ tenantCode }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setAvailableBranches)
|
||||
.catch(console.error)
|
||||
|
||||
fetchData(`${ url }tenants/${ tenantCode }/services`)
|
||||
.then(response => response.data)
|
||||
.then(setAvailableServices)
|
||||
.catch(console.error)
|
||||
|
||||
setShowModal(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Groups of Branches</h2>
|
||||
<Button variant="primary" style={ { backgroundColor: '#548CA8', borderColor: '#548CA8' } } className="mb-3"
|
||||
onClick={ onStartAddGroup }>Add Group</Button>
|
||||
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Branches</th>
|
||||
<th>Services</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ groups.map((group, index) => (
|
||||
<tr key={ index }>
|
||||
<td>{ group.id }</td>
|
||||
<td>{ group.name }</td>
|
||||
<td>{ group.branches.map(branch => branch.name).join(', ') }</td>
|
||||
<td>{ group.services.map(service => service.name).join(', ') }</td>
|
||||
<td className="d-flex justify-content-center gap-2">
|
||||
<Button variant="info"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ () => handleEditClick(group) }>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<Button variant="danger"
|
||||
onClick={ () => handleDeleteGroup(group) }>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)) }
|
||||
</tbody>
|
||||
</Table>
|
||||
|
||||
<Modal show={ showModal } onHide={ () => {
|
||||
setShowModal(false);
|
||||
} }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#334257', color: 'white' } }>
|
||||
<Modal.Title>{ selectedGroup ? 'EDIT GROUP' : 'ADD GROUP' }</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group controlId="formGroupName" className="mb-3">
|
||||
<Form.Label>Name</Form.Label>
|
||||
<Form.Control type="text"
|
||||
placeholder="Enter group name"
|
||||
value={ groupName }
|
||||
onChange={ (e) => setGroupName(e.target.value) } />
|
||||
</Form.Group>
|
||||
<Form.Group controlId="formGroupBranches" className="mb-3">
|
||||
{ selectedBranches.map((branch, index) => (
|
||||
<span key={ index } className="badge bg-secondary m-1"
|
||||
style={ { display: 'inline-flex', alignItems: 'center' } }>
|
||||
{ branch.name }{ ' ' }
|
||||
<Button variant="danger" size="sm" style={ {
|
||||
marginLeft: '5px',
|
||||
padding: '2px 5px',
|
||||
backgroundColor: '#dc3545',
|
||||
borderColor: '#dc3545'
|
||||
} } onClick={ () => handleRemoveBranch(branch) }>X</Button>
|
||||
</span>
|
||||
)) }
|
||||
<DropdownButton
|
||||
title="Select Branches"
|
||||
onSelect={ (eventKey) => handleBranchSelection(eventKey) }
|
||||
variant="btn btn-outline-secondary"
|
||||
>
|
||||
{ availableBranches.map((branch, index) => (
|
||||
<Dropdown.Item key={ index } eventKey={ branch.id }>{ branch.name }</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="formGroupServices" className="mb-3">
|
||||
{ selectedServices.map((service, index) => (
|
||||
<span key={ index } className="badge bg-secondary m-1"
|
||||
style={ { display: 'inline-flex', alignItems: 'center' } }>
|
||||
{ service.name }{ ' ' }
|
||||
<Button variant="danger" size="sm" style={ {
|
||||
marginLeft: '5px',
|
||||
padding: '2px 5px',
|
||||
backgroundColor: '#dc3545',
|
||||
borderColor: '#dc3545'
|
||||
} } onClick={ () => handleRemoveService(service) }>X</Button>
|
||||
</span>
|
||||
)) }
|
||||
<DropdownButton
|
||||
title="Select Services"
|
||||
onSelect={ (eventKey) => handleServiceSelection(eventKey) }
|
||||
variant="btn btn-outline-secondary"
|
||||
>
|
||||
{ availableServices.map((service, index) => (
|
||||
<Dropdown.Item key={ index }
|
||||
eventKey={ service.id }>{ service.name }</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{ selectedGroup ?
|
||||
<Button variant="primary" style={ { backgroundColor: '#548CA8', borderColor: '#548CA8' } }
|
||||
onClick={ handleEditGroup }>Save Changes</Button> :
|
||||
<Button variant="primary" style={ { backgroundColor: '#548CA8', borderColor: '#548CA8' } }
|
||||
onClick={ handleAddGroup }>Add Group</Button>
|
||||
}
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={ deleteConfirmation } onHide={ () => setDeleteConfirmation(false) }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#334257', color: 'white' } }>
|
||||
<Modal.Title>CONFIRMATION</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to delete this group?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={ () => setDeleteConfirmation(false) }>Cancel</Button>
|
||||
<Button variant="danger" onClick={ confirmDeleteGroup }>Delete</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={ errorMessage !== '' } onHide={ () => setErrorMessage('') } backdrop="static"
|
||||
keyboard={ false }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#dc3545', color: 'white' } }>
|
||||
<Modal.Title>ERROR</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>{ errorMessage }</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={ () => setErrorMessage('') }>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
#root-services {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#table-custom-services {
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-custom-services {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.button-custom-blue-services {
|
||||
background-color: var(--light-blue);
|
||||
}
|
||||
|
||||
.button-custom-blue-services:hover {
|
||||
background-color: #476072;
|
||||
}
|
||||
|
||||
.button-custom-blue-services:active {
|
||||
background-color: var(--blue) !important;
|
||||
}
|
||||
|
||||
#button-add-services {
|
||||
width: 100%;
|
||||
max-width: 175px;
|
||||
}
|
||||
|
||||
.modal-custom-header-services {
|
||||
background-color: var(--blue);
|
||||
color: white;
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import Header from "../../components/Header/Header";
|
||||
import Table from "react-bootstrap/Table";
|
||||
import Button from "react-bootstrap/Button";
|
||||
import Modal from "react-bootstrap/Modal";
|
||||
import Form from "react-bootstrap/Form";
|
||||
import "./ManageServices.css";
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { fetchData } from "../../fetching/Fetch";
|
||||
|
||||
const ManageServices = () => {
|
||||
const { tenantCode } = useParams();
|
||||
const [services, setServices] = useState([]);
|
||||
const [showAdd, setShowAdd] = useState(false);
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
const [showDelete, setShowDelete] = useState(false);
|
||||
const [selectedServiceId, setSelectedServiceId] = useState(-1);
|
||||
const [serviceNameInput, setServiceNameInput] = useState("");
|
||||
|
||||
|
||||
const getServices = () => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/tenants/${tenantCode}/services`,
|
||||
"GET"
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
setServices(res.data);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const addService = (serviceName) => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/tenants/${tenantCode}/services`,
|
||||
"POST",
|
||||
{ name: serviceName }
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getServices();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const editService = (serviceName) => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/tenants/${tenantCode}/services/${selectedServiceId}`,
|
||||
"PUT",
|
||||
{ name: serviceName }
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getServices();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const deleteService = () => {
|
||||
fetchData(
|
||||
`${SERVER_URL}/api/v1/tenants/${tenantCode}/services/${selectedServiceId}`,
|
||||
"DELETE"
|
||||
).then((res) => {
|
||||
if (res.success) {
|
||||
getServices();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getServices();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id="root-services">
|
||||
<h2>Manage Services</h2>
|
||||
<Table id="table-custom-services" variant="light" striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<th>Name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{services.map((service) => (
|
||||
<tr key={service.id}>
|
||||
<td>{service.id}</td>
|
||||
<td>{service.name}</td>
|
||||
<td>
|
||||
<Button
|
||||
id="button-edit"
|
||||
className="button-custom-services button-custom-blue-services"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
setServiceNameInput(service.name);
|
||||
setShowEdit(true);
|
||||
setSelectedServiceId(service.id);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
id="button-delete"
|
||||
className="button-custom"
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setShowDelete(true);
|
||||
setSelectedServiceId(service.id);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
<Button
|
||||
id="button-add-services"
|
||||
className="button-custom-services button-custom-blue-services"
|
||||
variant="success"
|
||||
onClick={() => {
|
||||
setShowAdd(true);
|
||||
}}
|
||||
>
|
||||
Add Service
|
||||
</Button>
|
||||
<Modal show={showAdd}>
|
||||
<Modal.Header className="modal-custom-header-services">
|
||||
<Modal.Title>Add Service</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group
|
||||
className="mb-3"
|
||||
controlId="formBasicEmail"
|
||||
>
|
||||
<Form.Label>Service Name</Form.Label>
|
||||
<Form.Control
|
||||
value={serviceNameInput}
|
||||
onChange={(e) =>
|
||||
setServiceNameInput(e.target.value)
|
||||
}
|
||||
type="text"
|
||||
placeholder="Enter Service Name"
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setServiceNameInput("");
|
||||
setShowAdd(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="button-custom-blue-services"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if(serviceNameInput === "") {
|
||||
alert("Service name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
setShowAdd(false);
|
||||
addService(serviceNameInput);
|
||||
setServiceNameInput("");
|
||||
}}
|
||||
>
|
||||
Add Service
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showEdit}>
|
||||
<Modal.Header className="modal-custom-header">
|
||||
<Modal.Title>Edit Service</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form>
|
||||
<Form.Group
|
||||
className="mb-3"
|
||||
controlId="formBasicEmail"
|
||||
>
|
||||
<Form.Label>Service Name</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
placeholder="Enter Service Name"
|
||||
value={serviceNameInput}
|
||||
onChange={(e) =>
|
||||
setServiceNameInput(e.target.value)
|
||||
}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setServiceNameInput("");
|
||||
setShowEdit(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
className="button-custom-blue-services"
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
if(serviceNameInput === "") {
|
||||
alert("Service name cannot be empty!");
|
||||
return;
|
||||
}
|
||||
setShowEdit(false);
|
||||
editService(serviceNameInput);
|
||||
setServiceNameInput("");
|
||||
}}
|
||||
>
|
||||
Edit Service
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={showDelete}>
|
||||
<Modal.Header className="modal-custom-header">
|
||||
<Modal.Title>Delete Service</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p>Are you sure you want to delete this service?</p>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setServiceNameInput("");
|
||||
setShowDelete(false);
|
||||
}}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => {
|
||||
setShowDelete(false);
|
||||
deleteService();
|
||||
}}
|
||||
>
|
||||
Delete Service
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageServices;
|
||||
@@ -0,0 +1,340 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Dropdown, DropdownButton, Form, Modal, Table } from 'react-bootstrap';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SERVER_URL } from '../../constants.js';
|
||||
import { fetchData } from '../../fetching/Fetch.js';
|
||||
|
||||
const ManageStationScreen = () => {
|
||||
const [showServiceModal, setShowServiceModal] = useState(false);
|
||||
const [showDisplayModal, setShowDisplayModal] = useState(false);
|
||||
const [stations, setStations] = useState([]);
|
||||
const [selectedStation, setSelectedStation] = useState(null);
|
||||
const [selectedBranch, setSelectedBranch] = useState(null);
|
||||
const [branches, setBranches] = useState([]);
|
||||
const [selectedServices, setSelectedServices] = useState([]);
|
||||
const [availableServices, setAvailableServices] = useState([]);
|
||||
const [availableDisplays, setAvailableDisplays] = useState([]);
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
const { tenantCode } = useParams();
|
||||
|
||||
const url = `${ SERVER_URL }/api/v1/`;
|
||||
|
||||
useEffect(() => {
|
||||
fetchBranches();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedBranch) {
|
||||
fetchStationsForBranch();
|
||||
fetchAvailableDisplays(selectedBranch);
|
||||
} else {
|
||||
setStations([]);
|
||||
}
|
||||
}, [selectedBranch]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedServices(selectedStation ? selectedStation.services : []);
|
||||
if (selectedStation) {
|
||||
fetchAvailableServices(selectedStation);
|
||||
}
|
||||
}, [selectedStation]);
|
||||
|
||||
function fetchBranches() {
|
||||
fetchData(`${ url }branches/${ tenantCode }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setBranches)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchStationsForBranch() {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ selectedBranch.id }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setStations)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchAvailableDisplays(branch) {
|
||||
fetchData(`${ url }displays/unassigned/${ tenantCode }/${ branch.id }`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setAvailableDisplays)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function fetchAssignableServices(station) {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ station.id }/services/assignable`)
|
||||
.then(response => response.data)
|
||||
.then(setAvailableServices)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
function onEditServices(station) {
|
||||
fetchAssignableServices(station)
|
||||
setShowServiceModal(true)
|
||||
setSelectedStation(station)
|
||||
setSelectedServices(station.services)
|
||||
}
|
||||
|
||||
function fetchAvailableServices(station) {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ station.id }/services?assigned=false`, 'GET')
|
||||
.then(response => response.data)
|
||||
.then(setAvailableServices)
|
||||
.catch(console.error)
|
||||
}
|
||||
|
||||
|
||||
function addService(service) {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ selectedStation.id }/services/${ service.id }`, 'PUT')
|
||||
.then(() => fetchAssignableServices(selectedStation))
|
||||
.then(fetchStationsForBranch)
|
||||
.catch(console.error)
|
||||
|
||||
setSelectedServices([...selectedServices, service]);
|
||||
}
|
||||
|
||||
function removeSelectedService(service) {
|
||||
fetchData(`${ url }stations/${ tenantCode }/${ selectedStation.id }/services/${ service.id }`, 'DELETE')
|
||||
.then(() => fetchAssignableServices(selectedStation))
|
||||
.then(fetchStationsForBranch)
|
||||
.catch(console.error)
|
||||
|
||||
const updatedServices = selectedServices.filter(selectedService => selectedService.id !== service.id)
|
||||
setSelectedServices(updatedServices)
|
||||
}
|
||||
|
||||
|
||||
// Ove dvije funkcije ispod isto treba ispraviti da se ne radi filtriranje na frontendu, ali nemam vise vremena.
|
||||
const addDisplayToStation = async (display) => {
|
||||
try {
|
||||
const response = await fetchData(`${ url }stations/${ tenantCode }/${ selectedStation.id }/displays/${ display.id }`, 'PUT');
|
||||
if (response.success) {
|
||||
const updatedStations = stations.map(station => {
|
||||
if (station.id === selectedStation.id) {
|
||||
if (station.display) {
|
||||
setAvailableDisplays(prevDisplays => [...prevDisplays, station.display]);
|
||||
}
|
||||
return {
|
||||
...station,
|
||||
display: display
|
||||
};
|
||||
}
|
||||
return station;
|
||||
});
|
||||
setStations(updatedStations);
|
||||
setAvailableDisplays(prevDisplays => prevDisplays.filter(d => d.id !== display.id));
|
||||
setSelectedStation(updatedStations.find(station => station.id === selectedStation.id));
|
||||
setShowDisplayModal(true);
|
||||
} else {
|
||||
console.error('Error adding display to station:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error adding display to station:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const removeDisplayFromStation = async () => {
|
||||
try {
|
||||
const response = await fetchData(`${ url }stations/${ tenantCode }/${ selectedStation.id }/displays/${ selectedStation.display.id }`, 'DELETE');
|
||||
if (response.success) {
|
||||
const updatedStations = stations.map(station => {
|
||||
if (station.id === selectedStation.id) {
|
||||
return {
|
||||
...station,
|
||||
display: null
|
||||
};
|
||||
}
|
||||
return station;
|
||||
});
|
||||
setStations(updatedStations);
|
||||
setAvailableDisplays(prevDisplays => [...prevDisplays, selectedStation.display]);
|
||||
setShowDisplayModal(false);
|
||||
} else {
|
||||
console.error('Error removing display from station:', response.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error removing display from station:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setSelectedServices(selectedStation ? selectedStation.services : []);
|
||||
setShowServiceModal(false);
|
||||
setShowDisplayModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="text-center">
|
||||
<h2>Teller Stations</h2>
|
||||
<DropdownButton title={ selectedBranch ? selectedBranch.name : 'Select Branch' }
|
||||
variant="btn btn-outline-secondary">
|
||||
{ branches.map((branch, index) => (
|
||||
<Dropdown.Item key={ index }
|
||||
onClick={ () => setSelectedBranch(branch) }>{ branch.name }</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
<div style={ { marginTop: '20px' } }>
|
||||
<Table striped bordered hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Station ID</th>
|
||||
<th>Station Name</th>
|
||||
<th>Services</th>
|
||||
<th>Service Action</th>
|
||||
<th>Displays</th>
|
||||
<th>Display Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ stations.map((station, index) => (
|
||||
<tr key={ index }>
|
||||
<td>{ station.id }</td>
|
||||
<td>{ station.name }</td>
|
||||
<td>
|
||||
{
|
||||
|
||||
station.services && station.services.length > 0 ? (
|
||||
station.services.map((service, serviceIndex) => (
|
||||
<span key={ serviceIndex } style={ { marginRight: '5px' } }>
|
||||
{ serviceIndex > 0 && ', ' }
|
||||
{ service.name }
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span>No services</span>
|
||||
) }
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ () => onEditServices(station) }
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</td>
|
||||
<td>
|
||||
{ station.display ? (
|
||||
<span>{ station.display.name }</span>
|
||||
) : (
|
||||
<span>No display</span>
|
||||
) }
|
||||
</td>
|
||||
<td>
|
||||
<Button
|
||||
variant="primary"
|
||||
style={ { backgroundColor: '#548CA8', color: 'white', borderColor: '#548CA8' } }
|
||||
onClick={ () => {
|
||||
setShowDisplayModal(true);
|
||||
setSelectedStation(station);
|
||||
} }
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
)) }
|
||||
</tbody>
|
||||
</Table>
|
||||
</div>
|
||||
<Modal show={ showServiceModal } onHide={ handleCloseModal }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#334257', color: 'white' } }>
|
||||
<Modal.Title>Manage Services for Station</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p><strong>Teller Station:</strong> { selectedStation && selectedStation.name }</p>
|
||||
<div>
|
||||
<div style={ { marginBottom: '10px' } }>
|
||||
<strong>Selected Services:</strong>
|
||||
{ selectedServices.length > 0 ? (
|
||||
selectedServices.map((service, index) => (
|
||||
<span key={ index } className="badge bg-secondary m-1"
|
||||
style={ { display: 'inline-flex', alignItems: 'center' } }>
|
||||
{ service.name }
|
||||
<Button variant="danger" size="sm" style={ {
|
||||
marginLeft: '5px',
|
||||
padding: '2px 5px',
|
||||
backgroundColor: '#dc3545',
|
||||
borderColor: '#dc3545'
|
||||
} } onClick={ () => removeSelectedService(service) }>X</Button>
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span> No service selected</span>
|
||||
) }
|
||||
</div>
|
||||
<Form.Group controlId="formGroupService" className="mb-3">
|
||||
<DropdownButton title={ 'Select Service' } variant="btn btn-outline-secondary">
|
||||
{ availableServices.map((service, index) => {
|
||||
const isAssigned = selectedServices.some(selectedService => selectedService.id === service.id);
|
||||
if (!isAssigned) {
|
||||
return (
|
||||
<Dropdown.Item key={ index }
|
||||
onClick={ () => addService(service) }>{ service.name }</Dropdown.Item>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}) }
|
||||
</DropdownButton>
|
||||
</Form.Group>
|
||||
|
||||
{ errorMessage && <p className="text-danger">{ errorMessage }</p> }
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={ handleCloseModal }>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
|
||||
<Modal show={ showDisplayModal } onHide={ handleCloseModal }>
|
||||
<Modal.Header closeButton style={ { backgroundColor: '#334257', color: 'white' } }>
|
||||
<Modal.Title>Manage Display for Station</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<p><strong>Teller Station:</strong> { selectedStation && selectedStation.name }</p>
|
||||
<div>
|
||||
<div style={ { marginBottom: '10px' } }>
|
||||
<strong>Selected Display:</strong>
|
||||
{ selectedStation && selectedStation.display ? (
|
||||
<span className="badge bg-secondary m-1" style={ {
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center'
|
||||
} }> { selectedStation.display.name }
|
||||
<Button variant="danger" size="sm" style={ {
|
||||
marginLeft: '5px',
|
||||
padding: '2px 5px',
|
||||
backgroundColor: '#dc3545',
|
||||
borderColor: '#dc3545'
|
||||
} } onClick={ removeDisplayFromStation }>X</Button>
|
||||
</span>
|
||||
) : (
|
||||
<span> No display selected</span>
|
||||
) }
|
||||
</div>
|
||||
<Form>
|
||||
<Form.Group controlId="formGroupDisplay" className="mb-3">
|
||||
<DropdownButton
|
||||
title={ 'Select Display' }
|
||||
variant="btn btn-outline-secondary"
|
||||
>
|
||||
{ availableDisplays.map((display, index) => (
|
||||
<Dropdown.Item key={ index }
|
||||
onClick={ () => addDisplayToStation(display) }>{ display.name }</Dropdown.Item>
|
||||
)) }
|
||||
</DropdownButton>
|
||||
|
||||
|
||||
</Form.Group>
|
||||
</Form>
|
||||
{ errorMessage && <p className="text-danger">{ errorMessage }</p> }
|
||||
</div>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={ handleCloseModal }>Close</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ManageStationScreen;
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { UserContext } from '../../context/UserContext.jsx';
|
||||
import {useContext} from "react";
|
||||
|
||||
export default function NotFound() {
|
||||
const navigate = useNavigate();
|
||||
const { user, setUser } = useContext(UserContext);
|
||||
|
||||
function handleHomeClick() {
|
||||
if (user) {
|
||||
navigate(`${user.tenantCode}/home`);
|
||||
} else {
|
||||
navigate('/login');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={ {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
gap: '10px'
|
||||
} }>
|
||||
<p>
|
||||
404 - The page does not exist.
|
||||
</p>
|
||||
<button style={ {
|
||||
width: 'fit-content',
|
||||
border: '1px solid blue',
|
||||
borderRadius: '5px',
|
||||
padding: '5px 30px',
|
||||
cursor: 'pointer'
|
||||
} }
|
||||
onClick={ handleHomeClick }>
|
||||
Go back home?
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color:ghostwhite;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||