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,89 @@
# Gradle files
.gradle/
build/
!gradle/wrapper/gradle-wrapper.jar
.gradle/
**/build/
**/out/
**/buildSrc/build/
**/buildSrc/.gradle/
**/local.properties
# Android Studio files
.idea/
*.iml
*.hprof
*.orig
*.log
*.lock
*.pyc
*.swp
*.swo
*.class
*.jar
*.war
*.ear
*.db
*.sqlite
*.apk
*.ap_
*.dex
*.class
*.so
*.dll
*.pdb
*.o
*.a
*.lib
*.nib
*.plist
*.hmap
*.ipa
*.xcuserstate
*.xcuserdatad
*.xcworkspace
*.xcuserdata
*.xcodeproj
*.xcodeproj/project.xcworkspace
*.xcodeproj/xcuserdata
*.xcodeproj/xcuserdata/*.xcuserdatad
*.xcodeproj/xcuserdata/*.xcuserdatad/xcschemes
# built application files
*.apk
*.ap_
# Mac files
.DS_Store
# files for the dex VM
*.dex
# Java class files
*.class
# generated files
bin/
gen/
# Ignore gradle files
.gradle/
build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
proguard-project.txt
# Eclipse files
.project
.classpath
.settings/
# Android Studio/IDEA
*.iml
.idea
.kotlin/
node_modules/
@@ -0,0 +1,15 @@
*.iml
.gradle
/local.properties
/.idea/caches
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
/.idea/navEditor.xml
/.idea/assetWizardSettings.xml
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
@@ -0,0 +1 @@
/build
@@ -0,0 +1,59 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.example.ma_ui_native"
compileSdk = 34
defaultConfig {
applicationId = "com.example.ma_ui_native"
minSdk = 34
targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.example.ma_ui_native
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.example.ma_ui_native", appContext.packageName)
}
}
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.MAUINative"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.MAUINative">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".SecondActivity"
android:exported="true"
android:theme="@style/Theme.MAUINative">
</activity>
</application>
</manifest>
@@ -0,0 +1,15 @@
package com.example.ma_ui_native
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
data class Drug(
var Id: Int = -1,
var Name: String = "",
var Category: String = "",
var NumberOfUnits: Int = 0,
var RetailPrice: Double = 0.0,
var Manufacturer: String = ""
){
}
@@ -0,0 +1,5 @@
package com.example.ma_ui_native;
public class DrugViewModel {
}
@@ -0,0 +1,300 @@
package com.example.ma_ui_native
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Edit
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.ma_ui_native.ui.theme.MAUINativeTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MAUINativeTheme {
val openDialog = remember { mutableStateOf(false) }
val dialogId = remember { mutableStateOf(-1) }
if(openDialog.value){
DeleteDialog(
dialogId.value,
onConfirm = {
Service.DeleteDrug(dialogId.value)
openDialog.value = false
},
onDismiss = {
openDialog.value = false
}
)
}
DrugList({
openDialog.value = true
dialogId.value = it
})
Column(
modifier = Modifier
.padding(48.dp)
.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.End,
) {
val context = LocalContext.current
FAB(onClick = {
context.startActivity(Intent(context, SecondActivity::class.java))
})
}
}
}
}
}
@Composable
fun FAB(onClick: () -> Unit) {
FloatingActionButton(
onClick = { onClick() },
shape = CircleShape,
contentColor = MaterialTheme.colorScheme.onPrimary,
containerColor = MaterialTheme.colorScheme.primary,
) {
Icon(Icons.Outlined.Edit, "Floating action button.")
}
}
@Composable
fun DrugList(onDelete: (Int) -> Unit){
LazyColumn(
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(top = 75.5.dp, bottom = 23.5.dp, start = 26.dp, end = 26.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy((10.dp))
){
items(
Service.GetDrugs(),
key = { it.value.Id},
){
DrugDisplay(it.value, onDelete)
}
}
}
@Composable
fun DrugDisplay(drug: Drug, onDelete: (Int) -> Unit){
Box {
Box(
modifier = Modifier
.matchParentSize()
.background(MaterialTheme.colorScheme.surface, RoundedCornerShape((12.dp)))
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(12.dp))
)
Column(
modifier = Modifier
.fillMaxSize()
) {
Row(
modifier = Modifier
.padding(top = 16.dp, start = 16.dp, end = 16.dp, bottom = 7.dp)
.fillMaxWidth()
.height(44.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(drug.Name, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.bodyLarge)
Text(drug.Manufacturer, color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodyMedium)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
val context = LocalContext.current
Button(
onClick = { GoToUpdate(drug.Id, context) },
colors = ButtonColors(
MaterialTheme.colorScheme.surface,
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.surface,
MaterialTheme.colorScheme.primary
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
) {
Text("Edit")
}
Button(
onClick = { onDelete(drug.Id) },
colors = ButtonColors(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.onPrimary,
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.onPrimary
),
) {
Text("Delete")
}
}
}
HorizontalDivider(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 7.dp)
)
Row(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 7.dp)
.shadow(2.dp, shape = RoundedCornerShape(12.dp))
.fillMaxWidth()
.height(44.dp)
.background(MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(12.dp))
) {
Box(
modifier = Modifier
.fillMaxHeight()
.background(MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp))
.fillMaxWidth(0.38f),
) {
Text("Category:", color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.titleSmall)
}
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(0.62f),
) {
Text(drug.Category, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.bodyMedium)
}
}
Row(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 7.dp)
.shadow(2.dp, shape = RoundedCornerShape(12.dp))
.fillMaxWidth()
.height(44.dp)
.background(MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(12.dp))
) {
Box(
modifier = Modifier
.fillMaxHeight()
.background(MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp))
.fillMaxWidth(0.38f),
) {
Text("# of Units:", color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.titleSmall)
}
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(0.62f),
) {
Text(drug.NumberOfUnits.toString(), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.bodyMedium)
}
}
Row(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 40.dp)
.shadow(2.dp, shape = RoundedCornerShape(12.dp))
.fillMaxWidth()
.height(44.dp)
.background(MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(12.dp))
) {
Box(
modifier = Modifier
.fillMaxHeight()
.background(MaterialTheme.colorScheme.primary, shape = RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp))
.fillMaxWidth(0.38f),
) {
Text("Price:", color = MaterialTheme.colorScheme.onPrimary, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.titleSmall)
}
Box(
modifier = Modifier
.fillMaxHeight()
.fillMaxWidth(0.62f),
) {
Text("$%.2f".format(drug.RetailPrice), color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.align(Alignment.Center), style = MaterialTheme.typography.bodyMedium)
}
}
}
}
}
fun GoToUpdate(id: Int, context: Context){
Log.d("Update", "Update for %d was clicked".format(id))
val bundle = Bundle()
bundle.putInt("id", id)
val intend = Intent(context, SecondActivity::class.java)
intend.putExtras(bundle)
context.startActivity(intend)
}
@Composable
fun DeleteDialog(id: Int, onConfirm: () -> Unit, onDismiss: () -> Unit) {
AlertDialog(
onDismissRequest = {
onDismiss()
},
title = {
Text("Delete Drug")
},
text = {
Text("Are you sure you want to delete this drug? This action can not be reversed")
},
confirmButton = {
Button(
onClick = {
onConfirm()
}
) {
Text("Delete")
}
},
dismissButton = {
Button(
onClick = {
onDismiss()
}
) {
Text("Cancel")
}
}
)
}
@@ -0,0 +1,273 @@
package com.example.ma_ui_native
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.unit.dp
import com.example.ma_ui_native.ui.theme.MAUINativeTheme
class SecondActivity : ComponentActivity() {
private var drug = Drug()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if(intent.hasExtra("id")) {
val id = intent.getIntExtra("id", -1)
if(id != -1) {
drug = Service.GetDrug(id)
}
}
enableEdgeToEdge()
setContent {
MAUINativeTheme {
Column (
modifier = Modifier
.background(MaterialTheme.colorScheme.surface)
.padding(top = 63.dp, start = 62.dp, end = 62.dp, bottom = 73.dp)
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
)
{
Row(
modifier = Modifier
.padding(bottom = 25.dp, start = 61.dp, end = 61.dp)
.fillMaxWidth()
.height(49.dp)
.border(
1.dp,
color = MaterialTheme.colorScheme.outlineVariant,
shape = RoundedCornerShape(8.dp)
),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(if(drug.Id == -1) "Add Drug" else "Update Drug", color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.labelLarge)
}
Column(
modifier = Modifier
.padding(bottom = 30.dp)
.fillMaxWidth()
) {
val drugName = remember { mutableStateOf(drug.Name) }
TextField(
value = drugName.value,
onValueChange = {
drugName.value = it
drug.Name = it
},
label = {Text("Name")},
singleLine = true,
trailingIcon = {
IconButton(
onClick = {
drugName.value = ""
drug.Name = ""
}
)
{
Icon(
Icons.Filled.Clear,
contentDescription = "Clear",
)
} },
supportingText = { Text(text = "*required", color = MaterialTheme.colorScheme.error) },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
val drugManufacturer = remember { mutableStateOf(drug.Manufacturer) }
TextField(
value = drugManufacturer.value,
onValueChange = {
drugManufacturer.value = it
drug.Manufacturer = it
},
label = {Text("Manufacturer")},
singleLine = true,
trailingIcon = {
IconButton(
onClick = {
drugManufacturer.value = ""
drug.Manufacturer = ""
}
)
{
Icon(
Icons.Filled.Clear,
contentDescription = "Clear",
)
} },
supportingText = { Text(text = "*required", color = MaterialTheme.colorScheme.error) },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
val drugCategory = remember { mutableStateOf(drug.Category) }
TextField(
value = drugCategory.value,
onValueChange = {
drugCategory.value = it
drug.Category = it
},
label = {Text("Category")},
singleLine = true,
trailingIcon = {
IconButton(
onClick = {
drugCategory.value = ""
drug.Category = ""
}
)
{
Icon(
Icons.Filled.Clear,
contentDescription = "Clear",
)
} },
supportingText = { Text(text = "*required", color = MaterialTheme.colorScheme.error) },
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
val drugNumberOfUnits = remember { mutableStateOf(drug.NumberOfUnits.toString()) }
if (drugNumberOfUnits.value == "0") {
drugNumberOfUnits.value = ""
}
TextField(
value = drugNumberOfUnits.value,
onValueChange = {
drugNumberOfUnits.value = it
drug.NumberOfUnits = it.toIntOrNull() ?: 0
},
label = {Text("Number of Units")},
singleLine = true,
trailingIcon = {
IconButton(
onClick = {
drugNumberOfUnits.value = ""
drug.NumberOfUnits = 0
}
)
{
Icon(
Icons.Filled.Clear,
contentDescription = "Clear",
)
} },
supportingText = { Text(text = "*required", color = MaterialTheme.colorScheme.error) },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
val drugRetailPrice = remember { mutableStateOf(drug.RetailPrice.toString()) }
if (drugRetailPrice.value == "0.0") {
drugRetailPrice.value = ""
}
TextField(
value = drugRetailPrice.value,
onValueChange = {
drugRetailPrice.value = it
drug.RetailPrice = (it.toDoubleOrNull() ?: 0.0)
},
label = {Text("Retail Price")},
singleLine = true,
trailingIcon = {
IconButton(
onClick = {
drugRetailPrice.value = ""
drug.RetailPrice = 0.0
}
)
{
Icon(
Icons.Filled.Clear,
contentDescription = "Clear",
)
} },
supportingText = { Text(text = "*required", color = MaterialTheme.colorScheme.error) },
prefix = { Text("$") },
keyboardOptions = KeyboardOptions.Default.copy(keyboardType = KeyboardType.Number),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
onClick = { finish() },
colors = ButtonColors(
MaterialTheme.colorScheme.surface,
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.surface,
MaterialTheme.colorScheme.primary
),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
) {
Text("Cancel")
}
Button(
onClick = {
if(drug.Id == -1) {
Service.AddDrug(drug)
} else {
Service.UpdateDrug(drug)
}
finish()
},
colors = ButtonColors(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.onPrimary,
MaterialTheme.colorScheme.onSurfaceVariant,
MaterialTheme.colorScheme.onSurface
),
enabled = drug.Name.isNotEmpty() && drug.Manufacturer.isNotEmpty() && drug.Category.isNotEmpty() && drug.NumberOfUnits > 0 && drug.RetailPrice > 0.0
) {
Text(if(drug.Id == -1) "Add" else "Update")
}
}
}
}
}
}
}
@@ -0,0 +1,49 @@
package com.example.ma_ui_native
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
object Service {
private var nextId = 11
private val DrugRepo = mutableStateListOf<MutableState<Drug>>(
mutableStateOf( Drug(1, "Paracetamol", "Analgesic", 100, 2.5, "GSK")),
mutableStateOf(Drug(2, "Amoxicillin", "Antibiotic", 50, 3.5, "Pfizer")),
mutableStateOf(Drug(3, "Omeprazole", "Antacid", 75, 1.5, "AstraZeneca")),
mutableStateOf(Drug(4, "Atorvastatin", "Anticholesterol", 25, 4.5, "Pfizer")),
mutableStateOf(Drug(5, "Metformin", "Antidiabetic", 30, 2.0, "Merck")),
mutableStateOf(Drug(6, "Amlodipine", "Antihypertensive", 40, 3.0, "Novartis")),
mutableStateOf(Drug(7, "Losartan", "Antihypertensive", 35, 3.5, "Merck")),
mutableStateOf(Drug(8, "Simvastatin", "Anticholesterol", 20, 4.0, "AstraZeneca")),
mutableStateOf(Drug(9, "Salbutamol", "Bronchodilator", 45, 2.5, "GSK")),
mutableStateOf(Drug(10, "Levothyroxine", "Hormone", 60, 1.5, "Novartis"))
)
public fun GetDrugs() : SnapshotStateList<MutableState<Drug>> {
return DrugRepo;
}
public fun GetDrug(id: Int) : Drug {
return DrugRepo.find { it.value.Id == id }?.value ?: Drug()
}
public fun AddDrug(drug: Drug) {
drug.Id = nextId++
DrugRepo.add(mutableStateOf( drug))
}
public fun UpdateDrug(drug: Drug) {
val index = DrugRepo.indexOfFirst { it.value.Id == drug.Id }
if (index != -1) {
DrugRepo[index] = mutableStateOf( drug)
}
}
public fun DeleteDrug(id: Int) {
val index = DrugRepo.indexOfFirst { it.value.Id == id }
if (index != -1) {
DrugRepo.removeAt(index)
}
}
}
@@ -0,0 +1,14 @@
package com.example.ma_ui_native.ui.theme
import androidx.compose.ui.graphics.Color
var Primary = Color(0xFF675496)
var OnPrimary = Color(0xFFFFFFFF)
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)
@@ -0,0 +1,58 @@
package com.example.ma_ui_native.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun MAUINativeTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
@@ -0,0 +1,34 @@
package com.example.ma_ui_native.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="m336,680 l144,-144 144,144 56,-56 -144,-144 144,-144 -56,-56 -144,144 -144,-144 -56,56 144,144 -144,144 56,56ZM480,880q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880ZM480,800q134,0 227,-93t93,-227q0,-134 -93,-227t-227,-93q-134,0 -227,93t-93,227q0,134 93,227t227,93ZM480,480Z"
android:fillColor="#e8eaed"/>
</vector>
@@ -0,0 +1,170 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>
@@ -0,0 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">MA-UI-Native</string>
</resources>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="Theme.MAUINative" parent="android:Theme.Material.Light.NoActionBar" />
</resources>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample backup rules file; uncomment and customize as necessary.
See https://developer.android.com/guide/topics/data/autobackup
for details.
Note: This file is ignored for devices older that API 31
See https://developer.android.com/about/versions/12/backup-restore
-->
<full-backup-content>
<!--
<include domain="sharedpref" path="."/>
<exclude domain="sharedpref" path="device.xml"/>
-->
</full-backup-content>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?><!--
Sample data extraction rules file; uncomment and customize as necessary.
See https://developer.android.com/about/versions/12/backup-restore#xml-changes
for details.
-->
<data-extraction-rules>
<cloud-backup>
<!-- TODO: Use <include> and <exclude> to control what is backed up.
<include .../>
<exclude .../>
-->
</cloud-backup>
<!--
<device-transfer>
<include .../>
<exclude .../>
</device-transfer>
-->
</data-extraction-rules>
@@ -0,0 +1,17 @@
package com.example.ma_ui_native
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}
@@ -0,0 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.kotlin.compose) apply false
}
@@ -0,0 +1,23 @@
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. For more details, visit
# https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
# org.gradle.parallel=true
# AndroidX package structure to make it clearer which packages are bundled with the
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
@@ -0,0 +1,32 @@
[versions]
agp = "8.7.1"
kotlin = "2.0.0"
coreKtx = "1.10.1"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.04.01"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
@@ -0,0 +1,6 @@
#Sun Oct 27 15:55:16 EET 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1,23 @@
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MA-UI-Native"
include(":app")
@@ -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"
]
}
@@ -0,0 +1,75 @@
# Mobile App Requirements: Medicinal Drug Management
## Description
You have been tasked with developing a mobile application for managing medicinal drugs for our local drug store. The app should allow users to view, add, edit, and delete medicinal drugs. The application should also provide offline access, allowing users to interact with the data even when not connected to the internet. When online, the app should sync any changes made locally with the remote server.
## 1. Main Screen: Business Object Listing
The main screen should provide a list of all available medicinal drugs.
### Each entry should display relevant information about the drug:
#### - Name: consisting of the drug's name.
#### - Category: indicating the drug's category or type. (e.g., Antibiotic, Painkiller, etc.)
#### - Number of Units: indicating the quantity of the drug available.
#### - Retail Price: displaying the price the drug will be selling at.
#### - Manufacturer: showing the company that manufactures the drug.
### Offline Behavior:
If offline, the user will be able to see the locally saved drugs
When online, automatically sync the the local database with the server
## 2. Create (Add New Medicinal Drugs)
### Input Form:
#### Allow users to input drug details such as name, category, number of units, retail price, and manufacturer.
#### Include data validation for required fields and proper formats:
- number of units - positive integer number
- retail price - positive decimal number
### Offline Behavior:
If offline, save the newly created drug locally with a message: “New drug saved locally. Will sync with server when online.”
Ensure that unsynced drugs are flagged for synchronization once the app reconnects to the server.
Online Synchronization:
When online, automatically sync the newly created drugs with the remote server and update the local database.
## 3. Update (Edit Existing Medicinal Drugs)
### Edit Functionality:
#### Users can edit existing drug. Provide a form with pre-filled fields for the selected drug.
Provide a clear “Save” button to confirm updates.
### Offline Behavior:
If offline, prevent updates with a message: “Cannot update while offline. Please reconnect to perform this operation.”
Changes should not be allowed without a network connection.
### Online Synchronization:
Ensure that any updates are immediately synced with the server when online, updating both local and remote records.
## 4. Delete (Remove Medicinal Drugs)
### Deletion Confirmation:
Allow users to delete a drug, but prompt for confirmation with a message: “Are you sure you want to delete this drug?”
### Offline Behavior:
If offline, allow deletion with a message: “Drug deleted locally. Will sync with server when online.”
Online Synchronization:
Once online, successfully sync deletions, removing the drug from both the local database and the remote server.
## 5. Data Persistence (Local Storage & Offline Access)
### Local Database:
The application must persist all business objects (medicinal drugs) in a local database for offline use.
This ensures that users can view and interact with previously synced data even when offline.
Synchronization:
On regaining network connectivity, the app should sync local changes (new, updated, or deleted drugs) with the remote server.
Synchronization should be seamless, with background processing where possible.
# Figma
https://www.figma.com/proto/gHQjuiaorcTJeI7Tl9zqfL/Silk-Road?node-id=6-2711&starting-point-node-id=13%3A3146
[Main](figma1.png)
[Delete](figma2.png)
[Update](figma3.png)
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because it is too large Load Diff
@@ -0,0 +1,12 @@
{
"dependencies": {
"body-parser": "^1.20.3",
"express": "^4.21.2",
"mysql": "^2.18.1",
"reconnecting-websocket": "^4.4.0",
"reconnectingwebsocket": "^1.0.0",
"socket.io": "^4.8.1",
"socket.io-client": "^4.8.1",
"ws": "^8.18.0"
}
}
@@ -0,0 +1,112 @@
const express = require('express');
const http = require('http');
const mysql = require('mysql');
const bodyParser = require('body-parser');
const cors = require('cors')
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
const app = express();
app.use(cors())
const server = http.createServer(app);
app.use(bodyParser.json());
// MySQL connection
const db = mysql.createConnection({
host: 'localhost',
port: 3306,
user: 'root',
database: 'mobile'
});
db.connect(err => {
if (err) {
console.error('Database connection failed:', err.stack);
return;
}
console.log('Connected to database.');
});
app.get('/', (req, res) => {
res.status(200).end();
});
// CRUD routes
app.get('/drug', (req, res) => {
db.query('SELECT * FROM drug', (err, results) => {
if (err) throw err;
res.json(results);
});
});
app.post('/drug', (req, res) => {
const newItem = req.body;
delete newItem.id;
db.query('INSERT INTO drug SET ?', newItem, (err, result) => {
if (err) throw err;
emit('itemAdded', { id: parseInt(result.insertId), ...newItem });
res.status(201).json({ id: parseInt(result.insertId), ...newItem });
});
});
app.put('/drug', (req, res) => {
const updatedItem = req.body;
const id = parseInt(req.query.id);
db.query('UPDATE drug SET ? WHERE id = ?', [updatedItem, id], (err) => {
if (err) throw err;
emit('itemUpdated', { id, ...updatedItem });
res.status(200).json({ id, ...updatedItem });
});
});
app.delete('/drug', (req, res) => {
const id = parseInt(req.query.id);
db.query('DELETE FROM drug WHERE id = ?', [id], (err) => {
if (err) throw err;
emit('itemDeleted', { id });
res.status(204).end();
});
});
// WebSocket connection
wss.on('connection', (socket) => {
console.log('New client connected');
socket.on('disconnect', () => {
console.log('Client disconnected');
});
socket.on('message', (message) => {
console.log('Message received:', message);
});
socket.on('close', () => {
console.log('Client disconnected');
});
socket.on('error', (err) => {
console.error('Error:', err);
});
});
wss.on('close', () => {
console.log('Client disconnected');
});
wss.on('error', (err) => {
console.error('Error:', err);
});
const emit = (event, data) => {
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify({ event, data }));
}
});
};
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});
@@ -0,0 +1,23 @@
const ReconnectingWebSocket = require("reconnecting-websocket");
const WS = require("ws");
const ws = new ReconnectingWebSocket(`ws://${"192.168.0.45"}:${"8080"}`, [], {
WebSocket: WS.WebSocket,
connectionTimeout: 1000,
startClosed: true
});
ws.onopen = () => {
console.log("Connected to server.");
}
ws.onmessage = data => {
const message = JSON.parse(data);
console.log("Received message:", message);
}
while (true) { }