Anul 3 Semestrul 1
@@ -0,0 +1,39 @@
|
||||
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
|
||||
|
||||
# dependencies
|
||||
node_modules/
|
||||
|
||||
# Expo
|
||||
.expo/
|
||||
dist/
|
||||
web-build/
|
||||
expo-env.d.ts
|
||||
|
||||
# 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
|
||||
|
||||
app-example
|
||||
android/
|
||||
@@ -0,0 +1,50 @@
|
||||
# Welcome to your Expo app 👋
|
||||
|
||||
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
|
||||
|
||||
## Get started
|
||||
|
||||
1. Install dependencies
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Start the app
|
||||
|
||||
```bash
|
||||
npx expo start
|
||||
```
|
||||
|
||||
In the output, you'll find options to open the app in a
|
||||
|
||||
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
|
||||
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
|
||||
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
|
||||
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
|
||||
|
||||
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
|
||||
|
||||
## Get a fresh project
|
||||
|
||||
When you're ready, run:
|
||||
|
||||
```bash
|
||||
npm run reset-project
|
||||
```
|
||||
|
||||
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
|
||||
|
||||
## Learn more
|
||||
|
||||
To learn more about developing your project with Expo, look at the following resources:
|
||||
|
||||
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
|
||||
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
|
||||
|
||||
## Join the community
|
||||
|
||||
Join our community of developers creating universal apps.
|
||||
|
||||
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
|
||||
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
|
||||
@@ -0,0 +1,72 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "MA-NonNative",
|
||||
"slug": "MA-NonNative",
|
||||
"version": "1.0.0",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "myapp",
|
||||
"userInterfaceStyle": "automatic",
|
||||
"newArchEnabled": true,
|
||||
"ios": {
|
||||
"supportsTablet": true
|
||||
},
|
||||
"android": {
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/adaptive-icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"package": "com.danielcujba54.MANonNative"
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
[
|
||||
"expo-splash-screen",
|
||||
{
|
||||
"image": "./assets/images/splash-icon.png",
|
||||
"imageWidth": 200,
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
}
|
||||
],
|
||||
["expo-sqlite",
|
||||
{
|
||||
"enableFTS": true,
|
||||
"useSQLCipher": true,
|
||||
"android": {
|
||||
"enableFTS": false,
|
||||
"useSQLCipher": false
|
||||
},
|
||||
"ios": {
|
||||
"customBuildFlags": ["-DSQLITE_ENABLE_DBSTAT_VTAB=1 -DSQLITE_ENABLE_SNAPSHOT=1"]
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"expo-build-properties",
|
||||
{
|
||||
"android": {
|
||||
"usesCleartextTraffic": true
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
},
|
||||
"extra": {
|
||||
"router": {
|
||||
"origin": false
|
||||
},
|
||||
"eas": {
|
||||
"projectId": "295efd7e-de78-4e54-b327-8eec9a3f9bf0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { store } from "@/redux/store";
|
||||
import { Stack } from "expo-router";
|
||||
import { Provider } from "react-redux";
|
||||
import { useMaterial3Theme } from "@pchmn/expo-material3-theme";
|
||||
import { MD3DarkTheme, MD3LightTheme, PaperProvider } from "react-native-paper";
|
||||
import { StatusBar, useColorScheme, Text } from "react-native";
|
||||
import { DataLoader } from "@/components/DataLoader";
|
||||
|
||||
|
||||
export default function RootLayout() {
|
||||
const colorScheme = useColorScheme();
|
||||
const {theme} = useMaterial3Theme();
|
||||
const paperTheme =
|
||||
colorScheme === 'dark'
|
||||
? { ...MD3DarkTheme, colors: theme.dark }
|
||||
: { ...MD3LightTheme, colors: theme.light };
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<DataLoader />
|
||||
<PaperProvider theme={paperTheme}>
|
||||
<StatusBar
|
||||
hidden={true}
|
||||
/>
|
||||
<Stack>
|
||||
<Stack.Screen name="index" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="add" options={{ headerShown: false }} />
|
||||
<Stack.Screen name="update/[id]" options={{ headerShown: false }} />
|
||||
</Stack>
|
||||
</PaperProvider>
|
||||
</Provider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import DrugList from "@/components/DrugList";
|
||||
import Form from "@/components/Form";
|
||||
import { Link } from "expo-router";
|
||||
import { Dimensions, Text, View } from "react-native";
|
||||
import { useTheme } from "react-native-paper";
|
||||
|
||||
export default function Add() {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<View style={{width: "100%", backgroundColor:theme.colors.surface, height: Dimensions.get('window').height}}>
|
||||
<Form />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import DrugList from "@/components/DrugList";
|
||||
import { Dimensions, Text, View } from "react-native";
|
||||
import { useTheme } from "react-native-paper";
|
||||
|
||||
|
||||
export default function Index() {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<View style={{width: "100%", backgroundColor:theme.colors.surface, height: Dimensions.get('window').height}}>
|
||||
<DrugList />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import Form from "@/components/Form";
|
||||
import { Link, useLocalSearchParams } from "expo-router";
|
||||
import { Dimensions, Text, View } from "react-native";
|
||||
import { useTheme } from "react-native-paper";
|
||||
|
||||
export default function Update() {
|
||||
const { id } = useLocalSearchParams() as { id: string };
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<View style={{width: "100%", backgroundColor:theme.colors.surface, height: Dimensions.get('window').height}}>
|
||||
<Form drugId={parseInt(id)} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,38 @@
|
||||
import { RootState } from "@/redux/store";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Snackbar } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
export const ConnectionNotification = () => {
|
||||
const dispatch = useDispatch();
|
||||
const status = useSelector((state: RootState) => state.status);
|
||||
const [visible, setVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!status.isConnected || !status.isOnline) {
|
||||
setVisible(true);
|
||||
}
|
||||
else{
|
||||
setVisible(false);
|
||||
}
|
||||
}, [status.isConnected, status.isOnline]);
|
||||
const onDismiss = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
return (
|
||||
<Snackbar
|
||||
wrapperStyle={{ top: 50, left: "50%", position: 'absolute', transform: [{ translateX: "-42%" }], zIndex: 9999}}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={{ zIndex: 9999, }}
|
||||
action={{
|
||||
label: 'Dismiss',
|
||||
onPress: () => {
|
||||
setVisible(false);
|
||||
},
|
||||
}}
|
||||
duration={60000}
|
||||
>
|
||||
No internet connection
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { initDB } from "@/database/localdb";
|
||||
import { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
|
||||
export const DataLoader = () => {
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
dispatch({ type: 'drug/initialize' });
|
||||
dispatch({ type: 'drug/fetch' });
|
||||
};
|
||||
fetchData();
|
||||
}, [dispatch]);
|
||||
return (<View></View>);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
import React from "react";
|
||||
import { Button, Dialog, MD3Theme, Portal, useTheme, Text } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
export default function DeletePopUp(props: {drugId: number, onCancel: () => void, onDelete: () => void}) {
|
||||
const theme = useTheme();
|
||||
const style = styles(theme);
|
||||
const navigation = useNavigation<any>(); //TODO: types...
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={true}
|
||||
>
|
||||
<Dialog.Title>Delete Drug</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Text variant="bodyMedium">Are you sure you want to delete this drug? This action can not be reversed</Text>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions>
|
||||
<Button contentStyle={{paddingHorizontal: 8}} mode="contained" onPress={props.onCancel}>Cancel</Button>
|
||||
<Button contentStyle={{paddingHorizontal: 8}} mode="contained" onPress={props.onDelete}>Delete</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = (theme: MD3Theme) => StyleSheet.create({
|
||||
|
||||
})
|
||||
@@ -0,0 +1,204 @@
|
||||
import { Drug } from "@/redux/features/drug/drug";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { Button, Divider, MD3Theme, useTheme } from "react-native-paper";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
|
||||
export default function DrugDisplay(props: {drug: Drug, onUpdate: (drugId: number) => void, onDelete: (drugId: number) => void}) {
|
||||
const theme = useTheme();
|
||||
const style = styles(theme);
|
||||
const status = useSelector((state: RootState) => state.status);
|
||||
|
||||
return (
|
||||
<View className="box" style={style.outerbox}>
|
||||
<View className="box" style={style.boxBackground} >
|
||||
|
||||
</View>
|
||||
<View className="column" style={style.boxForeground}>
|
||||
<View style={style.rowHeadlineMargin}>
|
||||
<View className="row" style={style.rowHeadline}>
|
||||
<View className="column" style={style.column}>
|
||||
<Text style={style.nameText}>{props.drug.name}</Text>
|
||||
<Text style={style.manufacturerText}>{props.drug.manufacturer}</Text>
|
||||
</View>
|
||||
<View className="row" style={style.rowCTA}>
|
||||
<Button disabled={!status.isConnected || !status.isOnline} contentStyle={{paddingHorizontal: 8}} textColor={theme.colors.primary} buttonColor={theme.colors.surface} style={style.editButton} onPress={() => props.onUpdate(props.drug.id)}>Edit</Button>
|
||||
<Button contentStyle={{paddingHorizontal: 8}} textColor={theme.colors.onPrimary} buttonColor={theme.colors.primary} onPress={() => props.onDelete(props.drug.id)} >Delete</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Divider style={style.horizontalDivider} />
|
||||
<View style={style.chipMargin}>
|
||||
<View className="row" style={style.chip}>
|
||||
<View className="box" style={style.labelChip}>
|
||||
<Text style={style.label}>Category:</Text>
|
||||
</View>
|
||||
<View className="box" style={style.valueChip}>
|
||||
<Text style={style.value}>{props.drug.category}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.chipMargin}>
|
||||
<View className="row" style={style.chip}>
|
||||
<View className="box" style={style.labelChip}>
|
||||
<Text style={style.label}># of Units:</Text>
|
||||
</View>
|
||||
<View className="box" style={style.valueChip}>
|
||||
<Text style={style.value}>{props.drug.numberOfUnits.toFixed(0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{...style.chipMargin, ...style.lastChipMargin}}>
|
||||
<View className="row" style={style.chip}>
|
||||
<View className="box" style={style.labelChip}>
|
||||
<Text style={style.label}>Price:</Text>
|
||||
</View>
|
||||
<View className="box" style={style.valueChip}>
|
||||
<Text style={style.value}>${props.drug.price.toFixed(2)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = (theme: MD3Theme) => StyleSheet.create({
|
||||
boxBackground: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderRadius: 12,
|
||||
borderColor: theme.colors.outlineVariant,
|
||||
borderWidth: 1,
|
||||
zIndex: 10,
|
||||
},
|
||||
boxForeground: {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 20,
|
||||
|
||||
},
|
||||
outerbox: {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
marginBottom: 8
|
||||
},
|
||||
rowHeadlineMargin:{
|
||||
marginTop: 16,
|
||||
marginRight: 16,
|
||||
marginBottom: 7,
|
||||
marginLeft: 16,
|
||||
},
|
||||
rowHeadline: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
height: 44,
|
||||
|
||||
},
|
||||
column:{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
rowCTA: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
},
|
||||
nameText:{
|
||||
color: theme.colors.onSurface,
|
||||
fontFamily: theme.fonts.bodyLarge.fontFamily,
|
||||
fontSize: theme.fonts.bodyLarge.fontSize,
|
||||
lineHeight: theme.fonts.bodyLarge.lineHeight,
|
||||
letterSpacing: theme.fonts.bodyLarge.letterSpacing,
|
||||
fontWeight: theme.fonts.bodyLarge.fontWeight,
|
||||
fontStyle: theme.fonts.bodyLarge.fontStyle
|
||||
},
|
||||
manufacturerText:{
|
||||
color: theme.colors.onSurfaceVariant,
|
||||
fontFamily: theme.fonts.bodyMedium.fontFamily,
|
||||
fontSize: theme.fonts.bodyMedium.fontSize,
|
||||
lineHeight: theme.fonts.bodyMedium.lineHeight,
|
||||
letterSpacing: theme.fonts.bodyMedium.letterSpacing,
|
||||
fontWeight: theme.fonts.bodyMedium.fontWeight,
|
||||
fontStyle: theme.fonts.bodyMedium.fontStyle
|
||||
},
|
||||
editButton:{
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.outline,
|
||||
},
|
||||
horizontalDivider:{
|
||||
marginRight: 16,
|
||||
marginBottom: 16,
|
||||
marginLeft: 7,
|
||||
},
|
||||
|
||||
chipMargin:{
|
||||
marginLeft: 16,
|
||||
marginRight: 16,
|
||||
marginBottom: 7,
|
||||
},
|
||||
|
||||
chip: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
shadowRadius: 12, //TODO: shadow wack
|
||||
shadowOffset: { width: 2, height: 2 },
|
||||
width: "100%" ,
|
||||
height: 44,
|
||||
backgroundColor: theme.colors.surfaceDisabled,
|
||||
borderRadius: 12
|
||||
},
|
||||
lastChipMargin: {
|
||||
marginBottom: 40
|
||||
},
|
||||
labelChip: {
|
||||
height: "100%",
|
||||
width: "38%",
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderTopLeftRadius: 12,
|
||||
borderBottomLeftRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
},
|
||||
|
||||
label: {
|
||||
color: theme.colors.onPrimary,
|
||||
textAlign: "center",
|
||||
fontFamily: theme.fonts.titleSmall.fontFamily,
|
||||
fontSize: theme.fonts.titleSmall.fontSize,
|
||||
lineHeight: theme.fonts.titleSmall.lineHeight,
|
||||
letterSpacing: theme.fonts.titleSmall.letterSpacing,
|
||||
fontWeight: theme.fonts.titleSmall.fontWeight,
|
||||
fontStyle: theme.fonts.titleSmall.fontStyle
|
||||
},
|
||||
valueChip: {
|
||||
height: "100%",
|
||||
width: "62%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
},
|
||||
|
||||
value: {
|
||||
color: theme.colors.onSurfaceVariant,
|
||||
textAlign: "center",
|
||||
fontFamily: theme.fonts.bodyMedium.fontFamily,
|
||||
fontSize: theme.fonts.bodyMedium.fontSize,
|
||||
lineHeight: theme.fonts.bodyMedium.lineHeight,
|
||||
letterSpacing: theme.fonts.bodyMedium.letterSpacing,
|
||||
fontWeight: theme.fonts.bodyMedium.fontWeight,
|
||||
fontStyle: theme.fonts.bodyMedium.fontStyle
|
||||
},
|
||||
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Drug } from "@/redux/features/drug/drug";
|
||||
import { View, StyleSheet, FlatList, Dimensions } from "react-native";
|
||||
import { RootState, AppDispatch } from "@/redux/store";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import React, { useState } from "react";
|
||||
import DrugDisplay from "./DrugDisplay";
|
||||
import { FAB, MD3Theme, useTheme } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import DeletePopUp from "./DeletePopUp";
|
||||
import { drugSlice, removeDrug } from "@/redux/features/drug/drugSlice";
|
||||
import { ConnectionNotification } from "./ConnectionNotification";
|
||||
import { ErrorNotification } from "./ErrorNotification";
|
||||
|
||||
export default function DrugList() {
|
||||
const drugs: Drug[] = useSelector((state: RootState) => state.drug.drugList);
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const style = styles(theme);
|
||||
const navigation = useNavigation<any>(); //TODO: types...
|
||||
|
||||
const [deleteIndex, useDeleteIndex] = useState(-1)
|
||||
|
||||
return (
|
||||
<View style={style.listMargin}>
|
||||
<ConnectionNotification />
|
||||
{/* <ErrorNotification /> */}
|
||||
<FAB
|
||||
color={theme.colors.onPrimary}
|
||||
style={style.fab}
|
||||
icon="pencil-outline"
|
||||
mode="elevated"
|
||||
onPress={() => {
|
||||
navigation.navigate("add");
|
||||
}}
|
||||
/>
|
||||
{deleteIndex != -1 ?
|
||||
<DeletePopUp
|
||||
drugId={deleteIndex}
|
||||
onCancel = {() => {
|
||||
useDeleteIndex(-1)
|
||||
}}
|
||||
onDelete = {() => {
|
||||
dispatch({type: "drug/remove", payload: deleteIndex})
|
||||
useDeleteIndex(-1)
|
||||
}}
|
||||
/> : null}
|
||||
<FlatList style={style.list}
|
||||
data={drugs}
|
||||
renderItem={({item})=><DrugDisplay
|
||||
key={item.id}
|
||||
drug={item}
|
||||
onUpdate={(drugId: number) => {
|
||||
navigation.navigate("update/[id]", {id: item.id});
|
||||
}}
|
||||
onDelete={(drugId: number) => {
|
||||
useDeleteIndex(drugId)
|
||||
}} />}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = (theme: MD3Theme) => StyleSheet.create({
|
||||
listMargin:{
|
||||
paddingTop: 75.5,
|
||||
paddingBottom: 23.5,
|
||||
paddingLeft: 26,
|
||||
paddingRight: 26,
|
||||
height: Dimensions.get('window').height,
|
||||
flex: 1
|
||||
},
|
||||
list: {
|
||||
backgroundColor: theme.colors.surface,
|
||||
height: "100%"
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
margin: 48,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 30,
|
||||
borderRadius: 30,
|
||||
backgroundColor: theme.colors.primary
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { RootState } from "@/redux/store";
|
||||
import { useState, useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Snackbar } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
export const ErrorNotification = () => {
|
||||
const dispatch = useDispatch();
|
||||
const status = useSelector((state: RootState) => state.errorStatus);
|
||||
const [visible, setVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
if (status.status) {
|
||||
setVisible(true);
|
||||
}
|
||||
else{
|
||||
setVisible(false);
|
||||
}
|
||||
}, [status.message]);
|
||||
const onDismiss = () => {
|
||||
dispatch({type: "errorStatus/setErrorStatus", payload: {status: false, message: ""}})
|
||||
};
|
||||
return (
|
||||
<View style={{ zIndex: 9999, flex: 1, justifyContent: 'center', position: 'absolute', width: '100%', bottom: 0, left: "50%", transform: [{ translateX: "-42%" }]}}>
|
||||
<Snackbar
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={{ zIndex: 9999}}
|
||||
action={{
|
||||
label: 'Dismiss',
|
||||
onPress: () => {
|
||||
setVisible(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{status.message}
|
||||
</Snackbar>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { Drug } from "@/redux/features/drug/drug";
|
||||
import { AppDispatch, RootState } from "@/redux/store";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dimensions, Text, View, StyleSheet } from "react-native";
|
||||
import { Button, HelperText, MD3Theme, TextInput, useTheme } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
export default function Form(props: {drugId?: number}) {
|
||||
const [drug, setDrug] = useState<Drug>({
|
||||
id: -1,
|
||||
name: "",
|
||||
manufacturer: "",
|
||||
category: "",
|
||||
price: 0,
|
||||
numberOfUnits: 0
|
||||
});
|
||||
const [price , setPrice] = useState("0");
|
||||
const updateDrug = useSelector((state: RootState) => state.drug.drugList.find(drug => drug.id === props.drugId));
|
||||
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const style = styles(theme);
|
||||
const navigation = useNavigation<any>(); //TODO: types...
|
||||
useEffect(() => {
|
||||
if(props.drugId){
|
||||
if(updateDrug){
|
||||
setDrug(updateDrug);
|
||||
setPrice(updateDrug.price.toFixed(2));
|
||||
}
|
||||
}
|
||||
}, [updateDrug]);
|
||||
|
||||
const onSubmit = () => {
|
||||
if(props.drugId){
|
||||
dispatch({type: "drug/update", payload: drug});
|
||||
}
|
||||
else{
|
||||
dispatch({type: "drug/add", payload: drug});
|
||||
}
|
||||
}
|
||||
|
||||
const isDisabled = () => {
|
||||
return drug.name === "" || drug.manufacturer === "" || drug.category === "" || drug.price === 0 || drug.numberOfUnits === 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.mainMargin}>
|
||||
<View style={style.column}>
|
||||
<View style={style.chipMargin}>
|
||||
<View style={style.chip}>
|
||||
<Text style={style.chipLabel}>{props.drugId ? "Update Drug" : "Add Drug"}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.fieldList}>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Name"
|
||||
value={drug?.name}
|
||||
onChangeText={(text) => {
|
||||
setDrug({...drug, name: text} as Drug)
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => setDrug({...drug, name: ""} as Drug)}/>}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Manufacturer"
|
||||
value={drug?.manufacturer}
|
||||
onChangeText={(text) => {
|
||||
setDrug({...drug, manufacturer: text} as Drug)
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => setDrug({...drug, manufacturer: ""} as Drug)}/>}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Category"
|
||||
value={drug?.category}
|
||||
onChangeText={(text) => {
|
||||
setDrug({...drug, category: text} as Drug)
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => setDrug({...drug, category: ""} as Drug)}/>}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Number of Units"
|
||||
value={drug?.numberOfUnits.toString()}
|
||||
keyboardType="numeric"
|
||||
onChangeText={(text) => {
|
||||
if(text === ""){
|
||||
setDrug({...drug, numberOfUnits: 0} as Drug)
|
||||
}
|
||||
else{
|
||||
setDrug({...drug, numberOfUnits: parseInt(text)} as Drug)
|
||||
}
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => setDrug({...drug, numberOfUnits: 0} as Drug)}/>}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Price"
|
||||
value={price}
|
||||
keyboardType="numeric"
|
||||
onChangeText={(text) => {
|
||||
setPrice(text);
|
||||
if(text === "" || isNaN(Number(text)) || text[text.length - 1] === '.'){
|
||||
setDrug({...drug, price: 0} as Drug)
|
||||
}
|
||||
else{
|
||||
setPrice(parseFloat(text).toString());
|
||||
setDrug({...drug, price: parseFloat(text)} as Drug)
|
||||
}
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => {setDrug({...drug, price: 0} as Drug); setPrice("")}}/>}
|
||||
left={<TextInput.Affix text="$ " />}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.buttonRow}>
|
||||
<Button mode="outlined" onPress={() => navigation.navigate("index")}>Cancel</Button>
|
||||
<Button mode="contained"
|
||||
textColor={theme.colors.onPrimary}
|
||||
buttonColor={theme.colors.primary}
|
||||
onPress={() => {
|
||||
if(!isDisabled())
|
||||
{
|
||||
onSubmit();
|
||||
navigation.navigate("index")
|
||||
}
|
||||
}}>{props.drugId ? "Update" : "Add"}</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = (theme: MD3Theme) => StyleSheet.create({
|
||||
mainMargin:{
|
||||
paddingTop: 63,
|
||||
paddingBottom: 62,
|
||||
paddingLeft: 62,
|
||||
paddingRight: 73,
|
||||
height: Dimensions.get('window').height,
|
||||
flex: 1
|
||||
},
|
||||
column: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.colors.surface,
|
||||
},
|
||||
chipMargin: {
|
||||
paddingBottom: 25,
|
||||
paddingLeft: 61,
|
||||
paddingRight: 61,
|
||||
},
|
||||
chip: {
|
||||
height: 49,
|
||||
width: "100%",
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.outlineVariant,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
chipLabel: {
|
||||
color: theme.colors.onSurface,
|
||||
textAlign: "center",
|
||||
fontFamily: theme.fonts.labelLarge.fontFamily,
|
||||
fontSize: theme.fonts.labelLarge.fontSize,
|
||||
lineHeight: theme.fonts.labelLarge.lineHeight,
|
||||
letterSpacing: theme.fonts.labelLarge.letterSpacing,
|
||||
fontWeight: theme.fonts.labelLarge.fontWeight,
|
||||
fontStyle: theme.fonts.labelLarge.fontStyle
|
||||
|
||||
},
|
||||
fieldList: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginBottom: 30,
|
||||
width: "100%"
|
||||
},
|
||||
buttonRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 8
|
||||
},
|
||||
inputWrapper: {
|
||||
marginBottom: 16,
|
||||
width: "100%"
|
||||
}
|
||||
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export const SERVER_IP = "192.168.0.45" as const;
|
||||
export const SERVER_PORT = 3000 as const;
|
||||
export const WS_PORT = 8080 as const;
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Drug } from '@/redux/features/drug/drug';
|
||||
import { openDatabaseAsync, SQLiteDatabase } from 'expo-sqlite'
|
||||
export const getDBConnection = async () => {
|
||||
return await openDatabaseAsync("drug.db");
|
||||
};
|
||||
|
||||
export type TempDrug = {
|
||||
id: number,
|
||||
type: number,
|
||||
drug?: Drug
|
||||
}
|
||||
|
||||
export const createTable = async (db: SQLiteDatabase) => {
|
||||
const exists = await db.getAllAsync(`SELECT name FROM sqlite_master WHERE type='table' AND name='Drug';`);
|
||||
if (exists.length > 0) {
|
||||
return;
|
||||
}
|
||||
const query = `CREATE TABLE IF NOT EXISTS Drug (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT,
|
||||
category TEXT,
|
||||
price REAL,
|
||||
numberOfUnits INTEGER,
|
||||
manufacturer TEXT
|
||||
);`;
|
||||
await db.runAsync(query);
|
||||
const tempTable = `CREATE TABLE IF NOT EXISTS TempDrug (
|
||||
id INTEGER,
|
||||
type INTEGER);`;
|
||||
await db.runAsync(tempTable);
|
||||
};
|
||||
|
||||
export const insertDrug = async (db: SQLiteDatabase, drug: Drug) => {
|
||||
const query = `INSERT INTO Drug (${drug.id !== -1 ? "id," : ""} name, category, price, numberOfUnits, manufacturer) VALUES (${drug.id !== -1 ? "?," : ""} ?, ?, ?, ?, ?);`;
|
||||
const values = [drug.name, drug.category, drug.price, drug.numberOfUnits, drug.manufacturer];
|
||||
if (drug.id !== -1) {
|
||||
values.unshift(drug.id);
|
||||
}
|
||||
const response = await db.runAsync(query, values);
|
||||
console.log("Inserted into local db");
|
||||
return response.lastInsertRowId;
|
||||
}
|
||||
|
||||
export const getDrugs = async (db: SQLiteDatabase) => {
|
||||
const query = `SELECT * FROM Drug;`;
|
||||
const response = await db.getAllAsync(query);
|
||||
const drugs: Drug[] = [];
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
drugs.push(response[i] as Drug);
|
||||
}
|
||||
return drugs;
|
||||
}
|
||||
|
||||
export const getDrug = async (db: SQLiteDatabase, id: number) => {
|
||||
const query = `SELECT * FROM Drug WHERE id = ?;`;
|
||||
const values = [id];
|
||||
const response = await db.getFirstAsync(query, values);
|
||||
return response as Drug;
|
||||
}
|
||||
|
||||
export const editDrug = async (db: SQLiteDatabase, drug: Drug) => {
|
||||
const query = `UPDATE Drug SET name = ?, category = ?, price = ?, numberOfUnits = ?, manufacturer = ? WHERE id = ?;`;
|
||||
const values = [drug.name, drug.category, drug.price, drug.numberOfUnits, drug.manufacturer, drug.id];
|
||||
await db.runAsync(query, values);
|
||||
}
|
||||
|
||||
export const deleteDrug = async (db: SQLiteDatabase, id: number) => {
|
||||
const query = `DELETE FROM Drug WHERE id = ?;`;
|
||||
const values = [id];
|
||||
await db.runAsync(query, values);
|
||||
console.log("Deleted from local db");
|
||||
}
|
||||
|
||||
export const rehydrateLocalDB = async (drugs: Drug[]) => {
|
||||
const db = await getDBConnection();
|
||||
await db.runAsync(`DELETE FROM Drug;`);
|
||||
for (let i = 0; i < drugs.length; i++) {
|
||||
await insertDrug(db, drugs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const initDB = async () => {
|
||||
const db = await getDBConnection();
|
||||
await createTable(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
export const insertTempDrug = async (db: SQLiteDatabase, tempDrug: TempDrug) => {
|
||||
console.log("Inserting temp drug");
|
||||
console.log(tempDrug);
|
||||
const query = `INSERT INTO TempDrug (id, type) VALUES (?, ?);`;
|
||||
const values = [tempDrug.id, tempDrug.type];
|
||||
await db.runAsync(query, values);
|
||||
}
|
||||
|
||||
export const getTempDrugs = async (db: SQLiteDatabase) => {
|
||||
const query = `SELECT Drug.*, TempDrug.id, TempDrug.type FROM TempDrug LEFT JOIN Drug ON TempDrug.id = Drug.id;`;
|
||||
const response = await db.getAllAsync(query);
|
||||
const tempDrugs: TempDrug[] = [];
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
const responseDrug = response[i] as any;
|
||||
const tempDrug = {
|
||||
id: responseDrug.id,
|
||||
type: responseDrug.type,
|
||||
drug: responseDrug.name ? {
|
||||
id: responseDrug.id,
|
||||
name: responseDrug.name,
|
||||
category: responseDrug.category,
|
||||
price: responseDrug.price,
|
||||
numberOfUnits: responseDrug.numberOfUnits,
|
||||
manufacturer: responseDrug.manufacturer
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
tempDrugs.push(tempDrug);
|
||||
}
|
||||
return tempDrugs;
|
||||
}
|
||||
|
||||
export const deleteTempDrug = async (db: SQLiteDatabase) => {
|
||||
const query = `DELETE FROM TempDrug`;
|
||||
await db.runAsync(query);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Drug } from "@/redux/features/drug/drug"
|
||||
import { SERVER_IP, SERVER_PORT } from "@/constants/Constants"
|
||||
import axios, { Axios, AxiosResponse } from "axios"
|
||||
import { TempDrug } from "./localdb";
|
||||
|
||||
export const insertDrugServer = async (drug: Drug) => {
|
||||
console.log(`Inserting drug ${drug}`);
|
||||
const response: AxiosResponse<Drug> = await axios.post(`http://${SERVER_IP}:${SERVER_PORT}/drug`, drug);
|
||||
if (response.status === 201) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error("Failed to insert drug");
|
||||
}
|
||||
|
||||
export const getDrugsServer = async () => {
|
||||
const response: AxiosResponse<Drug[]> = await axios.get(`http://${SERVER_IP}:${SERVER_PORT}/drug`);
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error("Failed to fetch drugs");
|
||||
}
|
||||
|
||||
export const editDrugServer = async (drug: Drug) => {
|
||||
console.log(`Editing drug ${drug}`);
|
||||
const response: AxiosResponse<Drug> = await axios.put(`http://${SERVER_IP}:${SERVER_PORT}/drug?id=${drug.id}`, drug);
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error("Failed to edit drug");
|
||||
}
|
||||
|
||||
export const deleteDrugServer = async (id: number) => {
|
||||
console.log(`Deleting drug with id ${id}`);
|
||||
const response: AxiosResponse = await axios.delete(`http://${SERVER_IP}:${SERVER_PORT}/drug?id=${id}`);
|
||||
if (response.status === 204) {
|
||||
return;
|
||||
}
|
||||
throw new Error("Failed to delete drug");
|
||||
}
|
||||
|
||||
export const syncOfflineData = async (offlineData: TempDrug[]) => {
|
||||
const idCounts: Record<number, number> = offlineData.reduce((acc, item) => {
|
||||
acc[item.id] = (acc[item.id] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<number, number>);
|
||||
|
||||
const filteredArray: TempDrug[] = offlineData.filter(item => idCounts[item.id] !== 2);
|
||||
|
||||
const tasks = [];
|
||||
for (const item of filteredArray) {
|
||||
if (item.type === 1) {
|
||||
tasks.push(insertDrugServer(item.drug!));
|
||||
} else {
|
||||
tasks.push(deleteDrugServer(item.id));
|
||||
}
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SERVER_IP, WS_PORT } from "@/constants/Constants";
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
import WS from 'ws';
|
||||
|
||||
const ws = new ReconnectingWebSocket(`ws://${SERVER_IP}:${WS_PORT}`, [], {
|
||||
WebSocket: WS.WebSocket,
|
||||
connectionTimeout: 1000,
|
||||
startClosed: true
|
||||
});
|
||||
|
||||
const getSocket = () => {
|
||||
return ws;
|
||||
}
|
||||
|
||||
export default getSocket;
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"build": {
|
||||
"preview": {
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
}
|
||||
},
|
||||
"preview2": {
|
||||
"android": {
|
||||
"gradleCommand": ":app:assembleRelease"
|
||||
}
|
||||
},
|
||||
"preview3": {
|
||||
"developmentClient": true
|
||||
},
|
||||
"preview4": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
|
||||
import { Drug } from '@/redux/features/drug/drug';
|
||||
import { deleteDrug, editDrug, getDBConnection, getDrugs, insertDrug, insertTempDrug } from '@/database/localdb';
|
||||
import { setErrorStatus } from '@/redux/features/errorstatus/errorStatusSlice';
|
||||
|
||||
export function* addDrug(action: { type: string, payload: Drug }): Generator<any, void, any> {
|
||||
try {
|
||||
const db = yield call(getDBConnection);
|
||||
const id = yield call(insertDrug, db, action.payload);
|
||||
yield call(insertTempDrug, db, { id: id, type: 1 });
|
||||
const { id: _, ...payloadWithoutId } = action.payload;
|
||||
yield put({ type: "drug/addDrug", payload: { id: id, ...payloadWithoutId } });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Local Database Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* removeDrug(action: { type: string, payload: number }): Generator<any, void, any> {
|
||||
try {
|
||||
const db = yield call(getDBConnection);
|
||||
yield call(deleteDrug, db, action.payload);
|
||||
yield call(insertTempDrug, db, { id: action.payload, type: 2 });
|
||||
yield put({ type: "drug/removeDrug", payload: action.payload });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Local Database Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* updateDrug(action: { type: string, payload: Drug }): Generator<any, void, any> {
|
||||
try {
|
||||
const db = yield call(getDBConnection);
|
||||
yield call(editDrug, db, action.payload);
|
||||
yield put({ type: "drug/updateDrug", payload: action.payload });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Local Database Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function* fetchDrugs(): Generator<any, void, any> {
|
||||
try {
|
||||
const db = yield call(getDBConnection);
|
||||
const drugs = yield call(getDrugs, db);
|
||||
yield put({ type: "drug/fetchDrugs", payload: drugs });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Local Database Error", status: true }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { initDB } from "@/database/localdb";
|
||||
import { call, put, select, takeEvery } from "redux-saga/effects";
|
||||
import { SERVER_IP, SERVER_PORT } from "@/constants/Constants";
|
||||
import axios, { AxiosResponse } from "axios"
|
||||
import { addDrug, fetchDrugs, removeDrug, updateDrug } from "./localdbSaga";
|
||||
import { fetch, NetInfoState } from "@react-native-community/netinfo";
|
||||
import { setConnected, setOnline } from "@/redux/features/status/statusSlice";
|
||||
import { Status } from "@/redux/features/status/status";
|
||||
import { addDrugServer, checkConnection, fetchDrugsServer, removeDrugServer, updateDrugServer, webSocketSaga } from "./serverSaga";
|
||||
|
||||
|
||||
function* initialize() {
|
||||
yield call(initDB);
|
||||
yield call(checkConnection);
|
||||
console.log("Initialized");
|
||||
yield put({ type: 'drug/fetch' });
|
||||
yield webSocketSaga();
|
||||
}
|
||||
|
||||
|
||||
function* fetchOfflineOrOnline() {
|
||||
const status: Status = yield select(state => state.status);
|
||||
if (status.isConnected && status.isOnline) {
|
||||
yield call(fetchDrugsServer);
|
||||
} else {
|
||||
yield call(fetchDrugs);
|
||||
}
|
||||
}
|
||||
|
||||
function* addDrugOfflineOrOnline(action: { type: string, payload: any }) {
|
||||
const status: Status = yield select(state => state.status);
|
||||
if (status.isConnected && status.isOnline) {
|
||||
yield call(addDrugServer, action);
|
||||
} else {
|
||||
yield call(addDrug, action);
|
||||
}
|
||||
}
|
||||
|
||||
function* removeDrugOfflineOrOnline(action: { type: string, payload: number }) {
|
||||
const status: Status = yield select(state => state.status);
|
||||
if (status.isConnected && status.isOnline) {
|
||||
yield call(removeDrugServer, action);
|
||||
} else {
|
||||
yield call(removeDrug, action);
|
||||
}
|
||||
}
|
||||
|
||||
function* updateDrugOfflineOrOnline(action: { type: string, payload: any }) {
|
||||
const status: Status = yield select(state => state.status);
|
||||
if (status.isConnected && status.isOnline) {
|
||||
yield call(updateDrugServer, action);
|
||||
} else {
|
||||
yield call(updateDrug, action);
|
||||
}
|
||||
}
|
||||
|
||||
export function* rootSaga() {
|
||||
yield takeEvery('drug/initialize', initialize);
|
||||
yield takeEvery('drug/fetch', fetchOfflineOrOnline);
|
||||
yield takeEvery('drug/add', addDrugOfflineOrOnline);
|
||||
yield takeEvery('drug/remove', removeDrugOfflineOrOnline);
|
||||
yield takeEvery('drug/update', updateDrugOfflineOrOnline);
|
||||
yield takeEvery('status/checkConnection', checkConnection);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { deleteDrugServer, editDrugServer, getDrugsServer, insertDrugServer, syncOfflineData } from "@/database/serverdb";
|
||||
import { Drug } from "@/redux/features/drug/drug";
|
||||
import { all, call, put, retry, select, take } from "redux-saga/effects";
|
||||
import { deleteDrug, deleteTempDrug, editDrug, getDBConnection, getTempDrugs, insertDrug, rehydrateLocalDB, TempDrug } from "@/database/localdb";
|
||||
import { SQLiteDatabase } from "expo-sqlite";
|
||||
import { Status } from "@/redux/features/status/status";
|
||||
import { SERVER_IP, SERVER_PORT } from "@/constants/Constants";
|
||||
import { setOnline, setConnected } from "@/redux/features/status/statusSlice";
|
||||
import { NetInfoState, fetch } from "@react-native-community/netinfo";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { store } from "@/redux/store";
|
||||
import { EventChannel, eventChannel } from "redux-saga";
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
import getSocket from "@/database/socket";
|
||||
import { setErrorStatus } from "@/redux/features/errorstatus/errorStatusSlice";
|
||||
import ReconnectingWebSocket from "reconnecting-websocket";
|
||||
|
||||
|
||||
export function* fetchDrugsServer() {
|
||||
try {
|
||||
const drugs: Drug[] = yield call(() => getDrugsServer());
|
||||
console.log(drugs);
|
||||
yield call(rehydrateLocalDB, drugs);
|
||||
yield put({ type: "drug/fetchDrugs", payload: drugs });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* addDrugServer(action: { type: string, payload: Drug }) {
|
||||
try {
|
||||
const drug: Drug = yield call(() => insertDrugServer(action.payload));
|
||||
const db: SQLiteDatabase = yield call(getDBConnection);
|
||||
console.log(drug);
|
||||
yield call(() => insertDrug(db, drug));
|
||||
yield put({ type: "drug/addDrug", payload: drug });
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* removeDrugServer(action: { type: string, payload: number }) {
|
||||
try {
|
||||
yield call(() => deleteDrugServer(action.payload));
|
||||
const db: SQLiteDatabase = yield call(getDBConnection);
|
||||
yield call(() => deleteDrug(db, action.payload));
|
||||
yield put({ type: "drug/removeDrug", payload: action.payload });
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* updateDrugServer(action: { type: string, payload: Drug }) {
|
||||
try {
|
||||
yield call(() => editDrugServer(action.payload));
|
||||
const db: SQLiteDatabase = yield call(getDBConnection);
|
||||
yield call(() => editDrug(db, action.payload));
|
||||
yield put({ type: "drug/updateDrug", payload: action.payload });
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function* checkConnection() {
|
||||
try {
|
||||
yield put(setOnline(false));
|
||||
yield put(setConnected(false));
|
||||
const internetConnection: NetInfoState = yield call(fetch);
|
||||
if (!internetConnection.isConnected) {
|
||||
console.error("No internet connection");
|
||||
return;
|
||||
}
|
||||
yield put(setOnline(true));
|
||||
console.log("Internet connection available");
|
||||
const response: AxiosResponse = yield call(axios.get, `http://${SERVER_IP}:${SERVER_PORT}/`, { timeout: 5000, validateStatus: () => true });
|
||||
if (!(response.status === 200)) {
|
||||
console.error("Connection to server failed");
|
||||
return;
|
||||
}
|
||||
console.log("Connection to server successful");
|
||||
yield put(setConnected(true));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function webSocketChannel() {
|
||||
const socket = getSocket();
|
||||
return eventChannel((emit) => {
|
||||
console.log("Websocket saga started");
|
||||
socket.onopen = () => {
|
||||
console.log("Connected to WebSocket server!");
|
||||
emit({ type: "connect" });
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("Received message:", data);
|
||||
emit({ type: data.event, payload: data.data });
|
||||
}
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log("Disconnected from server.");
|
||||
emit({ type: "disconnect" });
|
||||
};
|
||||
|
||||
return () => {
|
||||
socket.close();
|
||||
};
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function* webSocketSaga() {
|
||||
const channel: EventChannel<PayloadAction> = yield call(webSocketChannel);
|
||||
const socket: ReconnectingWebSocket = yield call(getSocket);
|
||||
socket.reconnect();
|
||||
while (true) {
|
||||
const action: PayloadAction<Drug | { id: number } | void> = yield take(channel);
|
||||
console.log(action);
|
||||
const db: SQLiteDatabase = yield call(getDBConnection);
|
||||
switch (action.type) {
|
||||
|
||||
case "connect":
|
||||
console.log("Connected to server via WebSocket");
|
||||
yield put({ type: "status/checkConnection" });
|
||||
const tempData: TempDrug[] = yield call(getTempDrugs, db);
|
||||
yield call(() => syncOfflineData(tempData));
|
||||
yield call(() => fetchDrugsServer());
|
||||
yield call(deleteTempDrug, db);
|
||||
break;
|
||||
case "itemAdded":
|
||||
yield call(() => insertDrug(db, action.payload as Drug));
|
||||
yield put({ type: "drug/addDrug", payload: action.payload as Drug });
|
||||
break;
|
||||
case "itemUpdated":
|
||||
yield call(() => editDrug(db, action.payload as Drug));
|
||||
yield put({ type: "drug/updateDrug", payload: action.payload as Drug });
|
||||
break;
|
||||
case "itemDeleted":
|
||||
yield call(() => deleteDrug(db, (action.payload as { id: number }).id));
|
||||
yield put({ type: "drug/removeDrug", payload: (action.payload as { id: number }).id });
|
||||
break;
|
||||
case "disconnect":
|
||||
console.log("Disconnected from server. Reconnecting...");
|
||||
yield put({ type: "status/checkConnection" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "ma-nonnative",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"test": "jest --watchAll",
|
||||
"lint": "expo lint"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@pchmn/expo-material3-theme": "^1.3.2",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-navigation/bottom-tabs": "^7.0.0",
|
||||
"@react-navigation/native": "^7.0.0",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"expo": "~52.0.11",
|
||||
"expo-blur": "~14.0.1",
|
||||
"expo-build-properties": "^0.13.2",
|
||||
"expo-constants": "~17.0.3",
|
||||
"expo-dev-client": "~5.0.6",
|
||||
"expo-font": "~13.0.1",
|
||||
"expo-haptics": "~14.0.0",
|
||||
"expo-linking": "~7.0.3",
|
||||
"expo-router": "~4.0.9",
|
||||
"expo-splash-screen": "~0.29.13",
|
||||
"expo-sqlite": "~15.0.3",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
"expo-symbols": "~0.2.0",
|
||||
"expo-system-ui": "~4.0.4",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.3",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-paper": "^5.12.5",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "^4.0.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-webview": "13.12.2",
|
||||
"react-redux": "^9.1.2",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-native-sqlite-storage": "^6.0.5",
|
||||
"@types/react-test-renderer": "^18.3.0",
|
||||
"@types/ws": "^8.5.14",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~52.0.2",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"private": true,
|
||||
"expo": {
|
||||
"doctor": {
|
||||
"reactNativeDirectoryCheck": {
|
||||
"exclude": [
|
||||
"@pchmn/expo-material3-theme",
|
||||
"@reduxjs/toolkit",
|
||||
"react-redux"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export type Drug = {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string;
|
||||
price: number;
|
||||
numberOfUnits: number;
|
||||
manufacturer: string;
|
||||
}
|
||||
|
||||
export type DrugState = {
|
||||
drugList: Drug[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Drug, DrugState } from "./drug";
|
||||
import { get } from "react-native/Libraries/TurboModule/TurboModuleRegistry";
|
||||
import { initDB, insertDrug } from "@/database/localdb";
|
||||
|
||||
|
||||
const initialState: DrugState = {
|
||||
drugList: [],
|
||||
};
|
||||
|
||||
|
||||
export const drugSlice = createSlice({
|
||||
name: "drug",
|
||||
initialState,
|
||||
reducers: {
|
||||
addDrug: (state, action: PayloadAction<Drug>) => {
|
||||
state.drugList.push(action.payload);
|
||||
},
|
||||
removeDrug: (state, action: PayloadAction<number>) => {
|
||||
console.log("Removing drug with id: ", action.payload);
|
||||
state.drugList = state.drugList.filter(drug => drug.id !== action.payload);
|
||||
},
|
||||
updateDrug: (state, action: PayloadAction<Drug>) => {
|
||||
console.log("Updating drug with id: ", action.payload.id);
|
||||
console.log(action.payload);
|
||||
const index = state.drugList.findIndex(drug => drug.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.drugList[index] = action.payload;
|
||||
}
|
||||
console.log(state.drugList);
|
||||
},
|
||||
fetchDrugs: (state, action: PayloadAction<Drug[]>) => {
|
||||
state.drugList = action.payload;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const { addDrug, removeDrug, updateDrug, fetchDrugs } = drugSlice.actions
|
||||
export default drugSlice.reducer
|
||||
@@ -0,0 +1,4 @@
|
||||
export type ErrorStatus = {
|
||||
status: boolean,
|
||||
message: string
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { ErrorStatus } from "./errorStatus";
|
||||
|
||||
|
||||
export const errorStatusSlice = createSlice({
|
||||
name: "status",
|
||||
initialState: { status: false, message: "" } as ErrorStatus,
|
||||
reducers: {
|
||||
setErrorStatus: (state, action: PayloadAction<ErrorStatus>) => {
|
||||
state.status = action.payload.status;
|
||||
state.message = action.payload.message;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const { setErrorStatus } = errorStatusSlice.actions
|
||||
export default errorStatusSlice.reducer
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Status = {
|
||||
isOnline: boolean;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Status } from "./status";
|
||||
|
||||
|
||||
export const statusSlice = createSlice({
|
||||
name: "status",
|
||||
initialState: { isOnline: false, isConnected: false } as Status,
|
||||
reducers: {
|
||||
setOnline: (state, action: PayloadAction<boolean>) => {
|
||||
state.isOnline = action.payload;
|
||||
},
|
||||
setConnected: (state, action: PayloadAction<boolean>) => {
|
||||
state.isConnected = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { setOnline, setConnected } = statusSlice.actions
|
||||
export default statusSlice.reducer
|
||||
@@ -0,0 +1,23 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { drugSlice } from "./features/drug/drugSlice";
|
||||
import createSagaMiddleware from "redux-saga";
|
||||
import { rootSaga } from "@/middleware/rootSaga";
|
||||
import { statusSlice } from "./features/status/statusSlice";
|
||||
import { errorStatusSlice } from "./features/errorstatus/errorStatusSlice";
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: { drug: drugSlice.reducer, status: statusSlice.reducer, errorStatus: errorStatusSlice.reducer },
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat([sagaMiddleware])
|
||||
});
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type AppStore = typeof store;
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
]
|
||||
}
|
||||