Anul 3 Semestrul 1

This commit is contained in:
2025-02-06 20:33:26 +02:00
parent 0b130ee18c
commit 184f3bd92e
313 changed files with 348499 additions and 0 deletions
@@ -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>
);
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

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;
}
}
}
File diff suppressed because it is too large Load Diff
@@ -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"
]
}