DONE: first init
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
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": {}
|
||||
}
|
||||
}
|
||||
Generated
+18023
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user