Anul 3 Semestrul 1
@@ -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>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 982 B |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
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>
|
||||
);
|
||||
}
|
||||
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 17 KiB |
@@ -0,0 +1,38 @@
|
||||
import { RootState } from "@/redux/store";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Snackbar } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
export const ConnectionNotification = () => {
|
||||
const dispatch = useDispatch();
|
||||
const status = useSelector((state: RootState) => state.status);
|
||||
const [visible, setVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!status.isConnected || !status.isOnline) {
|
||||
setVisible(true);
|
||||
}
|
||||
else{
|
||||
setVisible(false);
|
||||
}
|
||||
}, [status.isConnected, status.isOnline]);
|
||||
const onDismiss = () => {
|
||||
setVisible(false);
|
||||
};
|
||||
return (
|
||||
<Snackbar
|
||||
wrapperStyle={{ top: 50, left: "50%", position: 'absolute', transform: [{ translateX: "-42%" }], zIndex: 9999}}
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={{ zIndex: 9999, }}
|
||||
action={{
|
||||
label: 'Dismiss',
|
||||
onPress: () => {
|
||||
setVisible(false);
|
||||
},
|
||||
}}
|
||||
duration={60000}
|
||||
>
|
||||
No internet connection
|
||||
</Snackbar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { initDB } from "@/database/localdb";
|
||||
import { useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useDispatch } from "react-redux";
|
||||
|
||||
|
||||
export const DataLoader = () => {
|
||||
const dispatch = useDispatch();
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
dispatch({ type: 'drug/initialize' });
|
||||
dispatch({ type: 'drug/fetch' });
|
||||
};
|
||||
fetchData();
|
||||
}, [dispatch]);
|
||||
return (<View></View>);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
import React from "react";
|
||||
import { Button, Dialog, MD3Theme, Portal, useTheme, Text } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
|
||||
export default function DeletePopUp(props: {drugId: number, onCancel: () => void, onDelete: () => void}) {
|
||||
const theme = useTheme();
|
||||
const style = styles(theme);
|
||||
const navigation = useNavigation<any>(); //TODO: types...
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<Dialog
|
||||
visible={true}
|
||||
>
|
||||
<Dialog.Title>Delete Drug</Dialog.Title>
|
||||
<Dialog.Content>
|
||||
<Text variant="bodyMedium">Are you sure you want to delete this drug? This action can not be reversed</Text>
|
||||
</Dialog.Content>
|
||||
<Dialog.Actions>
|
||||
<Button contentStyle={{paddingHorizontal: 8}} mode="contained" onPress={props.onCancel}>Cancel</Button>
|
||||
<Button contentStyle={{paddingHorizontal: 8}} mode="contained" onPress={props.onDelete}>Delete</Button>
|
||||
</Dialog.Actions>
|
||||
</Dialog>
|
||||
</Portal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = (theme: MD3Theme) => StyleSheet.create({
|
||||
|
||||
})
|
||||
@@ -0,0 +1,204 @@
|
||||
import { Drug } from "@/redux/features/drug/drug";
|
||||
import { View, Text, StyleSheet } from "react-native";
|
||||
import { Button, Divider, MD3Theme, useTheme } from "react-native-paper";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/redux/store";
|
||||
|
||||
export default function DrugDisplay(props: {drug: Drug, onUpdate: (drugId: number) => void, onDelete: (drugId: number) => void}) {
|
||||
const theme = useTheme();
|
||||
const style = styles(theme);
|
||||
const status = useSelector((state: RootState) => state.status);
|
||||
|
||||
return (
|
||||
<View className="box" style={style.outerbox}>
|
||||
<View className="box" style={style.boxBackground} >
|
||||
|
||||
</View>
|
||||
<View className="column" style={style.boxForeground}>
|
||||
<View style={style.rowHeadlineMargin}>
|
||||
<View className="row" style={style.rowHeadline}>
|
||||
<View className="column" style={style.column}>
|
||||
<Text style={style.nameText}>{props.drug.name}</Text>
|
||||
<Text style={style.manufacturerText}>{props.drug.manufacturer}</Text>
|
||||
</View>
|
||||
<View className="row" style={style.rowCTA}>
|
||||
<Button disabled={!status.isConnected || !status.isOnline} contentStyle={{paddingHorizontal: 8}} textColor={theme.colors.primary} buttonColor={theme.colors.surface} style={style.editButton} onPress={() => props.onUpdate(props.drug.id)}>Edit</Button>
|
||||
<Button contentStyle={{paddingHorizontal: 8}} textColor={theme.colors.onPrimary} buttonColor={theme.colors.primary} onPress={() => props.onDelete(props.drug.id)} >Delete</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<Divider style={style.horizontalDivider} />
|
||||
<View style={style.chipMargin}>
|
||||
<View className="row" style={style.chip}>
|
||||
<View className="box" style={style.labelChip}>
|
||||
<Text style={style.label}>Category:</Text>
|
||||
</View>
|
||||
<View className="box" style={style.valueChip}>
|
||||
<Text style={style.value}>{props.drug.category}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.chipMargin}>
|
||||
<View className="row" style={style.chip}>
|
||||
<View className="box" style={style.labelChip}>
|
||||
<Text style={style.label}># of Units:</Text>
|
||||
</View>
|
||||
<View className="box" style={style.valueChip}>
|
||||
<Text style={style.value}>{props.drug.numberOfUnits.toFixed(0)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
<View style={{...style.chipMargin, ...style.lastChipMargin}}>
|
||||
<View className="row" style={style.chip}>
|
||||
<View className="box" style={style.labelChip}>
|
||||
<Text style={style.label}>Price:</Text>
|
||||
</View>
|
||||
<View className="box" style={style.valueChip}>
|
||||
<Text style={style.value}>${props.drug.price.toFixed(2)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = (theme: MD3Theme) => StyleSheet.create({
|
||||
boxBackground: {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundColor: theme.colors.surface,
|
||||
borderRadius: 12,
|
||||
borderColor: theme.colors.outlineVariant,
|
||||
borderWidth: 1,
|
||||
zIndex: 10,
|
||||
},
|
||||
boxForeground: {
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 20,
|
||||
|
||||
},
|
||||
outerbox: {
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
marginBottom: 8
|
||||
},
|
||||
rowHeadlineMargin:{
|
||||
marginTop: 16,
|
||||
marginRight: 16,
|
||||
marginBottom: 7,
|
||||
marginLeft: 16,
|
||||
},
|
||||
rowHeadline: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
height: 44,
|
||||
|
||||
},
|
||||
column:{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
},
|
||||
rowCTA: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 8,
|
||||
},
|
||||
nameText:{
|
||||
color: theme.colors.onSurface,
|
||||
fontFamily: theme.fonts.bodyLarge.fontFamily,
|
||||
fontSize: theme.fonts.bodyLarge.fontSize,
|
||||
lineHeight: theme.fonts.bodyLarge.lineHeight,
|
||||
letterSpacing: theme.fonts.bodyLarge.letterSpacing,
|
||||
fontWeight: theme.fonts.bodyLarge.fontWeight,
|
||||
fontStyle: theme.fonts.bodyLarge.fontStyle
|
||||
},
|
||||
manufacturerText:{
|
||||
color: theme.colors.onSurfaceVariant,
|
||||
fontFamily: theme.fonts.bodyMedium.fontFamily,
|
||||
fontSize: theme.fonts.bodyMedium.fontSize,
|
||||
lineHeight: theme.fonts.bodyMedium.lineHeight,
|
||||
letterSpacing: theme.fonts.bodyMedium.letterSpacing,
|
||||
fontWeight: theme.fonts.bodyMedium.fontWeight,
|
||||
fontStyle: theme.fonts.bodyMedium.fontStyle
|
||||
},
|
||||
editButton:{
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.outline,
|
||||
},
|
||||
horizontalDivider:{
|
||||
marginRight: 16,
|
||||
marginBottom: 16,
|
||||
marginLeft: 7,
|
||||
},
|
||||
|
||||
chipMargin:{
|
||||
marginLeft: 16,
|
||||
marginRight: 16,
|
||||
marginBottom: 7,
|
||||
},
|
||||
|
||||
chip: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
shadowRadius: 12, //TODO: shadow wack
|
||||
shadowOffset: { width: 2, height: 2 },
|
||||
width: "100%" ,
|
||||
height: 44,
|
||||
backgroundColor: theme.colors.surfaceDisabled,
|
||||
borderRadius: 12
|
||||
},
|
||||
lastChipMargin: {
|
||||
marginBottom: 40
|
||||
},
|
||||
labelChip: {
|
||||
height: "100%",
|
||||
width: "38%",
|
||||
backgroundColor: theme.colors.primary,
|
||||
borderTopLeftRadius: 12,
|
||||
borderBottomLeftRadius: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
},
|
||||
|
||||
label: {
|
||||
color: theme.colors.onPrimary,
|
||||
textAlign: "center",
|
||||
fontFamily: theme.fonts.titleSmall.fontFamily,
|
||||
fontSize: theme.fonts.titleSmall.fontSize,
|
||||
lineHeight: theme.fonts.titleSmall.lineHeight,
|
||||
letterSpacing: theme.fonts.titleSmall.letterSpacing,
|
||||
fontWeight: theme.fonts.titleSmall.fontWeight,
|
||||
fontStyle: theme.fonts.titleSmall.fontStyle
|
||||
},
|
||||
valueChip: {
|
||||
height: "100%",
|
||||
width: "62%",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
},
|
||||
|
||||
value: {
|
||||
color: theme.colors.onSurfaceVariant,
|
||||
textAlign: "center",
|
||||
fontFamily: theme.fonts.bodyMedium.fontFamily,
|
||||
fontSize: theme.fonts.bodyMedium.fontSize,
|
||||
lineHeight: theme.fonts.bodyMedium.lineHeight,
|
||||
letterSpacing: theme.fonts.bodyMedium.letterSpacing,
|
||||
fontWeight: theme.fonts.bodyMedium.fontWeight,
|
||||
fontStyle: theme.fonts.bodyMedium.fontStyle
|
||||
},
|
||||
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Drug } from "@/redux/features/drug/drug";
|
||||
import { View, StyleSheet, FlatList, Dimensions } from "react-native";
|
||||
import { RootState, AppDispatch } from "@/redux/store";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import React, { useState } from "react";
|
||||
import DrugDisplay from "./DrugDisplay";
|
||||
import { FAB, MD3Theme, useTheme } from "react-native-paper";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import DeletePopUp from "./DeletePopUp";
|
||||
import { drugSlice, removeDrug } from "@/redux/features/drug/drugSlice";
|
||||
import { ConnectionNotification } from "./ConnectionNotification";
|
||||
import { ErrorNotification } from "./ErrorNotification";
|
||||
|
||||
export default function DrugList() {
|
||||
const drugs: Drug[] = useSelector((state: RootState) => state.drug.drugList);
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const style = styles(theme);
|
||||
const navigation = useNavigation<any>(); //TODO: types...
|
||||
|
||||
const [deleteIndex, useDeleteIndex] = useState(-1)
|
||||
|
||||
return (
|
||||
<View style={style.listMargin}>
|
||||
<ConnectionNotification />
|
||||
{/* <ErrorNotification /> */}
|
||||
<FAB
|
||||
color={theme.colors.onPrimary}
|
||||
style={style.fab}
|
||||
icon="pencil-outline"
|
||||
mode="elevated"
|
||||
onPress={() => {
|
||||
navigation.navigate("add");
|
||||
}}
|
||||
/>
|
||||
{deleteIndex != -1 ?
|
||||
<DeletePopUp
|
||||
drugId={deleteIndex}
|
||||
onCancel = {() => {
|
||||
useDeleteIndex(-1)
|
||||
}}
|
||||
onDelete = {() => {
|
||||
dispatch({type: "drug/remove", payload: deleteIndex})
|
||||
useDeleteIndex(-1)
|
||||
}}
|
||||
/> : null}
|
||||
<FlatList style={style.list}
|
||||
data={drugs}
|
||||
renderItem={({item})=><DrugDisplay
|
||||
key={item.id}
|
||||
drug={item}
|
||||
onUpdate={(drugId: number) => {
|
||||
navigation.navigate("update/[id]", {id: item.id});
|
||||
}}
|
||||
onDelete={(drugId: number) => {
|
||||
useDeleteIndex(drugId)
|
||||
}} />}
|
||||
keyExtractor={item => item.id.toString()}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = (theme: MD3Theme) => StyleSheet.create({
|
||||
listMargin:{
|
||||
paddingTop: 75.5,
|
||||
paddingBottom: 23.5,
|
||||
paddingLeft: 26,
|
||||
paddingRight: 26,
|
||||
height: Dimensions.get('window').height,
|
||||
flex: 1
|
||||
},
|
||||
list: {
|
||||
backgroundColor: theme.colors.surface,
|
||||
height: "100%"
|
||||
},
|
||||
fab: {
|
||||
position: 'absolute',
|
||||
margin: 48,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 30,
|
||||
borderRadius: 30,
|
||||
backgroundColor: theme.colors.primary
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,39 @@
|
||||
import { RootState } from "@/redux/store";
|
||||
import { useState, useEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Snackbar } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
export const ErrorNotification = () => {
|
||||
const dispatch = useDispatch();
|
||||
const status = useSelector((state: RootState) => state.errorStatus);
|
||||
const [visible, setVisible] = useState(false);
|
||||
useEffect(() => {
|
||||
if (status.status) {
|
||||
setVisible(true);
|
||||
}
|
||||
else{
|
||||
setVisible(false);
|
||||
}
|
||||
}, [status.message]);
|
||||
const onDismiss = () => {
|
||||
dispatch({type: "errorStatus/setErrorStatus", payload: {status: false, message: ""}})
|
||||
};
|
||||
return (
|
||||
<View style={{ zIndex: 9999, flex: 1, justifyContent: 'center', position: 'absolute', width: '100%', bottom: 0, left: "50%", transform: [{ translateX: "-42%" }]}}>
|
||||
<Snackbar
|
||||
visible={visible}
|
||||
onDismiss={onDismiss}
|
||||
style={{ zIndex: 9999}}
|
||||
action={{
|
||||
label: 'Dismiss',
|
||||
onPress: () => {
|
||||
setVisible(false);
|
||||
},
|
||||
}}
|
||||
>
|
||||
{status.message}
|
||||
</Snackbar>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { Drug } from "@/redux/features/drug/drug";
|
||||
import { AppDispatch, RootState } from "@/redux/store";
|
||||
import { useNavigation } from "@react-navigation/native";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Dimensions, Text, View, StyleSheet } from "react-native";
|
||||
import { Button, HelperText, MD3Theme, TextInput, useTheme } from "react-native-paper";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
export default function Form(props: {drugId?: number}) {
|
||||
const [drug, setDrug] = useState<Drug>({
|
||||
id: -1,
|
||||
name: "",
|
||||
manufacturer: "",
|
||||
category: "",
|
||||
price: 0,
|
||||
numberOfUnits: 0
|
||||
});
|
||||
const [price , setPrice] = useState("0");
|
||||
const updateDrug = useSelector((state: RootState) => state.drug.drugList.find(drug => drug.id === props.drugId));
|
||||
|
||||
const dispatch: AppDispatch = useDispatch();
|
||||
const theme = useTheme();
|
||||
const style = styles(theme);
|
||||
const navigation = useNavigation<any>(); //TODO: types...
|
||||
useEffect(() => {
|
||||
if(props.drugId){
|
||||
if(updateDrug){
|
||||
setDrug(updateDrug);
|
||||
setPrice(updateDrug.price.toFixed(2));
|
||||
}
|
||||
}
|
||||
}, [updateDrug]);
|
||||
|
||||
const onSubmit = () => {
|
||||
if(props.drugId){
|
||||
dispatch({type: "drug/update", payload: drug});
|
||||
}
|
||||
else{
|
||||
dispatch({type: "drug/add", payload: drug});
|
||||
}
|
||||
}
|
||||
|
||||
const isDisabled = () => {
|
||||
return drug.name === "" || drug.manufacturer === "" || drug.category === "" || drug.price === 0 || drug.numberOfUnits === 0;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={style.mainMargin}>
|
||||
<View style={style.column}>
|
||||
<View style={style.chipMargin}>
|
||||
<View style={style.chip}>
|
||||
<Text style={style.chipLabel}>{props.drugId ? "Update Drug" : "Add Drug"}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.fieldList}>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Name"
|
||||
value={drug?.name}
|
||||
onChangeText={(text) => {
|
||||
setDrug({...drug, name: text} as Drug)
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => setDrug({...drug, name: ""} as Drug)}/>}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Manufacturer"
|
||||
value={drug?.manufacturer}
|
||||
onChangeText={(text) => {
|
||||
setDrug({...drug, manufacturer: text} as Drug)
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => setDrug({...drug, manufacturer: ""} as Drug)}/>}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Category"
|
||||
value={drug?.category}
|
||||
onChangeText={(text) => {
|
||||
setDrug({...drug, category: text} as Drug)
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => setDrug({...drug, category: ""} as Drug)}/>}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Number of Units"
|
||||
value={drug?.numberOfUnits.toString()}
|
||||
keyboardType="numeric"
|
||||
onChangeText={(text) => {
|
||||
if(text === ""){
|
||||
setDrug({...drug, numberOfUnits: 0} as Drug)
|
||||
}
|
||||
else{
|
||||
setDrug({...drug, numberOfUnits: parseInt(text)} as Drug)
|
||||
}
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => setDrug({...drug, numberOfUnits: 0} as Drug)}/>}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
<View style={style.inputWrapper}>
|
||||
<TextInput
|
||||
label="Price"
|
||||
value={price}
|
||||
keyboardType="numeric"
|
||||
onChangeText={(text) => {
|
||||
setPrice(text);
|
||||
if(text === "" || isNaN(Number(text)) || text[text.length - 1] === '.'){
|
||||
setDrug({...drug, price: 0} as Drug)
|
||||
}
|
||||
else{
|
||||
setPrice(parseFloat(text).toString());
|
||||
setDrug({...drug, price: parseFloat(text)} as Drug)
|
||||
}
|
||||
}}
|
||||
right={<TextInput.Icon icon="close" onPress={() => {setDrug({...drug, price: 0} as Drug); setPrice("")}}/>}
|
||||
left={<TextInput.Affix text="$ " />}
|
||||
/>
|
||||
<HelperText type="error" visible={true}>*required</HelperText>
|
||||
</View>
|
||||
</View>
|
||||
<View style={style.buttonRow}>
|
||||
<Button mode="outlined" onPress={() => navigation.navigate("index")}>Cancel</Button>
|
||||
<Button mode="contained"
|
||||
textColor={theme.colors.onPrimary}
|
||||
buttonColor={theme.colors.primary}
|
||||
onPress={() => {
|
||||
if(!isDisabled())
|
||||
{
|
||||
onSubmit();
|
||||
navigation.navigate("index")
|
||||
}
|
||||
}}>{props.drugId ? "Update" : "Add"}</Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = (theme: MD3Theme) => StyleSheet.create({
|
||||
mainMargin:{
|
||||
paddingTop: 63,
|
||||
paddingBottom: 62,
|
||||
paddingLeft: 62,
|
||||
paddingRight: 73,
|
||||
height: Dimensions.get('window').height,
|
||||
flex: 1
|
||||
},
|
||||
column: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.colors.surface,
|
||||
},
|
||||
chipMargin: {
|
||||
paddingBottom: 25,
|
||||
paddingLeft: 61,
|
||||
paddingRight: 61,
|
||||
},
|
||||
chip: {
|
||||
height: 49,
|
||||
width: "100%",
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.colors.outlineVariant,
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
chipLabel: {
|
||||
color: theme.colors.onSurface,
|
||||
textAlign: "center",
|
||||
fontFamily: theme.fonts.labelLarge.fontFamily,
|
||||
fontSize: theme.fonts.labelLarge.fontSize,
|
||||
lineHeight: theme.fonts.labelLarge.lineHeight,
|
||||
letterSpacing: theme.fonts.labelLarge.letterSpacing,
|
||||
fontWeight: theme.fonts.labelLarge.fontWeight,
|
||||
fontStyle: theme.fonts.labelLarge.fontStyle
|
||||
|
||||
},
|
||||
fieldList: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
marginBottom: 30,
|
||||
width: "100%"
|
||||
},
|
||||
buttonRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
gap: 8
|
||||
},
|
||||
inputWrapper: {
|
||||
marginBottom: 16,
|
||||
width: "100%"
|
||||
}
|
||||
|
||||
})
|
||||
@@ -0,0 +1,3 @@
|
||||
export const SERVER_IP = "192.168.0.45" as const;
|
||||
export const SERVER_PORT = 3000 as const;
|
||||
export const WS_PORT = 8080 as const;
|
||||
@@ -0,0 +1,124 @@
|
||||
import { Drug } from '@/redux/features/drug/drug';
|
||||
import { openDatabaseAsync, SQLiteDatabase } from 'expo-sqlite'
|
||||
export const getDBConnection = async () => {
|
||||
return await openDatabaseAsync("drug.db");
|
||||
};
|
||||
|
||||
export type TempDrug = {
|
||||
id: number,
|
||||
type: number,
|
||||
drug?: Drug
|
||||
}
|
||||
|
||||
export const createTable = async (db: SQLiteDatabase) => {
|
||||
const exists = await db.getAllAsync(`SELECT name FROM sqlite_master WHERE type='table' AND name='Drug';`);
|
||||
if (exists.length > 0) {
|
||||
return;
|
||||
}
|
||||
const query = `CREATE TABLE IF NOT EXISTS Drug (
|
||||
id INTEGER PRIMARY KEY,
|
||||
name TEXT,
|
||||
category TEXT,
|
||||
price REAL,
|
||||
numberOfUnits INTEGER,
|
||||
manufacturer TEXT
|
||||
);`;
|
||||
await db.runAsync(query);
|
||||
const tempTable = `CREATE TABLE IF NOT EXISTS TempDrug (
|
||||
id INTEGER,
|
||||
type INTEGER);`;
|
||||
await db.runAsync(tempTable);
|
||||
};
|
||||
|
||||
export const insertDrug = async (db: SQLiteDatabase, drug: Drug) => {
|
||||
const query = `INSERT INTO Drug (${drug.id !== -1 ? "id," : ""} name, category, price, numberOfUnits, manufacturer) VALUES (${drug.id !== -1 ? "?," : ""} ?, ?, ?, ?, ?);`;
|
||||
const values = [drug.name, drug.category, drug.price, drug.numberOfUnits, drug.manufacturer];
|
||||
if (drug.id !== -1) {
|
||||
values.unshift(drug.id);
|
||||
}
|
||||
const response = await db.runAsync(query, values);
|
||||
console.log("Inserted into local db");
|
||||
return response.lastInsertRowId;
|
||||
}
|
||||
|
||||
export const getDrugs = async (db: SQLiteDatabase) => {
|
||||
const query = `SELECT * FROM Drug;`;
|
||||
const response = await db.getAllAsync(query);
|
||||
const drugs: Drug[] = [];
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
drugs.push(response[i] as Drug);
|
||||
}
|
||||
return drugs;
|
||||
}
|
||||
|
||||
export const getDrug = async (db: SQLiteDatabase, id: number) => {
|
||||
const query = `SELECT * FROM Drug WHERE id = ?;`;
|
||||
const values = [id];
|
||||
const response = await db.getFirstAsync(query, values);
|
||||
return response as Drug;
|
||||
}
|
||||
|
||||
export const editDrug = async (db: SQLiteDatabase, drug: Drug) => {
|
||||
const query = `UPDATE Drug SET name = ?, category = ?, price = ?, numberOfUnits = ?, manufacturer = ? WHERE id = ?;`;
|
||||
const values = [drug.name, drug.category, drug.price, drug.numberOfUnits, drug.manufacturer, drug.id];
|
||||
await db.runAsync(query, values);
|
||||
}
|
||||
|
||||
export const deleteDrug = async (db: SQLiteDatabase, id: number) => {
|
||||
const query = `DELETE FROM Drug WHERE id = ?;`;
|
||||
const values = [id];
|
||||
await db.runAsync(query, values);
|
||||
console.log("Deleted from local db");
|
||||
}
|
||||
|
||||
export const rehydrateLocalDB = async (drugs: Drug[]) => {
|
||||
const db = await getDBConnection();
|
||||
await db.runAsync(`DELETE FROM Drug;`);
|
||||
for (let i = 0; i < drugs.length; i++) {
|
||||
await insertDrug(db, drugs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const initDB = async () => {
|
||||
const db = await getDBConnection();
|
||||
await createTable(db);
|
||||
return db;
|
||||
}
|
||||
|
||||
export const insertTempDrug = async (db: SQLiteDatabase, tempDrug: TempDrug) => {
|
||||
console.log("Inserting temp drug");
|
||||
console.log(tempDrug);
|
||||
const query = `INSERT INTO TempDrug (id, type) VALUES (?, ?);`;
|
||||
const values = [tempDrug.id, tempDrug.type];
|
||||
await db.runAsync(query, values);
|
||||
}
|
||||
|
||||
export const getTempDrugs = async (db: SQLiteDatabase) => {
|
||||
const query = `SELECT Drug.*, TempDrug.id, TempDrug.type FROM TempDrug LEFT JOIN Drug ON TempDrug.id = Drug.id;`;
|
||||
const response = await db.getAllAsync(query);
|
||||
const tempDrugs: TempDrug[] = [];
|
||||
for (let i = 0; i < response.length; i++) {
|
||||
const responseDrug = response[i] as any;
|
||||
const tempDrug = {
|
||||
id: responseDrug.id,
|
||||
type: responseDrug.type,
|
||||
drug: responseDrug.name ? {
|
||||
id: responseDrug.id,
|
||||
name: responseDrug.name,
|
||||
category: responseDrug.category,
|
||||
price: responseDrug.price,
|
||||
numberOfUnits: responseDrug.numberOfUnits,
|
||||
manufacturer: responseDrug.manufacturer
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
tempDrugs.push(tempDrug);
|
||||
}
|
||||
return tempDrugs;
|
||||
}
|
||||
|
||||
export const deleteTempDrug = async (db: SQLiteDatabase) => {
|
||||
const query = `DELETE FROM TempDrug`;
|
||||
await db.runAsync(query);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Drug } from "@/redux/features/drug/drug"
|
||||
import { SERVER_IP, SERVER_PORT } from "@/constants/Constants"
|
||||
import axios, { Axios, AxiosResponse } from "axios"
|
||||
import { TempDrug } from "./localdb";
|
||||
|
||||
export const insertDrugServer = async (drug: Drug) => {
|
||||
console.log(`Inserting drug ${drug}`);
|
||||
const response: AxiosResponse<Drug> = await axios.post(`http://${SERVER_IP}:${SERVER_PORT}/drug`, drug);
|
||||
if (response.status === 201) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error("Failed to insert drug");
|
||||
}
|
||||
|
||||
export const getDrugsServer = async () => {
|
||||
const response: AxiosResponse<Drug[]> = await axios.get(`http://${SERVER_IP}:${SERVER_PORT}/drug`);
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error("Failed to fetch drugs");
|
||||
}
|
||||
|
||||
export const editDrugServer = async (drug: Drug) => {
|
||||
console.log(`Editing drug ${drug}`);
|
||||
const response: AxiosResponse<Drug> = await axios.put(`http://${SERVER_IP}:${SERVER_PORT}/drug?id=${drug.id}`, drug);
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error("Failed to edit drug");
|
||||
}
|
||||
|
||||
export const deleteDrugServer = async (id: number) => {
|
||||
console.log(`Deleting drug with id ${id}`);
|
||||
const response: AxiosResponse = await axios.delete(`http://${SERVER_IP}:${SERVER_PORT}/drug?id=${id}`);
|
||||
if (response.status === 204) {
|
||||
return;
|
||||
}
|
||||
throw new Error("Failed to delete drug");
|
||||
}
|
||||
|
||||
export const syncOfflineData = async (offlineData: TempDrug[]) => {
|
||||
const idCounts: Record<number, number> = offlineData.reduce((acc, item) => {
|
||||
acc[item.id] = (acc[item.id] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<number, number>);
|
||||
|
||||
const filteredArray: TempDrug[] = offlineData.filter(item => idCounts[item.id] !== 2);
|
||||
|
||||
const tasks = [];
|
||||
for (const item of filteredArray) {
|
||||
if (item.type === 1) {
|
||||
tasks.push(insertDrugServer(item.drug!));
|
||||
} else {
|
||||
tasks.push(deleteDrugServer(item.id));
|
||||
}
|
||||
}
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { SERVER_IP, WS_PORT } from "@/constants/Constants";
|
||||
import ReconnectingWebSocket from 'reconnecting-websocket';
|
||||
import WS from 'ws';
|
||||
|
||||
const ws = new ReconnectingWebSocket(`ws://${SERVER_IP}:${WS_PORT}`, [], {
|
||||
WebSocket: WS.WebSocket,
|
||||
connectionTimeout: 1000,
|
||||
startClosed: true
|
||||
});
|
||||
|
||||
const getSocket = () => {
|
||||
return ws;
|
||||
}
|
||||
|
||||
export default getSocket;
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"build": {
|
||||
"preview": {
|
||||
"android": {
|
||||
"buildType": "apk"
|
||||
}
|
||||
},
|
||||
"preview2": {
|
||||
"android": {
|
||||
"gradleCommand": ":app:assembleRelease"
|
||||
}
|
||||
},
|
||||
"preview3": {
|
||||
"developmentClient": true
|
||||
},
|
||||
"preview4": {
|
||||
"distribution": "internal"
|
||||
},
|
||||
"production": {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
|
||||
|
||||
import { Drug } from '@/redux/features/drug/drug';
|
||||
import { deleteDrug, editDrug, getDBConnection, getDrugs, insertDrug, insertTempDrug } from '@/database/localdb';
|
||||
import { setErrorStatus } from '@/redux/features/errorstatus/errorStatusSlice';
|
||||
|
||||
export function* addDrug(action: { type: string, payload: Drug }): Generator<any, void, any> {
|
||||
try {
|
||||
const db = yield call(getDBConnection);
|
||||
const id = yield call(insertDrug, db, action.payload);
|
||||
yield call(insertTempDrug, db, { id: id, type: 1 });
|
||||
const { id: _, ...payloadWithoutId } = action.payload;
|
||||
yield put({ type: "drug/addDrug", payload: { id: id, ...payloadWithoutId } });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Local Database Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* removeDrug(action: { type: string, payload: number }): Generator<any, void, any> {
|
||||
try {
|
||||
const db = yield call(getDBConnection);
|
||||
yield call(deleteDrug, db, action.payload);
|
||||
yield call(insertTempDrug, db, { id: action.payload, type: 2 });
|
||||
yield put({ type: "drug/removeDrug", payload: action.payload });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Local Database Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* updateDrug(action: { type: string, payload: Drug }): Generator<any, void, any> {
|
||||
try {
|
||||
const db = yield call(getDBConnection);
|
||||
yield call(editDrug, db, action.payload);
|
||||
yield put({ type: "drug/updateDrug", payload: action.payload });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Local Database Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function* fetchDrugs(): Generator<any, void, any> {
|
||||
try {
|
||||
const db = yield call(getDBConnection);
|
||||
const drugs = yield call(getDrugs, db);
|
||||
yield put({ type: "drug/fetchDrugs", payload: drugs });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Local Database Error", status: true }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { initDB } from "@/database/localdb";
|
||||
import { call, put, select, takeEvery } from "redux-saga/effects";
|
||||
import { SERVER_IP, SERVER_PORT } from "@/constants/Constants";
|
||||
import axios, { AxiosResponse } from "axios"
|
||||
import { addDrug, fetchDrugs, removeDrug, updateDrug } from "./localdbSaga";
|
||||
import { fetch, NetInfoState } from "@react-native-community/netinfo";
|
||||
import { setConnected, setOnline } from "@/redux/features/status/statusSlice";
|
||||
import { Status } from "@/redux/features/status/status";
|
||||
import { addDrugServer, checkConnection, fetchDrugsServer, removeDrugServer, updateDrugServer, webSocketSaga } from "./serverSaga";
|
||||
|
||||
|
||||
function* initialize() {
|
||||
yield call(initDB);
|
||||
yield call(checkConnection);
|
||||
console.log("Initialized");
|
||||
yield put({ type: 'drug/fetch' });
|
||||
yield webSocketSaga();
|
||||
}
|
||||
|
||||
|
||||
function* fetchOfflineOrOnline() {
|
||||
const status: Status = yield select(state => state.status);
|
||||
if (status.isConnected && status.isOnline) {
|
||||
yield call(fetchDrugsServer);
|
||||
} else {
|
||||
yield call(fetchDrugs);
|
||||
}
|
||||
}
|
||||
|
||||
function* addDrugOfflineOrOnline(action: { type: string, payload: any }) {
|
||||
const status: Status = yield select(state => state.status);
|
||||
if (status.isConnected && status.isOnline) {
|
||||
yield call(addDrugServer, action);
|
||||
} else {
|
||||
yield call(addDrug, action);
|
||||
}
|
||||
}
|
||||
|
||||
function* removeDrugOfflineOrOnline(action: { type: string, payload: number }) {
|
||||
const status: Status = yield select(state => state.status);
|
||||
if (status.isConnected && status.isOnline) {
|
||||
yield call(removeDrugServer, action);
|
||||
} else {
|
||||
yield call(removeDrug, action);
|
||||
}
|
||||
}
|
||||
|
||||
function* updateDrugOfflineOrOnline(action: { type: string, payload: any }) {
|
||||
const status: Status = yield select(state => state.status);
|
||||
if (status.isConnected && status.isOnline) {
|
||||
yield call(updateDrugServer, action);
|
||||
} else {
|
||||
yield call(updateDrug, action);
|
||||
}
|
||||
}
|
||||
|
||||
export function* rootSaga() {
|
||||
yield takeEvery('drug/initialize', initialize);
|
||||
yield takeEvery('drug/fetch', fetchOfflineOrOnline);
|
||||
yield takeEvery('drug/add', addDrugOfflineOrOnline);
|
||||
yield takeEvery('drug/remove', removeDrugOfflineOrOnline);
|
||||
yield takeEvery('drug/update', updateDrugOfflineOrOnline);
|
||||
yield takeEvery('status/checkConnection', checkConnection);
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
import { deleteDrugServer, editDrugServer, getDrugsServer, insertDrugServer, syncOfflineData } from "@/database/serverdb";
|
||||
import { Drug } from "@/redux/features/drug/drug";
|
||||
import { all, call, put, retry, select, take } from "redux-saga/effects";
|
||||
import { deleteDrug, deleteTempDrug, editDrug, getDBConnection, getTempDrugs, insertDrug, rehydrateLocalDB, TempDrug } from "@/database/localdb";
|
||||
import { SQLiteDatabase } from "expo-sqlite";
|
||||
import { Status } from "@/redux/features/status/status";
|
||||
import { SERVER_IP, SERVER_PORT } from "@/constants/Constants";
|
||||
import { setOnline, setConnected } from "@/redux/features/status/statusSlice";
|
||||
import { NetInfoState, fetch } from "@react-native-community/netinfo";
|
||||
import axios, { AxiosResponse } from "axios";
|
||||
import { io, Socket } from "socket.io-client";
|
||||
import { store } from "@/redux/store";
|
||||
import { EventChannel, eventChannel } from "redux-saga";
|
||||
import { PayloadAction } from "@reduxjs/toolkit";
|
||||
import getSocket from "@/database/socket";
|
||||
import { setErrorStatus } from "@/redux/features/errorstatus/errorStatusSlice";
|
||||
import ReconnectingWebSocket from "reconnecting-websocket";
|
||||
|
||||
|
||||
export function* fetchDrugsServer() {
|
||||
try {
|
||||
const drugs: Drug[] = yield call(() => getDrugsServer());
|
||||
console.log(drugs);
|
||||
yield call(rehydrateLocalDB, drugs);
|
||||
yield put({ type: "drug/fetchDrugs", payload: drugs });
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* addDrugServer(action: { type: string, payload: Drug }) {
|
||||
try {
|
||||
const drug: Drug = yield call(() => insertDrugServer(action.payload));
|
||||
const db: SQLiteDatabase = yield call(getDBConnection);
|
||||
console.log(drug);
|
||||
yield call(() => insertDrug(db, drug));
|
||||
yield put({ type: "drug/addDrug", payload: drug });
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* removeDrugServer(action: { type: string, payload: number }) {
|
||||
try {
|
||||
yield call(() => deleteDrugServer(action.payload));
|
||||
const db: SQLiteDatabase = yield call(getDBConnection);
|
||||
yield call(() => deleteDrug(db, action.payload));
|
||||
yield put({ type: "drug/removeDrug", payload: action.payload });
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function* updateDrugServer(action: { type: string, payload: Drug }) {
|
||||
try {
|
||||
yield call(() => editDrugServer(action.payload));
|
||||
const db: SQLiteDatabase = yield call(getDBConnection);
|
||||
yield call(() => editDrug(db, action.payload));
|
||||
yield put({ type: "drug/updateDrug", payload: action.payload });
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function* checkConnection() {
|
||||
try {
|
||||
yield put(setOnline(false));
|
||||
yield put(setConnected(false));
|
||||
const internetConnection: NetInfoState = yield call(fetch);
|
||||
if (!internetConnection.isConnected) {
|
||||
console.error("No internet connection");
|
||||
return;
|
||||
}
|
||||
yield put(setOnline(true));
|
||||
console.log("Internet connection available");
|
||||
const response: AxiosResponse = yield call(axios.get, `http://${SERVER_IP}:${SERVER_PORT}/`, { timeout: 5000, validateStatus: () => true });
|
||||
if (!(response.status === 200)) {
|
||||
console.error("Connection to server failed");
|
||||
return;
|
||||
}
|
||||
console.log("Connection to server successful");
|
||||
yield put(setConnected(true));
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
yield put(setErrorStatus({ message: "There has been a Network Error", status: true }));
|
||||
}
|
||||
}
|
||||
|
||||
export function webSocketChannel() {
|
||||
const socket = getSocket();
|
||||
return eventChannel((emit) => {
|
||||
console.log("Websocket saga started");
|
||||
socket.onopen = () => {
|
||||
console.log("Connected to WebSocket server!");
|
||||
emit({ type: "connect" });
|
||||
};
|
||||
|
||||
socket.onmessage = (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log("Received message:", data);
|
||||
emit({ type: data.event, payload: data.data });
|
||||
}
|
||||
|
||||
socket.onclose = () => {
|
||||
console.log("Disconnected from server.");
|
||||
emit({ type: "disconnect" });
|
||||
};
|
||||
|
||||
return () => {
|
||||
socket.close();
|
||||
};
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
export function* webSocketSaga() {
|
||||
const channel: EventChannel<PayloadAction> = yield call(webSocketChannel);
|
||||
const socket: ReconnectingWebSocket = yield call(getSocket);
|
||||
socket.reconnect();
|
||||
while (true) {
|
||||
const action: PayloadAction<Drug | { id: number } | void> = yield take(channel);
|
||||
console.log(action);
|
||||
const db: SQLiteDatabase = yield call(getDBConnection);
|
||||
switch (action.type) {
|
||||
|
||||
case "connect":
|
||||
console.log("Connected to server via WebSocket");
|
||||
yield put({ type: "status/checkConnection" });
|
||||
const tempData: TempDrug[] = yield call(getTempDrugs, db);
|
||||
yield call(() => syncOfflineData(tempData));
|
||||
yield call(() => fetchDrugsServer());
|
||||
yield call(deleteTempDrug, db);
|
||||
break;
|
||||
case "itemAdded":
|
||||
yield call(() => insertDrug(db, action.payload as Drug));
|
||||
yield put({ type: "drug/addDrug", payload: action.payload as Drug });
|
||||
break;
|
||||
case "itemUpdated":
|
||||
yield call(() => editDrug(db, action.payload as Drug));
|
||||
yield put({ type: "drug/updateDrug", payload: action.payload as Drug });
|
||||
break;
|
||||
case "itemDeleted":
|
||||
yield call(() => deleteDrug(db, (action.payload as { id: number }).id));
|
||||
yield put({ type: "drug/removeDrug", payload: (action.payload as { id: number }).id });
|
||||
break;
|
||||
case "disconnect":
|
||||
console.log("Disconnected from server. Reconnecting...");
|
||||
yield put({ type: "status/checkConnection" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"name": "ma-nonnative",
|
||||
"main": "expo-router/entry",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"reset-project": "node ./scripts/reset-project.js",
|
||||
"android": "expo run:android",
|
||||
"ios": "expo run:ios",
|
||||
"web": "expo start --web",
|
||||
"test": "jest --watchAll",
|
||||
"lint": "expo lint"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-expo"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@pchmn/expo-material3-theme": "^1.3.2",
|
||||
"@react-native-community/netinfo": "^11.4.1",
|
||||
"@react-navigation/bottom-tabs": "^7.0.0",
|
||||
"@react-navigation/native": "^7.0.0",
|
||||
"@reduxjs/toolkit": "^2.3.0",
|
||||
"axios": "^1.7.9",
|
||||
"expo": "~52.0.11",
|
||||
"expo-blur": "~14.0.1",
|
||||
"expo-build-properties": "^0.13.2",
|
||||
"expo-constants": "~17.0.3",
|
||||
"expo-dev-client": "~5.0.6",
|
||||
"expo-font": "~13.0.1",
|
||||
"expo-haptics": "~14.0.0",
|
||||
"expo-linking": "~7.0.3",
|
||||
"expo-router": "~4.0.9",
|
||||
"expo-splash-screen": "~0.29.13",
|
||||
"expo-sqlite": "~15.0.3",
|
||||
"expo-status-bar": "~2.0.0",
|
||||
"expo-symbols": "~0.2.0",
|
||||
"expo-system-ui": "~4.0.4",
|
||||
"expo-web-browser": "~14.0.1",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-native": "0.76.3",
|
||||
"react-native-gesture-handler": "~2.20.2",
|
||||
"react-native-paper": "^5.12.5",
|
||||
"react-native-safe-area-context": "4.12.0",
|
||||
"react-native-screens": "^4.0.0",
|
||||
"react-native-web": "~0.19.13",
|
||||
"react-native-webview": "13.12.2",
|
||||
"react-redux": "^9.1.2",
|
||||
"reconnecting-websocket": "^4.4.0",
|
||||
"redux-saga": "^1.3.0",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"ws": "^8.18.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.25.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "~18.3.12",
|
||||
"@types/react-native-sqlite-storage": "^6.0.5",
|
||||
"@types/react-test-renderer": "^18.3.0",
|
||||
"@types/ws": "^8.5.14",
|
||||
"jest": "^29.2.1",
|
||||
"jest-expo": "~52.0.2",
|
||||
"react-test-renderer": "18.3.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"private": true,
|
||||
"expo": {
|
||||
"doctor": {
|
||||
"reactNativeDirectoryCheck": {
|
||||
"exclude": [
|
||||
"@pchmn/expo-material3-theme",
|
||||
"@reduxjs/toolkit",
|
||||
"react-redux"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
export type Drug = {
|
||||
id: number;
|
||||
name: string;
|
||||
category: string;
|
||||
price: number;
|
||||
numberOfUnits: number;
|
||||
manufacturer: string;
|
||||
}
|
||||
|
||||
export type DrugState = {
|
||||
drugList: Drug[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Drug, DrugState } from "./drug";
|
||||
import { get } from "react-native/Libraries/TurboModule/TurboModuleRegistry";
|
||||
import { initDB, insertDrug } from "@/database/localdb";
|
||||
|
||||
|
||||
const initialState: DrugState = {
|
||||
drugList: [],
|
||||
};
|
||||
|
||||
|
||||
export const drugSlice = createSlice({
|
||||
name: "drug",
|
||||
initialState,
|
||||
reducers: {
|
||||
addDrug: (state, action: PayloadAction<Drug>) => {
|
||||
state.drugList.push(action.payload);
|
||||
},
|
||||
removeDrug: (state, action: PayloadAction<number>) => {
|
||||
console.log("Removing drug with id: ", action.payload);
|
||||
state.drugList = state.drugList.filter(drug => drug.id !== action.payload);
|
||||
},
|
||||
updateDrug: (state, action: PayloadAction<Drug>) => {
|
||||
console.log("Updating drug with id: ", action.payload.id);
|
||||
console.log(action.payload);
|
||||
const index = state.drugList.findIndex(drug => drug.id === action.payload.id);
|
||||
if (index !== -1) {
|
||||
state.drugList[index] = action.payload;
|
||||
}
|
||||
console.log(state.drugList);
|
||||
},
|
||||
fetchDrugs: (state, action: PayloadAction<Drug[]>) => {
|
||||
state.drugList = action.payload;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const { addDrug, removeDrug, updateDrug, fetchDrugs } = drugSlice.actions
|
||||
export default drugSlice.reducer
|
||||
@@ -0,0 +1,4 @@
|
||||
export type ErrorStatus = {
|
||||
status: boolean,
|
||||
message: string
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { ErrorStatus } from "./errorStatus";
|
||||
|
||||
|
||||
export const errorStatusSlice = createSlice({
|
||||
name: "status",
|
||||
initialState: { status: false, message: "" } as ErrorStatus,
|
||||
reducers: {
|
||||
setErrorStatus: (state, action: PayloadAction<ErrorStatus>) => {
|
||||
state.status = action.payload.status;
|
||||
state.message = action.payload.message;
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
export const { setErrorStatus } = errorStatusSlice.actions
|
||||
export default errorStatusSlice.reducer
|
||||
@@ -0,0 +1,5 @@
|
||||
export type Status = {
|
||||
isOnline: boolean;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
|
||||
import { Status } from "./status";
|
||||
|
||||
|
||||
export const statusSlice = createSlice({
|
||||
name: "status",
|
||||
initialState: { isOnline: false, isConnected: false } as Status,
|
||||
reducers: {
|
||||
setOnline: (state, action: PayloadAction<boolean>) => {
|
||||
state.isOnline = action.payload;
|
||||
},
|
||||
setConnected: (state, action: PayloadAction<boolean>) => {
|
||||
state.isConnected = action.payload;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const { setOnline, setConnected } = statusSlice.actions
|
||||
export default statusSlice.reducer
|
||||
@@ -0,0 +1,23 @@
|
||||
import { configureStore } from "@reduxjs/toolkit";
|
||||
import { drugSlice } from "./features/drug/drugSlice";
|
||||
import createSagaMiddleware from "redux-saga";
|
||||
import { rootSaga } from "@/middleware/rootSaga";
|
||||
import { statusSlice } from "./features/status/statusSlice";
|
||||
import { errorStatusSlice } from "./features/errorstatus/errorStatusSlice";
|
||||
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: { drug: drugSlice.reducer, status: statusSlice.reducer, errorStatus: errorStatusSlice.reducer },
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().concat([sagaMiddleware])
|
||||
});
|
||||
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
|
||||
export type RootState = ReturnType<typeof store.getState>;
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type AppStore = typeof store;
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "expo/tsconfig.base",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".expo/types/**/*.ts",
|
||||
"expo-env.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -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)
|
||||
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 25 KiB |
@@ -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) { }
|
||||
|
||||
|
||||