diff --git a/.vscode/settings.json b/.vscode/settings.json index e0f15db..be4fbc2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,3 +1,4 @@ { - "java.configuration.updateBuildConfiguration": "automatic" + "java.configuration.updateBuildConfiguration": "automatic", + "cmake.sourceDirectory": "E:/JOKI/smartinfuse/linux" } \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ddad42f..c88f80a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,14 @@ + + + + + + + + function configurationCacheProblems() { return ( // begin-report-data -{"diagnostics":[{"locations":[{"path":"E:\\JOKI\\smartinfuse\\android\\app\\build.gradle","line":11},{"path":"build file 'E:\\JOKI\\smartinfuse\\android\\build.gradle'","line":30}],"problem":[{"text":"Groovy DSL script compilation problem"}],"severity":"ERROR","contextualLabel":"Could not compile build file 'E:\\JOKI\\smartinfuse\\android\\app\\build.gradle'.","error":{"summary":[{"text":"at "},{"name":"build_djun6d34t187xj944t8h2kljy$_run_closure3.doCall$original(E:\\JOKI\\smartinfuse\\android\\build.gradle:30)"}],"parts":[{"text":"org.gradle.groovy.scripts.ScriptCompilationException: Could not compile build file 'E:\\JOKI\\smartinfuse\\android\\app\\build.gradle'.\r\n"},{"internalText":"\tat org.gradle.groovy.scripts.internal.DefaultScriptCompilationHandler.lambda$wrapCompilationFailure$0(DefaultScriptCompilationHandler.java:224)\r\n\tat org.gradle.api.problems.internal.DefaultProblemReporter.throwing(DefaultProblemReporter.java:69)\r\n\tat org.gradle.groovy.scripts.internal.DefaultScriptCompilationHandler.wrapCompilationFailure(DefaultScriptCompilationHandler.java:219)\r\n\tat org.gradle.groovy.scripts.internal.DefaultScriptCompilationHandler.compileScript(DefaultScriptCompilationHandler.java:162)\r\n\tat org.gradle.groovy.scripts.internal.DefaultScriptCompilationHandler.compileToDir(DefaultScriptCompilationHandler.java:112)\r\n\tat org.gradle.groovy.scripts.internal.BuildOperationBackedScriptCompilationHandler$2.run(BuildOperationBackedScriptCompilationHandler.java:54)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)\r\n\tat org.gradle.groovy.scripts.internal.BuildOperationBackedScriptCompilationHandler.compileToDir(BuildOperationBackedScriptCompilationHandler.java:51)\r\n\tat org.gradle.groovy.scripts.internal.GroovyScriptClassCompiler$GroovyScriptCompilationAndInstrumentation.compile(GroovyScriptClassCompiler.java:295)\r\n\tat org.gradle.internal.scripts.BuildScriptCompilationAndInstrumentation.execute(BuildScriptCompilationAndInstrumentation.java:136)\r\n\tat org.gradle.internal.execution.steps.ExecuteStep.executeInternal(ExecuteStep.java:105)\r\n\tat org.gradle.internal.execution.steps.ExecuteStep.access$000(ExecuteStep.java:44)\r\n\tat org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:59)\r\n\tat org.gradle.internal.execution.steps.ExecuteStep$1.call(ExecuteStep.java:56)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)\r\n\tat org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:56)\r\n\tat org.gradle.internal.execution.steps.ExecuteStep.execute(ExecuteStep.java:44)\r\n\tat org.gradle.internal.execution.steps.CancelExecutionStep.execute(CancelExecutionStep.java:42)\r\n\tat org.gradle.internal.execution.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:75)\r\n\tat org.gradle.internal.execution.steps.TimeoutStep.execute(TimeoutStep.java:55)\r\n\tat org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:50)\r\n\tat org.gradle.internal.execution.steps.PreCreateOutputParentsStep.execute(PreCreateOutputParentsStep.java:28)\r\n\tat org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:61)\r\n\tat org.gradle.internal.execution.steps.BroadcastChangingOutputsStep.execute(BroadcastChangingOutputsStep.java:26)\r\n\tat org.gradle.internal.execution.steps.NoInputChangesStep.execute(NoInputChangesStep.java:30)\r\n\tat org.gradle.internal.execution.steps.NoInputChangesStep.execute(NoInputChangesStep.java:21)\r\n\tat org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:69)\r\n\tat org.gradle.internal.execution.steps.CaptureOutputsAfterExecutionStep.execute(CaptureOutputsAfterExecutionStep.java:46)\r\n\tat org.gradle.internal.execution.steps.BuildCacheStep.executeWithoutCache(BuildCacheStep.java:189)\r\n\tat org.gradle.internal.execution.steps.BuildCacheStep.lambda$execute$1(BuildCacheStep.java:75)\r\n\tat org.gradle.internal.Either$Right.fold(Either.java:175)\r\n\tat org.gradle.internal.execution.caching.CachingState.fold(CachingState.java:62)\r\n\tat org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:73)\r\n\tat org.gradle.internal.execution.steps.BuildCacheStep.execute(BuildCacheStep.java:48)\r\n\tat org.gradle.internal.execution.steps.NeverUpToDateStep.execute(NeverUpToDateStep.java:34)\r\n\tat org.gradle.internal.execution.steps.NeverUpToDateStep.execute(NeverUpToDateStep.java:22)\r\n\tat org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:37)\r\n\tat org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsFinishedStep.execute(MarkSnapshottingInputsFinishedStep.java:27)\r\n\tat org.gradle.internal.execution.steps.ResolveNonIncrementalCachingStateStep.executeDelegate(ResolveNonIncrementalCachingStateStep.java:50)\r\n\tat org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:71)\r\n\tat org.gradle.internal.execution.steps.AbstractResolveCachingStateStep.execute(AbstractResolveCachingStateStep.java:39)\r\n\tat org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:107)\r\n\tat org.gradle.internal.execution.steps.ValidateStep.execute(ValidateStep.java:56)\r\n\tat org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:64)\r\n\tat org.gradle.internal.execution.steps.AbstractCaptureStateBeforeExecutionStep.execute(AbstractCaptureStateBeforeExecutionStep.java:43)\r\n\tat org.gradle.internal.execution.steps.legacy.MarkSnapshottingInputsStartedStep.execute(MarkSnapshottingInputsStartedStep.java:38)\r\n\tat org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.lambda$executeInTemporaryWorkspace$3(AssignImmutableWorkspaceStep.java:209)\r\n\tat org.gradle.internal.execution.workspace.impl.CacheBasedImmutableWorkspaceProvider$1.withTemporaryWorkspace(CacheBasedImmutableWorkspaceProvider.java:116)\r\n\tat org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.executeInTemporaryWorkspace(AssignImmutableWorkspaceStep.java:199)\r\n\tat org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.lambda$execute$0(AssignImmutableWorkspaceStep.java:121)\r\n\tat org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.execute(AssignImmutableWorkspaceStep.java:121)\r\n\tat org.gradle.internal.execution.steps.AssignImmutableWorkspaceStep.execute(AssignImmutableWorkspaceStep.java:90)\r\n\tat org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:38)\r\n\tat org.gradle.internal.execution.steps.ChoosePipelineStep.execute(ChoosePipelineStep.java:23)\r\n\tat org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.lambda$execute$2(ExecuteWorkBuildOperationFiringStep.java:67)\r\n\tat org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:67)\r\n\tat org.gradle.internal.execution.steps.ExecuteWorkBuildOperationFiringStep.execute(ExecuteWorkBuildOperationFiringStep.java:39)\r\n\tat org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:46)\r\n\tat org.gradle.internal.execution.steps.IdentityCacheStep.execute(IdentityCacheStep.java:34)\r\n\tat org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:48)\r\n\tat org.gradle.internal.execution.steps.IdentifyStep.execute(IdentifyStep.java:35)\r\n\tat org.gradle.internal.execution.impl.DefaultExecutionEngine$1.execute(DefaultExecutionEngine.java:61)\r\n\tat org.gradle.groovy.scripts.internal.GroovyScriptClassCompiler.doCompile(GroovyScriptClassCompiler.java:167)\r\n\tat org.gradle.groovy.scripts.internal.GroovyScriptClassCompiler.compile(GroovyScriptClassCompiler.java:129)\r\n\tat org.gradle.groovy.scripts.internal.CrossBuildInMemoryCachingScriptClassCache.getOrCompile(CrossBuildInMemoryCachingScriptClassCache.java:53)\r\n\tat org.gradle.groovy.scripts.internal.BuildScopeInMemoryCachingScriptClassCompiler.compile(BuildScopeInMemoryCachingScriptClassCompiler.java:50)\r\n\tat org.gradle.groovy.scripts.DefaultScriptCompilerFactory$ScriptCompilerImpl.compile(DefaultScriptCompilerFactory.java:49)\r\n\tat org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:131)\r\n\tat org.gradle.configuration.BuildOperationScriptPlugin$1.run(BuildOperationScriptPlugin.java:68)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)\r\n\tat org.gradle.configuration.BuildOperationScriptPlugin.lambda$apply$0(BuildOperationScriptPlugin.java:65)\r\n\tat org.gradle.internal.code.DefaultUserCodeApplicationContext.apply(DefaultUserCodeApplicationContext.java:44)\r\n\tat org.gradle.configuration.BuildOperationScriptPlugin.apply(BuildOperationScriptPlugin.java:65)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:411)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:429)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:410)\r\n\tat org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:46)\r\n\tat org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:27)\r\n\tat org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:35)\r\n\tat org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.lambda$run$0(LifecycleProjectEvaluator.java:109)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:411)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:429)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:410)\r\n\tat org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.run(LifecycleProjectEvaluator.java:100)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)\r\n\tat org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:72)\r\n\tat org.gradle.api.internal.project.DefaultProject.evaluateUnchecked(DefaultProject.java:828)\r\n\tat org.gradle.api.internal.project.ProjectLifecycleController.lambda$ensureSelfConfigured$2(ProjectLifecycleController.java:85)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255)\r\n\tat org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266)\r\n\tat org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$maybeTransitionIfNotCurrentlyTransitioning$10(StateTransitionController.java:199)\r\n\tat org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:36)\r\n\tat org.gradle.internal.model.StateTransitionController.maybeTransitionIfNotCurrentlyTransitioning(StateTransitionController.java:195)\r\n\tat org.gradle.api.internal.project.ProjectLifecycleController.ensureSelfConfigured(ProjectLifecycleController.java:85)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.ensureConfigured(DefaultProjectStateRegistry.java:385)\r\n\tat org.gradle.api.internal.project.DefaultProject.evaluationDependsOn(DefaultProject.java:914)\r\n\tat org.gradle.api.internal.project.DefaultProject.evaluationDependsOn(DefaultProject.java:905)\r\n\tat org.gradle.api.Project$evaluationDependsOn$0.call(Unknown Source)\r\n"},{"text":"\tat build_djun6d34t187xj944t8h2kljy$_run_closure3.doCall$original(E:\\JOKI\\smartinfuse\\android\\build.gradle:30)\r\n\tat build_djun6d34t187xj944t8h2kljy$_run_closure3.doCall(E:\\JOKI\\smartinfuse\\android\\build.gradle)\r\n"},{"internalText":"\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\r\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n\tat org.gradle.util.internal.ClosureBackedAction.execute(ClosureBackedAction.java:73)\r\n\tat org.gradle.util.internal.ConfigureUtil.configureTarget(ConfigureUtil.java:166)\r\n\tat org.gradle.util.internal.ConfigureUtil.configure(ConfigureUtil.java:107)\r\n\tat org.gradle.util.internal.ConfigureUtil$WrappedConfigureAction.execute(ConfigureUtil.java:178)\r\n\tat org.gradle.api.internal.DefaultMutationGuard$1.execute(DefaultMutationGuard.java:66)\r\n\tat org.gradle.internal.Actions.with(Actions.java:206)\r\n\tat org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator$1.run(BuildOperationCrossProjectConfigurator.java:68)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)\r\n\tat org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator.lambda$runProjectConfigureAction$0(BuildOperationCrossProjectConfigurator.java:65)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:411)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:429)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:410)\r\n\tat org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator.runProjectConfigureAction(BuildOperationCrossProjectConfigurator.java:65)\r\n\tat org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator.access$100(BuildOperationCrossProjectConfigurator.java:31)\r\n\tat org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator$BlockConfigureBuildOperation.run(BuildOperationCrossProjectConfigurator.java:106)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)\r\n\tat org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator.runBlockConfigureAction(BuildOperationCrossProjectConfigurator.java:61)\r\n\tat org.gradle.api.internal.project.BuildOperationCrossProjectConfigurator.subprojects(BuildOperationCrossProjectConfigurator.java:47)\r\n\tat org.gradle.api.internal.project.DefaultProject.subprojects(DefaultProject.java:781)\r\n\tat org.gradle.api.internal.project.DefaultProject.subprojects(DefaultProject.java:771)\r\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\r\n\tat java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\r\n\tat java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\r\n\tat org.gradle.internal.metaobject.BeanDynamicObject$MetaClassAdapter.invokeMethod(BeanDynamicObject.java:547)\r\n\tat org.gradle.internal.metaobject.BeanDynamicObject.tryInvokeMethod(BeanDynamicObject.java:218)\r\n\tat org.gradle.api.internal.project.MutableStateAccessAwareDynamicObject.tryInvokeMethod(MutableStateAccessAwareDynamicObject.java:111)\r\n\tat org.gradle.internal.metaobject.CompositeDynamicObject.tryInvokeMethod(CompositeDynamicObject.java:99)\r\n\tat org.gradle.internal.extensibility.MixInClosurePropertiesAsMethodsDynamicObject.tryInvokeMethod(MixInClosurePropertiesAsMethodsDynamicObject.java:38)\r\n\tat org.gradle.groovy.scripts.BasicScript$ScriptDynamicObject.tryInvokeMethod(BasicScript.java:138)\r\n\tat org.gradle.internal.metaobject.AbstractDynamicObject.invokeMethod(AbstractDynamicObject.java:163)\r\n\tat org.gradle.api.internal.project.DefaultDynamicLookupRoutine.invokeMethod(DefaultDynamicLookupRoutine.java:58)\r\n\tat org.gradle.groovy.scripts.BasicScript.invokeMethod(BasicScript.java:87)\r\n"},{"text":"\tat build_djun6d34t187xj944t8h2kljy.run(E:\\JOKI\\smartinfuse\\android\\build.gradle:29)\r\n"},{"internalText":"\tat org.gradle.groovy.scripts.internal.DefaultScriptRunnerFactory$ScriptRunnerImpl.run(DefaultScriptRunnerFactory.java:91)\r\n\tat org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.lambda$apply$1(DefaultScriptPluginFactory.java:141)\r\n\tat org.gradle.configuration.ProjectScriptTarget.addConfiguration(ProjectScriptTarget.java:79)\r\n\tat org.gradle.configuration.DefaultScriptPluginFactory$ScriptPluginImpl.apply(DefaultScriptPluginFactory.java:144)\r\n\tat org.gradle.configuration.BuildOperationScriptPlugin$1.run(BuildOperationScriptPlugin.java:68)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)\r\n\tat org.gradle.configuration.BuildOperationScriptPlugin.lambda$apply$0(BuildOperationScriptPlugin.java:65)\r\n\tat org.gradle.internal.code.DefaultUserCodeApplicationContext.apply(DefaultUserCodeApplicationContext.java:44)\r\n\tat org.gradle.configuration.BuildOperationScriptPlugin.apply(BuildOperationScriptPlugin.java:65)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:411)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:429)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:410)\r\n\tat org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:46)\r\n\tat org.gradle.configuration.project.BuildScriptProcessor.execute(BuildScriptProcessor.java:27)\r\n\tat org.gradle.configuration.project.ConfigureActionsProjectEvaluator.evaluate(ConfigureActionsProjectEvaluator.java:35)\r\n\tat org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.lambda$run$0(LifecycleProjectEvaluator.java:109)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$applyToMutableState$1(DefaultProjectStateRegistry.java:411)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.lambda$fromMutableState$2(DefaultProjectStateRegistry.java:434)\r\n\tat org.gradle.internal.work.DefaultWorkerLeaseService.withReplacedLocks(DefaultWorkerLeaseService.java:359)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.fromMutableState(DefaultProjectStateRegistry.java:434)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.applyToMutableState(DefaultProjectStateRegistry.java:410)\r\n\tat org.gradle.configuration.project.LifecycleProjectEvaluator$EvaluateProject.run(LifecycleProjectEvaluator.java:100)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)\r\n\tat org.gradle.configuration.project.LifecycleProjectEvaluator.evaluate(LifecycleProjectEvaluator.java:72)\r\n\tat org.gradle.api.internal.project.DefaultProject.evaluateUnchecked(DefaultProject.java:828)\r\n\tat org.gradle.api.internal.project.ProjectLifecycleController.lambda$ensureSelfConfigured$2(ProjectLifecycleController.java:85)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255)\r\n\tat org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266)\r\n\tat org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$maybeTransitionIfNotCurrentlyTransitioning$10(StateTransitionController.java:199)\r\n\tat org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:36)\r\n\tat org.gradle.internal.model.StateTransitionController.maybeTransitionIfNotCurrentlyTransitioning(StateTransitionController.java:195)\r\n\tat org.gradle.api.internal.project.ProjectLifecycleController.ensureSelfConfigured(ProjectLifecycleController.java:85)\r\n\tat org.gradle.api.internal.project.DefaultProjectStateRegistry$ProjectStateImpl.ensureConfigured(DefaultProjectStateRegistry.java:385)\r\n\tat org.gradle.execution.TaskPathProjectEvaluator.configure(TaskPathProjectEvaluator.java:42)\r\n\tat org.gradle.execution.TaskPathProjectEvaluator.configureHierarchy(TaskPathProjectEvaluator.java:56)\r\n\tat org.gradle.configuration.DefaultProjectsPreparer.prepareProjects(DefaultProjectsPreparer.java:50)\r\n\tat org.gradle.configuration.BuildTreePreparingProjectsPreparer.prepareProjects(BuildTreePreparingProjectsPreparer.java:65)\r\n\tat org.gradle.configuration.BuildOperationFiringProjectsPreparer$ConfigureBuild.run(BuildOperationFiringProjectsPreparer.java:52)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:29)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$1.execute(DefaultBuildOperationRunner.java:26)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.run(DefaultBuildOperationRunner.java:47)\r\n\tat org.gradle.configuration.BuildOperationFiringProjectsPreparer.prepareProjects(BuildOperationFiringProjectsPreparer.java:40)\r\n\tat org.gradle.initialization.VintageBuildModelController.lambda$prepareProjects$2(VintageBuildModelController.java:84)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$doTransition$14(StateTransitionController.java:255)\r\n\tat org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266)\r\n\tat org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:254)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$transitionIfNotPreviously$11(StateTransitionController.java:213)\r\n\tat org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:36)\r\n\tat org.gradle.internal.model.StateTransitionController.transitionIfNotPreviously(StateTransitionController.java:209)\r\n\tat org.gradle.initialization.VintageBuildModelController.prepareProjects(VintageBuildModelController.java:84)\r\n\tat org.gradle.initialization.VintageBuildModelController.getConfiguredModel(VintageBuildModelController.java:64)\r\n\tat org.gradle.internal.build.DefaultBuildLifecycleController.lambda$withProjectsConfigured$1(DefaultBuildLifecycleController.java:133)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$notInState$3(StateTransitionController.java:132)\r\n\tat org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:46)\r\n\tat org.gradle.internal.model.StateTransitionController.notInState(StateTransitionController.java:128)\r\n\tat org.gradle.internal.build.DefaultBuildLifecycleController.withProjectsConfigured(DefaultBuildLifecycleController.java:133)\r\n\tat org.gradle.internal.build.DefaultBuildToolingModelController.locateBuilderForTarget(DefaultBuildToolingModelController.java:58)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeModelCreator$DefaultBuildTreeModelController.lambda$locateBuilderForTarget$0(DefaultBuildTreeModelCreator.java:64)\r\n\tat org.gradle.internal.build.DefaultBuildLifecycleController.withToolingModels(DefaultBuildLifecycleController.java:327)\r\n\tat org.gradle.internal.build.AbstractBuildState.withToolingModels(AbstractBuildState.java:160)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeModelCreator$DefaultBuildTreeModelController.locateBuilderForTarget(DefaultBuildTreeModelCreator.java:64)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeModelCreator$DefaultBuildTreeModelController.locateBuilderForDefaultTarget(DefaultBuildTreeModelCreator.java:59)\r\n\tat org.gradle.tooling.internal.provider.runner.BuildModelActionRunner$ModelCreateAction.fromBuildModel(BuildModelActionRunner.java:81)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeModelCreator.fromBuildModel(DefaultBuildTreeModelCreator.java:48)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.lambda$fromBuildModel$2(DefaultBuildTreeLifecycleController.java:90)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.lambda$runBuild$4(DefaultBuildTreeLifecycleController.java:120)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$transition$6(StateTransitionController.java:169)\r\n\tat org.gradle.internal.model.StateTransitionController.doTransition(StateTransitionController.java:266)\r\n\tat org.gradle.internal.model.StateTransitionController.lambda$transition$7(StateTransitionController.java:169)\r\n\tat org.gradle.internal.work.DefaultSynchronizer.withLock(DefaultSynchronizer.java:46)\r\n\tat org.gradle.internal.model.StateTransitionController.transition(StateTransitionController.java:169)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.runBuild(DefaultBuildTreeLifecycleController.java:117)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeLifecycleController.fromBuildModel(DefaultBuildTreeLifecycleController.java:82)\r\n\tat org.gradle.tooling.internal.provider.runner.BuildModelActionRunner.run(BuildModelActionRunner.java:49)\r\n\tat org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)\r\n\tat org.gradle.internal.buildtree.ProblemReportingBuildActionRunner.run(ProblemReportingBuildActionRunner.java:49)\r\n\tat org.gradle.launcher.exec.BuildOutcomeReportingBuildActionRunner.run(BuildOutcomeReportingBuildActionRunner.java:71)\r\n\tat org.gradle.tooling.internal.provider.FileSystemWatchingBuildActionRunner.run(FileSystemWatchingBuildActionRunner.java:135)\r\n\tat org.gradle.launcher.exec.BuildCompletionNotifyingBuildActionRunner.run(BuildCompletionNotifyingBuildActionRunner.java:41)\r\n\tat org.gradle.launcher.exec.RootBuildLifecycleBuildActionExecutor.lambda$execute$0(RootBuildLifecycleBuildActionExecutor.java:54)\r\n\tat org.gradle.composite.internal.DefaultRootBuildState.run(DefaultRootBuildState.java:130)\r\n\tat org.gradle.launcher.exec.RootBuildLifecycleBuildActionExecutor.execute(RootBuildLifecycleBuildActionExecutor.java:54)\r\n\tat org.gradle.internal.buildtree.InitDeprecationLoggingActionExecutor.execute(InitDeprecationLoggingActionExecutor.java:62)\r\n\tat org.gradle.internal.buildtree.InitProblems.execute(InitProblems.java:36)\r\n\tat org.gradle.internal.buildtree.DefaultBuildTreeContext.execute(DefaultBuildTreeContext.java:40)\r\n\tat org.gradle.launcher.exec.BuildTreeLifecycleBuildActionExecutor.lambda$execute$0(BuildTreeLifecycleBuildActionExecutor.java:71)\r\n\tat org.gradle.internal.buildtree.BuildTreeState.run(BuildTreeState.java:60)\r\n\tat org.gradle.launcher.exec.BuildTreeLifecycleBuildActionExecutor.execute(BuildTreeLifecycleBuildActionExecutor.java:71)\r\n\tat org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor$2.call(RunAsBuildOperationBuildActionExecutor.java:67)\r\n\tat org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor$2.call(RunAsBuildOperationBuildActionExecutor.java:63)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:209)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$CallableBuildOperationWorker.execute(DefaultBuildOperationRunner.java:204)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:66)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner$2.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:166)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.execute(DefaultBuildOperationRunner.java:59)\r\n\tat org.gradle.internal.operations.DefaultBuildOperationRunner.call(DefaultBuildOperationRunner.java:53)\r\n\tat org.gradle.launcher.exec.RunAsBuildOperationBuildActionExecutor.execute(RunAsBuildOperationBuildActionExecutor.java:63)\r\n\tat org.gradle.launcher.exec.RunAsWorkerThreadBuildActionExecutor.lambda$execute$0(RunAsWorkerThreadBuildActionExecutor.java:36)\r\n\tat org.gradle.internal.work.DefaultWorkerLeaseService.withLocks(DefaultWorkerLeaseService.java:263)\r\n\tat org.gradle.internal.work.DefaultWorkerLeaseService.runAsWorkerThread(DefaultWorkerLeaseService.java:127)\r\n\tat org.gradle.launcher.exec.RunAsWorkerThreadBuildActionExecutor.execute(RunAsWorkerThreadBuildActionExecutor.java:36)\r\n\tat org.gradle.tooling.internal.provider.continuous.ContinuousBuildActionExecutor.execute(ContinuousBuildActionExecutor.java:110)\r\n\tat org.gradle.tooling.internal.provider.SubscribableBuildActionExecutor.execute(SubscribableBuildActionExecutor.java:64)\r\n\tat org.gradle.internal.session.DefaultBuildSessionContext.execute(DefaultBuildSessionContext.java:46)\r\n\tat org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor$ActionImpl.apply(BuildSessionLifecycleBuildActionExecutor.java:92)\r\n\tat org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor$ActionImpl.apply(BuildSessionLifecycleBuildActionExecutor.java:80)\r\n\tat org.gradle.internal.session.BuildSessionState.run(BuildSessionState.java:71)\r\n\tat org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor.execute(BuildSessionLifecycleBuildActionExecutor.java:62)\r\n\tat org.gradle.internal.buildprocess.execution.BuildSessionLifecycleBuildActionExecutor.execute(BuildSessionLifecycleBuildActionExecutor.java:41)\r\n\tat org.gradle.internal.buildprocess.execution.StartParamsValidatingActionExecutor.execute(StartParamsValidatingActionExecutor.java:64)\r\n\tat org.gradle.internal.buildprocess.execution.StartParamsValidatingActionExecutor.execute(StartParamsValidatingActionExecutor.java:32)\r\n\tat org.gradle.internal.buildprocess.execution.SessionFailureReportingActionExecutor.execute(SessionFailureReportingActionExecutor.java:51)\r\n\tat org.gradle.internal.buildprocess.execution.SessionFailureReportingActionExecutor.execute(SessionFailureReportingActionExecutor.java:39)\r\n\tat org.gradle.internal.buildprocess.execution.SetupLoggingActionExecutor.execute(SetupLoggingActionExecutor.java:47)\r\n\tat org.gradle.internal.buildprocess.execution.SetupLoggingActionExecutor.execute(SetupLoggingActionExecutor.java:31)\r\n\tat org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:70)\r\n\tat org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)\r\n\tat org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)\r\n\tat org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:39)\r\n\tat org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)\r\n\tat org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:29)\r\n\tat org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)\r\n\tat org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:35)\r\n\tat org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)\r\n\tat org.gradle.launcher.daemon.server.exec.ForwardClientInput.lambda$execute$0(ForwardClientInput.java:40)\r\n\tat org.gradle.internal.daemon.clientinput.ClientInputForwarder.forwardInput(ClientInputForwarder.java:80)\r\n\tat org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:37)\r\n\tat org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)\r\n\tat org.gradle.launcher.daemon.server.exec.LogAndCheckHealth.execute(LogAndCheckHealth.java:64)\r\n\tat org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)\r\n\tat org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:63)\r\n\tat org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)\r\n\tat org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)\r\n\tat org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:84)\r\n\tat org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:37)\r\n\tat org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:104)\r\n\tat org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:52)\r\n\tat org.gradle.launcher.daemon.server.DaemonStateCoordinator.lambda$runCommand$0(DaemonStateCoordinator.java:321)\r\n\tat org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:64)\r\n\tat org.gradle.internal.concurrent.AbstractManagedExecutor$1.run(AbstractManagedExecutor.java:48)\r\n"},{"text":"Caused by: org.codehaus.groovy.control.MultipleCompilationErrorsException: startup failed:\r\nbuild file 'E:\\JOKI\\smartinfuse\\android\\app\\build.gradle': 11: unable to resolve class val \n @ line 11, column 5.\r\n val keystoreProperties = Properties()\r\n ^\r\n\r\nbuild file 'E:\\JOKI\\smartinfuse\\android\\app\\build.gradle': 12: unable to resolve class val \n @ line 12, column 5.\r\n val keystorePropertiesFile = rootProject.file(\"key.properties\")\r\n ^\r\n\r\n2 errors\r\n\r\n"},{"internalText":"\tat org.gradle.groovy.scripts.internal.DefaultScriptCompilationHandler.compileScript(DefaultScriptCompilationHandler.java:160)\r\n\t... 327 more\r\n"}]},"problemId":[{"name":"groovy-dsl","displayName":"Groovy DSL script compilation"},{"name":"compilation","displayName":"Compilation"},{"name":"compilation-failed","displayName":"Groovy DSL script compilation problem"}]}],"problemsReport":{"totalProblemCount":1,"buildName":"android","requestedTasks":"","documentationLink":"https://docs.gradle.org/8.12/userguide/problems-report.html","documentationLinkCaption":"Problem report","summaries":[]}} +{"diagnostics":[{"locations":[{"path":"build file 'C:\\Users\\rahma\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\awesome_notifications-0.10.1\\android\\build.gradle'","line":8},{"path":"build file 'C:\\Users\\rahma\\AppData\\Local\\Pub\\Cache\\hosted\\pub.dev\\awesome_notifications-0.10.1\\android\\build.gradle'","line":8}],"problem":[{"text":"Space-assignment syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10.0."}],"contextualLabel":"Space-assignment syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/8.12/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"space-assignment-syntax-in-groovy-dsl","displayName":"Space-assignment syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('url = ') instead."}]]}],"problemsReport":{"totalProblemCount":1,"buildName":"android","requestedTasks":"assembleDebug","documentationLink":"https://docs.gradle.org/8.12/userguide/problems-report.html","documentationLinkCaption":"Problem report","summaries":[]}} // end-report-data );} diff --git a/lib/app/models/chart_data_point.dart b/lib/app/models/chart_data_point.dart index 27d0e52..8a885aa 100644 --- a/lib/app/models/chart_data_point.dart +++ b/lib/app/models/chart_data_point.dart @@ -1,14 +1,21 @@ class ChartDataPoint { - final int hour; + final DateTime timestamp; final double dropsPerMinute; ChartDataPoint({ - required this.hour, + required this.timestamp, required this.dropsPerMinute, }); + /// Label waktu format HH:mm, dipakai di sumbu X grafik & tooltip. + String get timeLabel { + final h = timestamp.hour.toString().padLeft(2, '0'); + final m = timestamp.minute.toString().padLeft(2, '0'); + return '$h:$m'; + } + @override String toString() { - return 'ChartDataPoint(hour: $hour, dropsPerMinute: $dropsPerMinute)'; + return 'ChartDataPoint(timestamp: $timestamp, dropsPerMinute: $dropsPerMinute)'; } } \ No newline at end of file diff --git a/lib/app/models/realtime_monitoring_model.dart b/lib/app/models/realtime_monitoring_model.dart index 452f7ca..4fcad42 100644 --- a/lib/app/models/realtime_monitoring_model.dart +++ b/lib/app/models/realtime_monitoring_model.dart @@ -1,5 +1,3 @@ -// lib/app/models/realtime_monitoring_model.dart - class RealtimeMonitoringModel { final String deviceId; final String roomId; @@ -10,6 +8,7 @@ class RealtimeMonitoringModel { final DateTime lastUpdate; final int servoAngleOpen; final int servoAngleClose; + final bool buzzerActive; // ← tambah ini RealtimeMonitoringModel({ required this.deviceId, @@ -21,9 +20,9 @@ class RealtimeMonitoringModel { required this.lastUpdate, this.servoAngleOpen = 90, this.servoAngleClose = 0, + this.buzzerActive = false, // ← tambah ini }); - // From Realtime Database factory RealtimeMonitoringModel.fromRealtimeDB( String deviceId, String roomId, @@ -37,6 +36,7 @@ class RealtimeMonitoringModel { deviceId: deviceId, roomId: roomId, servoOpen: controlling['servo_open'] ?? false, + buzzerActive: controlling['buzzer_active'] ?? false, // ← tambah ini lastCommand: DateTime.parse( controlling['last_command'] ?? DateTime.now().toIso8601String(), ), @@ -50,11 +50,11 @@ class RealtimeMonitoringModel { ); } - // To Realtime Database Map toRealtimeDB() { return { 'controlling': { 'servo_open': servoOpen, + 'buzzer_active': buzzerActive, // ← tambah ini 'last_command': lastCommand.toIso8601String(), }, 'monitoring': { @@ -73,6 +73,7 @@ class RealtimeMonitoringModel { String? deviceId, String? roomId, bool? servoOpen, + bool? buzzerActive, // ← tambah ini DateTime? lastCommand, String? deviceStatus, double? dropRate, @@ -84,6 +85,7 @@ class RealtimeMonitoringModel { deviceId: deviceId ?? this.deviceId, roomId: roomId ?? this.roomId, servoOpen: servoOpen ?? this.servoOpen, + buzzerActive: buzzerActive ?? this.buzzerActive, // ← tambah ini lastCommand: lastCommand ?? this.lastCommand, deviceStatus: deviceStatus ?? this.deviceStatus, dropRate: dropRate ?? this.dropRate, diff --git a/lib/app/modules/device-detail/controllers/device_detail_controller.dart b/lib/app/modules/device-detail/controllers/device_detail_controller.dart index 8c27953..08441d5 100644 --- a/lib/app/modules/device-detail/controllers/device_detail_controller.dart +++ b/lib/app/modules/device-detail/controllers/device_detail_controller.dart @@ -24,7 +24,7 @@ class DeviceDetailController extends GetxController { final isServoActive = false.obs; final deviceStatus = 'disconnected'.obs; final isLoading = true.obs; - + final isBuzzerActive = false.obs; final RxList chartData = [].obs; StreamSubscription? _realtimeSubscription; @@ -117,6 +117,7 @@ class DeviceDetailController extends GetxController { lastUpdate.value = data.lastUpdate; isServoActive.value = data.servoOpen; deviceStatus.value = data.deviceStatus; + isBuzzerActive.value = data.buzzerActive; } }, onError: (error) => AppSnackbar.error('Koneksi realtime terputus')); } @@ -140,15 +141,16 @@ class DeviceDetailController extends GetxController { } void _processHistoryData(List histories) { + // Ambil 10 data TERBARU histories.sort((a, b) => b.timestamp.compareTo(a.timestamp)); - final latestHistories = histories.take(10).toList(); + // Urutkan lagi dari lama -> baru supaya grafik terbaca kiri ke kanan latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp)); final List points = latestHistories.map((history) { return ChartDataPoint( - hour: history.timestamp.hour, + timestamp: history.timestamp, dropsPerMinute: history.dropPerMinute, ); }).toList(); @@ -186,7 +188,18 @@ class DeviceDetailController extends GetxController { ); } + Future turnOffBuzzer() async { + try { + final dbRoomId = roomId!.toLowerCase().replaceAll(' ', '_'); + await _realtimeService.controlBuzzer(dbRoomId, deviceId!, false); + isBuzzerActive.value = false; + AppSnackbar.success('Buzzer dimatikan'); + } catch (e) { + AppSnackbar.error('Gagal mematikan buzzer'); + } + } + Future refreshData() async { await _loadDeviceData(); } -} +} \ No newline at end of file diff --git a/lib/app/modules/device-detail/views/device_detail_view.dart b/lib/app/modules/device-detail/views/device_detail_view.dart index 9921e9e..4019ecf 100644 --- a/lib/app/modules/device-detail/views/device_detail_view.dart +++ b/lib/app/modules/device-detail/views/device_detail_view.dart @@ -49,15 +49,24 @@ class DeviceDetailView extends GetView { children: [ _buildSection( title: 'Monitoring Infus', - onSeeAll: () {}, child: _buildMonitoringCard(), ), const SizedBox(height: 20), _buildSection( title: 'Kontrol Alat', - onSeeAll: () {}, child: _buildControlCard(), ), + const SizedBox(height: 12), + // Buzzer card — hanya tampil kalau buzzer aktif + Obx(() { + if (!controller.isBuzzerActive.value) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: _buildBuzzerCard(), + ); + }), const SizedBox(height: 20), _buildSection( title: 'History Data', @@ -100,19 +109,7 @@ class DeviceDetailView extends GetView { fontWeight: FontWeight.bold, color: Colors.black87, ), - ), - if (onSeeAll != null) - GestureDetector( - onTap: onSeeAll, - child: const Text( - 'See all', - style: TextStyle( - fontSize: 13, - color: Color(0xFF2196F3), - fontWeight: FontWeight.w600, - ), - ), - ), + ), ], ), ), @@ -335,6 +332,98 @@ class DeviceDetailView extends GetView { ); } + /// Card buzzer — muncul saat buzzer_active == true + Widget _buildBuzzerCard() { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.red[200]!), + boxShadow: [ + BoxShadow( + color: Colors.red.withOpacity(0.08), + blurRadius: 12, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + children: [ + // Icon dengan animasi pulse + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.red[100], + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.notifications_active_rounded, + size: 28, + color: Colors.red[700], + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Buzzer Aktif', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: Colors.red[800], + ), + ), + const SizedBox(height: 4), + Text( + 'Tetesan infus berhenti. Harap periksa pasien.', + style: TextStyle( + fontSize: 12, + color: Colors.red[600], + height: 1.4, + ), + ), + ], + ), + ), + const SizedBox(width: 12), + // Tombol matikan buzzer + ElevatedButton( + onPressed: controller.turnOffBuzzer, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 10, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.volume_off_rounded, size: 18), + SizedBox(height: 3), + Text( + 'Matikan', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ], + ), + ); + } + Widget _buildEmptyChart() { return Container( padding: const EdgeInsets.all(32), @@ -375,4 +464,4 @@ class DeviceDetailView extends GetView { ), ); } -} +} \ No newline at end of file diff --git a/lib/app/modules/home-patient/controllers/home_patient_controller.dart b/lib/app/modules/home-patient/controllers/home_patient_controller.dart index 0ac39a6..3356d73 100644 --- a/lib/app/modules/home-patient/controllers/home_patient_controller.dart +++ b/lib/app/modules/home-patient/controllers/home_patient_controller.dart @@ -5,6 +5,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'dart:async'; import '../../../services/firestore_service.dart'; import '../../../services/realtime_database_service.dart'; +import '../../../services/notification_service.dart'; import '../../../models/patient_model.dart'; import '../../../models/chart_data_point.dart'; import '../../../models/history_model.dart'; @@ -51,6 +52,7 @@ class NotificationData { class HomePatientController extends GetxController { final FirestoreService _firestoreService = FirestoreService(); final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService(); + final NotificationService _notificationService = NotificationService(); final deviceId = ''.obs; final roomId = ''.obs; @@ -71,6 +73,8 @@ class HomePatientController extends GetxController { deviceStatus: 'disconnected', ).obs; + final isBuzzerActive = false.obs; + final chartDataPoints = [].obs; final notificationsList = [].obs; @@ -82,9 +86,12 @@ class HomePatientController extends GetxController { StreamSubscription? _notificationsSubscription; StreamSubscription? _historySubscription; + final Set _alertedDevices = {}; + @override void onInit() { super.onInit(); + _initializeNotificationService(); _loadDataFromArguments(); } @@ -101,6 +108,15 @@ class HomePatientController extends GetxController { super.onClose(); } + Future _initializeNotificationService() async { + try { + await _notificationService.initialize(); + print('Notification service initialized'); + } catch (e) { + print('Error initializing notification service: $e'); + } + } + void _loadDataFromArguments() async { try { final args = Get.arguments as Map?; @@ -176,14 +192,16 @@ class HomePatientController extends GetxController { final dbRoomId = roomId.value.toLowerCase().replaceAll(' ', '_'); - _realtimeSubscription?.cancel(); // Cancel previous subscription if exists + _realtimeSubscription?.cancel(); _realtimeSubscription = _realtimeService .getDeviceStream(dbRoomId, deviceId.value) .listen( - (device) { + (device) async { if (device != null) { - _updateCurrentInfusData(device); + await _updateCurrentInfusData(device); + await _checkDropRate(device); + isBuzzerActive.value = device.buzzerActive; } else { print('Received null device data'); } @@ -200,7 +218,7 @@ class HomePatientController extends GetxController { } } - void _updateCurrentInfusData(RealtimeMonitoringModel device) async { + Future _updateCurrentInfusData(RealtimeMonitoringModel device) async { try { final room = await _firestoreService.getRoomById(device.roomId); @@ -217,11 +235,61 @@ class HomePatientController extends GetxController { } } + Future _checkDropRate(RealtimeMonitoringModel device) async { + try { + final deviceId = device.deviceId; + final dropRate = device.dropRate; + final deviceStatus = device.deviceStatus; + + if (dropRate == 0.0 && deviceStatus == 'connected') { + if (!_alertedDevices.contains(deviceId)) { + final patient = await _firestoreService.getPatientByDeviceId( + deviceId, + ); + final room = await _firestoreService.getRoomById(device.roomId); + + if (patient != null) { + await _notificationService.showDropRateAlertForPatient( + patientId: patient.id, + patientName: patient.namePatient, + deviceId: deviceId, + roomName: room?.roomName ?? device.roomId, + ); + + _alertedDevices.add(deviceId); + print('Alert sent for device: $deviceId'); + } + } + } else if (dropRate > 0.0) { + if (_alertedDevices.contains(deviceId)) { + _alertedDevices.remove(deviceId); + _notificationService.clearCooldown(deviceId); + await _notificationService.cancelNotificationByDeviceId(deviceId); + print('Device reset and notification dismissed: $deviceId'); + } + } + } catch (e) { + print('Error checking drop rate: $e'); + } + } + + Future turnOffBuzzer() async { + try { + final dbRoomId = roomId.value.toLowerCase().replaceAll(' ', '_'); + await _realtimeService.controlBuzzer(dbRoomId, deviceId.value, false); + isBuzzerActive.value = false; + AppSnackbar.success('Buzzer dimatikan'); + } catch (e) { + print('Error turning off buzzer: $e'); + AppSnackbar.error('Gagal mematikan buzzer'); + } + } + void _loadHistoryData() { try { isLoadingHistory.value = true; - _historySubscription?.cancel(); // Cancel previous subscription if exists + _historySubscription?.cancel(); _historySubscription = _firestoreService .getDeviceHistoriesStream(deviceId.value) @@ -257,12 +325,16 @@ class HomePatientController extends GetxController { void _processHistoryData(List histories) { try { + // Ambil 10 data TERBARU histories.sort((a, b) => b.timestamp.compareTo(a.timestamp)); final latestHistories = histories.take(10).toList(); + + // Urutkan lagi dari lama -> baru supaya grafik terbaca kiri ke kanan latestHistories.sort((a, b) => a.timestamp.compareTo(b.timestamp)); + final List points = latestHistories.map((history) { return ChartDataPoint( - hour: history.timestamp.hour, + timestamp: history.timestamp, dropsPerMinute: history.dropPerMinute, ); }).toList(); @@ -278,8 +350,7 @@ class HomePatientController extends GetxController { try { isLoadingNotifications.value = true; - _notificationsSubscription - ?.cancel(); // Cancel previous subscription if exists + _notificationsSubscription?.cancel(); _notificationsSubscription = _firestoreService .getNotificationsStream() @@ -313,13 +384,12 @@ class HomePatientController extends GetxController { ); } catch (e) { print('Error processing notification ${notif.id}: $e'); - // Continue with other notifications } } notifList.sort((a, b) => b.createdAt.compareTo(a.createdAt)); - notificationsList.value = notifList; + notificationsList.value = notifList.take(2).toList(); isLoadingNotifications.value = false; } catch (e) { print('Error processing notifications: $e'); @@ -349,6 +419,7 @@ class HomePatientController extends GetxController { context: context, builder: (BuildContext context) { return AlertDialog( + backgroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), @@ -370,9 +441,7 @@ class HomePatientController extends GetxController { await _openWhatsApp(); }, style: ElevatedButton.styleFrom( - backgroundColor: const Color( - 0xFF25D366, - ), // WhatsApp green color + backgroundColor: const Color(0xFF25D366), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), @@ -395,8 +464,7 @@ class HomePatientController extends GetxController { Future _openWhatsApp() async { try { - const String nursePhoneNumber = - '6281234567890'; + const String nursePhoneNumber = '6281231901277'; final String message = Uri.encodeComponent( 'Halo, saya ${patientName.value} di ruangan ${currentInfusData.value.room}. ' @@ -506,11 +574,19 @@ class HomePatientController extends GetxController { Future refreshData() async { try { await _loadPatientData(); - // Tidak perlu reload history karena sudah menggunakan stream - // History akan auto-update } catch (e) { print('Error refreshing data: $e'); AppSnackbar.error('Gagal refresh data: $e'); } } -} + + void clearDeviceAlert(String deviceId) { + _alertedDevices.remove(deviceId); + _notificationService.clearCooldown(deviceId); + } + + void clearAllAlerts() { + _alertedDevices.clear(); + _notificationService.clearAllCooldowns(); + } +} \ No newline at end of file diff --git a/lib/app/modules/home-patient/views/home_patient_view.dart b/lib/app/modules/home-patient/views/home_patient_view.dart index a9ebbb4..ff8800c 100644 --- a/lib/app/modules/home-patient/views/home_patient_view.dart +++ b/lib/app/modules/home-patient/views/home_patient_view.dart @@ -12,8 +12,6 @@ class HomePatientView extends GetView { @override Widget build(BuildContext context) { - Get.put(HomePatientController()); - return Scaffold( backgroundColor: Colors.white, body: SafeArea( @@ -33,9 +31,9 @@ class HomePatientView extends GetView { Expanded( child: Obx( () => Text( - 'Halo ${controller.patientName.value}', + 'Halo Wali Pasien ${controller.patientName.value}', style: const TextStyle( - fontSize: 24, + fontSize: 20, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, @@ -67,36 +65,111 @@ class HomePatientView extends GetView { const SizedBox(height: 15), + // ── BUZZER ALERT BANNER ────────────────────────────────── + Obx(() { + if (!controller.isBuzzerActive.value) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 12), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Colors.red[50], + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.red[200]!), + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.red[100], + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.notifications_active_rounded, + color: Colors.red[700], + size: 26, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Buzzer Aktif!', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: Colors.red[800], + ), + ), + const SizedBox(height: 3), + Text( + 'Tetesan infus berhenti. Harap hubungi perawat.', + style: TextStyle( + fontSize: 12, + color: Colors.red[600], + height: 1.4, + ), + ), + ], + ), + ), + const SizedBox(width: 10), + ElevatedButton( + onPressed: controller.turnOffBuzzer, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red[700], + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + elevation: 0, + ), + child: const Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.volume_off_rounded, size: 18), + SizedBox(height: 2), + Text( + 'Matikan', + style: TextStyle(fontSize: 11), + ), + ], + ), + ), + ], + ), + ), + ); + }), + // ── END BUZZER ALERT BANNER ────────────────────────────── + // Monitoring Infus Section Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( + children: const [ + Text( 'Monitoring Infus', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), - TextButton( - onPressed: () { - // TODO: Navigate to monitoring detail - }, - child: const Text( - 'See all', - style: TextStyle( - color: Color(0xFF2196F3), - fontWeight: FontWeight.w600, - ), - ), - ), ], ), ), - // Monitoring Card - Gunakan MonitoringCard dari widgets + // Monitoring Card Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Obx(() { @@ -114,24 +187,20 @@ class HomePatientView extends GetView { final infusData = controller.currentInfusData.value; - // Konversi ke InfusMonitoring untuk MonitoringCard final monitoring = InfusMonitoring( id: controller.deviceId.value, patientName: controller.patientName.value, deviceId: infusData.deviceId, room: infusData.room, - roomId: '', // Not needed for display + roomId: '', dropsPerMinute: infusData.dropsPerMinute, - lastUpdate: - DateTime.now(), // Will be calculated from updateTime + lastUpdate: DateTime.now(), deviceStatus: infusData.deviceStatus, ); return MonitoringCard( monitoring: monitoring, - onTap: () { - // TODO: Navigate to detail - }, + onTap: () {}, ); }), ), @@ -143,31 +212,19 @@ class HomePatientView extends GetView { padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( + children: const [ + Text( 'History Data', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), - TextButton( - onPressed: () { - // TODO: Navigate to history detail - }, - child: const Text( - 'See all', - style: TextStyle( - color: Color(0xFF2196F3), - fontWeight: FontWeight.w600, - ), - ), - ), ], ), ), - // Chart Card - Gunakan HistoryGrafikCard dari widgets + // Chart Card Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Obx(() { @@ -185,9 +242,7 @@ class HomePatientView extends GetView { return HistoryGrafikCard( dataPoints: controller.chartDataPoints.toList(), - onBookmarkTap: () { - // TODO: Bookmark functionality - }, + onBookmarkTap: () {}, ); }), ), @@ -199,26 +254,14 @@ class HomePatientView extends GetView { padding: const EdgeInsets.symmetric(horizontal: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( + children: const [ + Text( 'History Notifikasi', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), - TextButton( - onPressed: () { - // TODO: Navigate to notifications - }, - child: const Text( - 'See all', - style: TextStyle( - color: Color(0xFF2196F3), - fontWeight: FontWeight.w600, - ), - ), - ), ], ), ), @@ -278,9 +321,7 @@ class HomePatientView extends GetView { room: notification.room, deviceId: notification.deviceId, timeAgo: notification.timeAgo, - onTap: () { - // TODO: Handle notification tap - }, + onTap: () {}, ); }, ); @@ -293,7 +334,6 @@ class HomePatientView extends GetView { ), ), - // Floating Action Button - Call Nurse floatingActionButton: FloatingActionButton( onPressed: () => controller.showCallDialog(context), backgroundColor: const Color(0xFF2196F3), @@ -301,4 +341,4 @@ class HomePatientView extends GetView { ), ); } -} +} \ No newline at end of file diff --git a/lib/app/modules/home/controllers/home_controller.dart b/lib/app/modules/home/controllers/home_controller.dart index 5eb410f..06c6d73 100644 --- a/lib/app/modules/home/controllers/home_controller.dart +++ b/lib/app/modules/home/controllers/home_controller.dart @@ -4,6 +4,7 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'dart:async'; import '../../../services/firestore_service.dart'; import '../../../services/realtime_database_service.dart'; +import '../../../services/notification_service.dart'; import '../../../models/patient_model.dart'; import '../../../models/room_model.dart'; import '../../../models/realtime_monitoring_model.dart'; @@ -35,6 +36,7 @@ class HomeController extends GetxController { final searchController = TextEditingController(); final FirestoreService _firestoreService = FirestoreService(); final RealtimeDatabaseService _realtimeService = RealtimeDatabaseService(); + final NotificationService _notificationService = NotificationService(); final currentCarouselIndex = 0.obs; final bannerImages = [ @@ -45,12 +47,27 @@ class HomeController extends GetxController { final monitoringList = [].obs; final isLoading = true.obs; + List _cachedRooms = []; + StreamSubscription? _patientsSubscription; - StreamSubscription? _realtimeSubscription; + StreamSubscription? _roomsSubscription; + + // Map deviceId -> StreamSubscription untuk per-device realtime stream + final Map _deviceSubscriptions = {}; + + // Map deviceId -> data realtime terbaru + final Map _realtimeDataMap = {}; + + List _latestPatients = []; + + bool _patientsReady = false; + + final Set _alertedDevices = {}; @override void onInit() { super.onInit(); + _initializeNotificationService(); _initializeMonitoring(); } @@ -58,44 +75,128 @@ class HomeController extends GetxController { void onClose() { searchController.dispose(); _patientsSubscription?.cancel(); - _realtimeSubscription?.cancel(); + _roomsSubscription?.cancel(); + // Cancel semua per-device subscriptions + for (final sub in _deviceSubscriptions.values) { + sub.cancel(); + } + _deviceSubscriptions.clear(); super.onClose(); } + Future _initializeNotificationService() async { + try { + await _notificationService.initialize(); + } catch (e) { + print('Error initializing notification service: $e'); + } + } + void _initializeMonitoring() { isLoading.value = true; + _patientsReady = false; + // 1. Cache rooms + _roomsSubscription = _firestoreService.getRoomsStream().listen((rooms) { + _cachedRooms = rooms; + }); + + // 2. Patients stream _patientsSubscription = _firestoreService.getPatientsStream().listen( - (patients) async { - _realtimeSubscription = _realtimeService.getAllDevicesStream().listen(( - realtimeDevices, - ) { - _combineMonitoringData(patients, realtimeDevices); - }); + (patients) { + _latestPatients = patients; + _patientsReady = true; + _syncDeviceSubscriptions(patients); }, onError: (error) { + print('Error patients stream: $error'); isLoading.value = false; - AppSnackbar.error('Gagal memuat data'); + AppSnackbar.error('Gagal memuat data pasien'); }, ); + + // Timeout fallback + Future.delayed(const Duration(seconds: 8), () { + if (isLoading.value) { + print('Timeout: force stop loading'); + isLoading.value = false; + } + }); } - void _combineMonitoringData( - List patients, - List realtimeDevices, - ) async { + /// Sinkronisasi per-device stream — ikuti pola HomePatientController + /// pakai getDeviceStream(dbRoomId, deviceId) bukan getAllDevicesStream() + void _syncDeviceSubscriptions(List patients) { + final currentDeviceIds = patients.map((p) => p.deviceId).toSet(); + final existingDeviceIds = _deviceSubscriptions.keys.toSet(); + + // Cancel stream untuk device yang sudah tidak ada di patients + final removedIds = existingDeviceIds.difference(currentDeviceIds); + for (final deviceId in removedIds) { + _deviceSubscriptions[deviceId]?.cancel(); + _deviceSubscriptions.remove(deviceId); + _realtimeDataMap.remove(deviceId); + } + + // Tambah stream untuk device baru + final newIds = currentDeviceIds.difference(existingDeviceIds); + for (final patient in patients.where((p) => newIds.contains(p.deviceId))) { + _subscribeToDevice(patient); + } + } + + void _subscribeToDevice(PatientModel patient) { + // Format room ID sama persis seperti HomePatientController + final dbRoomId = patient.roomId.toLowerCase().replaceAll(' ', '_'); + final deviceId = patient.deviceId; + + print('Subscribing to device: $deviceId in room: $dbRoomId'); + + final sub = _realtimeService + .getDeviceStream(dbRoomId, deviceId) + .listen( + (device) { + if (device != null) { + _realtimeDataMap[deviceId] = device; + } else { + // Device null = disconnected, hapus dari map + _realtimeDataMap.remove(deviceId); + } + // Rebuild setiap ada update dari device manapun + if (_patientsReady) { + _rebuildMonitoringList(); + } + }, + onError: (error) { + print('Error stream device $deviceId: $error'); + // Tetap rebuild walau error, device akan tampil sebagai disconnected + if (_patientsReady) { + _rebuildMonitoringList(); + } + }, + cancelOnError: false, + ); + + _deviceSubscriptions[deviceId] = sub; + } + + void _rebuildMonitoringList() { final List combinedData = []; - for (var patient in patients) { - final realtimeDevice = realtimeDevices.firstWhereOrNull( - (device) => device.deviceId == patient.deviceId, - ); - - final roomName = await _getRoomName(patient.roomId); + for (var patient in _latestPatients) { + final realtimeDevice = _realtimeDataMap[patient.deviceId]; + final roomName = _getRoomNameFromCache(patient.roomId); final dropRate = realtimeDevice?.dropRate ?? 0.0; final lastUpdate = realtimeDevice?.lastUpdate ?? DateTime.now(); final deviceStatus = realtimeDevice?.deviceStatus ?? 'disconnected'; + _checkDropRate( + patient: patient, + dropRate: dropRate, + roomName: roomName, + deviceStatus: deviceStatus, + ); + combinedData.add( InfusMonitoring( id: patient.id, @@ -114,16 +215,37 @@ class HomeController extends GetxController { isLoading.value = false; } - Future _getRoomName(String roomId) async { - try { - final rooms = await _firestoreService.getRoomsStream().first; - final room = rooms.firstWhereOrNull( - (r) => - r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(), - ); - return room?.roomName ?? roomId; - } catch (e) { - return roomId; + String _getRoomNameFromCache(String roomId) { + final room = _cachedRooms.firstWhereOrNull( + (r) => r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(), + ); + return room?.roomName ?? roomId; + } + + Future _checkDropRate({ + required PatientModel patient, + required double dropRate, + required String roomName, + required String deviceStatus, + }) async { + final deviceId = patient.deviceId; + + if (dropRate == 0.0 && deviceStatus == 'connected') { + if (!_alertedDevices.contains(deviceId)) { + await _notificationService.showDropRateAlert( + patientId: patient.id, + patientName: patient.namePatient, + deviceId: deviceId, + roomName: roomName, + ); + _alertedDevices.add(deviceId); + } + } else if (dropRate > 0.0) { + if (_alertedDevices.contains(deviceId)) { + _alertedDevices.remove(deviceId); + _notificationService.clearCooldown(deviceId); + await _notificationService.cancelNotificationByDeviceId(deviceId); + } } } @@ -207,7 +329,9 @@ class HomeController extends GetxController { String getLastUpdateText(DateTime lastUpdate) { final difference = DateTime.now().difference(lastUpdate); - if (difference.inMinutes < 60) { + if (difference.inMinutes < 1) { + return 'Just now'; + } else if (difference.inMinutes < 60) { return '${difference.inMinutes}m ago'; } else if (difference.inHours < 24) { return '${difference.inHours}h ago'; @@ -215,4 +339,14 @@ class HomeController extends GetxController { return '${difference.inDays}d ago'; } } + + void clearDeviceAlert(String deviceId) { + _alertedDevices.remove(deviceId); + _notificationService.clearCooldown(deviceId); + } + + void clearAllAlerts() { + _alertedDevices.clear(); + _notificationService.clearAllCooldowns(); + } } diff --git a/lib/app/modules/home/views/home_view.dart b/lib/app/modules/home/views/home_view.dart index 5a146a7..252ccc4 100644 --- a/lib/app/modules/home/views/home_view.dart +++ b/lib/app/modules/home/views/home_view.dart @@ -9,10 +9,8 @@ class HomeView extends GetView { @override Widget build(BuildContext context) { - Get.put(HomeController()); - return Scaffold( - backgroundColor: Colors.white, + backgroundColor: const Color(0xFFF5F7FA), body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -23,8 +21,9 @@ class HomeView extends GetView { currentCarouselIndex: controller.currentCarouselIndex, onPageChanged: controller.updateCarouselIndex, ), - const SizedBox(height: 15), + const SizedBox(height: 16), _buildSectionHeader(), + const SizedBox(height: 8), Expanded(child: _buildMonitoringList()), ], ), @@ -33,24 +32,49 @@ class HomeView extends GetView { } Widget _buildHeader(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(20), + return Container( + color: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'Halo Perawat', - style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Selamat Datang 👋', + style: TextStyle( + fontSize: 13, + color: Colors.grey[500], + fontWeight: FontWeight.w400, + ), + ), + const SizedBox(height: 2), + const Text( + 'Halo, Perawat', + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.bold, + color: Color(0xFF1A1D2E), + ), + ), + ], ), InkWell( onTap: () => controller.showLogoutDialog(context), + borderRadius: BorderRadius.circular(12), child: Container( - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.all(10), decoration: BoxDecoration( - border: Border.all(color: Colors.grey[300]!), - borderRadius: BorderRadius.circular(10), + color: const Color(0xFFFFF0F0), + border: Border.all(color: const Color(0xFFFFCDD2)), + borderRadius: BorderRadius.circular(12), + ), + child: const Icon( + Icons.logout_rounded, + color: Colors.red, + size: 20, ), - child: const Icon(Icons.logout, color: Colors.red), ), ), ], @@ -66,15 +90,13 @@ class HomeView extends GetView { children: [ const Text( 'Monitoring Infus', - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - TextButton( - onPressed: () => Get.toNamed('/all-monitoring'), - child: const Text( - 'Lihat Semua', - style: TextStyle(color: Color(0xFF0091EA)), + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.bold, + color: Color(0xFF1A1D2E), ), ), + ], ), ); @@ -83,52 +105,203 @@ class HomeView extends GetView { Widget _buildMonitoringList() { return Obx(() { if (controller.isLoading.value) { - return const Center( - child: CircularProgressIndicator(color: Color(0xFF0091EA)), - ); + return _buildLoadingState(); } if (controller.monitoringList.isEmpty) { return _buildEmptyState(); } - return ListView.builder( - padding: const EdgeInsets.symmetric(horizontal: 20), - itemCount: controller.monitoringList.length, - itemBuilder: (context, index) { - final item = controller.monitoringList[index]; - return MonitoringCard( - monitoring: item, - onTap: () => Get.toNamed( - '/device-detail', - arguments: { - 'deviceId': item.deviceId, - 'roomId': item.roomId, - }, - ), - ); + return RefreshIndicator( + color: const Color(0xFF0091EA), + onRefresh: () async { + // Trigger refresh — stream akan otomatis update, + // ini hanya untuk UX pull-to-refresh + await Future.delayed(const Duration(milliseconds: 800)); }, + child: ListView.builder( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), + physics: const AlwaysScrollableScrollPhysics(), + itemCount: controller.monitoringList.length, + itemBuilder: (context, index) { + final item = controller.monitoringList[index]; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: MonitoringCard( + monitoring: item, + onTap: () => Get.toNamed( + '/device-detail', + arguments: {'deviceId': item.deviceId, 'roomId': item.roomId}, + ), + ), + ); + }, + ), ); }); } - Widget _buildEmptyState() { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.medical_services_outlined, - size: 80, - color: Colors.grey[400], - ), - const SizedBox(height: 16), - Text( - 'Tidak ada data monitoring', - style: TextStyle(fontSize: 16, color: Colors.grey[600]), + Widget _buildLoadingState() { + return ListView.builder( + padding: const EdgeInsets.fromLTRB(20, 4, 20, 20), + itemCount: 4, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _buildSkeletonCard(), + ); + }, + ); + } + + Widget _buildSkeletonCard() { + return Container( + height: 110, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.04), + blurRadius: 8, + offset: const Offset(0, 2), ), ], ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + _shimmerBox(width: 36, height: 36, radius: 10), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _shimmerBox(width: 140, height: 13, radius: 6), + const SizedBox(height: 6), + _shimmerBox(width: 90, height: 11, radius: 6), + ], + ), + const Spacer(), + _shimmerBox(width: 60, height: 26, radius: 20), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + _shimmerBox(width: 80, height: 11, radius: 6), + const SizedBox(width: 16), + _shimmerBox(width: 60, height: 11, radius: 6), + ], + ), + ], + ), + ), ); } -} \ No newline at end of file + + Widget _shimmerBox({ + required double width, + required double height, + double radius = 4, + }) { + return _ShimmerWidget( + child: Container( + width: width, + height: height, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(radius), + ), + ), + ); + } + + Widget _buildEmptyState() { + return Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 40), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 100, + height: 100, + decoration: BoxDecoration( + color: const Color(0xFFE3F2FD), + borderRadius: BorderRadius.circular(30), + ), + child: const Icon( + Icons.water_drop_outlined, + size: 48, + color: Color(0xFF0091EA), + ), + ), + const SizedBox(height: 20), + const Text( + 'Belum Ada Data', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF1A1D2E), + ), + ), + const SizedBox(height: 8), + Text( + 'Data monitoring infus akan tampil di sini setelah perangkat terhubung.', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: Colors.grey[500], + height: 1.5, + ), + ), + ], + ), + ), + ); + } +} + +/// Widget shimmer animasi untuk skeleton loading +class _ShimmerWidget extends StatefulWidget { + final Widget child; + const _ShimmerWidget({required this.child}); + + @override + State<_ShimmerWidget> createState() => _ShimmerWidgetState(); +} + +class _ShimmerWidgetState extends State<_ShimmerWidget> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(reverse: true); + _animation = Tween( + begin: 0.4, + end: 1.0, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return FadeTransition(opacity: _animation, child: widget.child); + } +} diff --git a/lib/app/modules/login/controllers/login_controller.dart b/lib/app/modules/login/controllers/login_controller.dart index 19aa2ac..3ccb131 100644 --- a/lib/app/modules/login/controllers/login_controller.dart +++ b/lib/app/modules/login/controllers/login_controller.dart @@ -18,7 +18,6 @@ class LoginController extends GetxController { } void login() async { - // Validasi input if (usernameController.text.isEmpty || passwordController.text.isEmpty) { Get.snackbar( 'Error', @@ -30,7 +29,6 @@ class LoginController extends GetxController { return; } - // Validasi format email if (!GetUtils.isEmail(usernameController.text)) { Get.snackbar( 'Error', @@ -45,27 +43,23 @@ class LoginController extends GetxController { try { isLoading.value = true; - // Login dengan Firebase Authentication UserCredential userCredential = await _auth.signInWithEmailAndPassword( email: usernameController.text.trim(), password: passwordController.text.trim(), ); if (userCredential.user != null) { - // Cek apakah user ada di collection users final userDoc = await _firestore .collection('users') .doc(userCredential.user!.uid) .get(); if (userDoc.exists) { - // User ada di collection users (Guardian/Patient) final userData = userDoc.data()!; final deviceId = userData['device_id'] as String?; final userName = userData['name'] as String? ?? 'User'; if (deviceId != null && deviceId.isNotEmpty) { - // User adalah guardian/patient dengan device_id Get.snackbar( 'Berhasil', 'Selamat datang, $userName!', @@ -74,7 +68,6 @@ class LoginController extends GetxController { snackPosition: SnackPosition.TOP, ); - // Navigasi ke home_patient dengan device_id Get.offAllNamed( '/home-patient', arguments: { @@ -84,7 +77,6 @@ class LoginController extends GetxController { }, ); } else { - // User ada tapi tidak punya device_id Get.snackbar( 'Error', 'Akun Anda belum terdaftar dengan device', @@ -95,7 +87,6 @@ class LoginController extends GetxController { await _auth.signOut(); } } else { - // User TIDAK ada di collection users = Perawat/Admin Get.snackbar( 'Berhasil', 'Login berhasil! Selamat datang, Perawat', @@ -104,7 +95,6 @@ class LoginController extends GetxController { snackPosition: SnackPosition.TOP, ); - // Navigasi ke navbar/home perawat Get.offAllNamed('/navbar'); } } @@ -156,12 +146,20 @@ class LoginController extends GetxController { } void contactNurse() async { - const phoneNumber = '+6281231901277'; // Tanpa spasi - final url = Uri.parse('https://wa.me/$phoneNumber'); + const phoneNumber = '6281231901277'; + final message = Uri.encodeComponent( + 'Halo, saya wali pasien dari (Nama Pasien) ' + 'Saya ingin meminta akun untuk login ke aplikasi Smart Infus.', + ); + // Construct the WhatsApp URL with the message + final url = Uri.parse('https://wa.me/$phoneNumber?text=$message'); + + // Attempt to launch WhatsApp if (await canLaunchUrl(url)) { await launchUrl(url, mode: LaunchMode.externalApplication); } else { + // Show an error if WhatsApp can't be opened Get.snackbar( 'Error', 'Tidak dapat membuka WhatsApp', diff --git a/lib/app/modules/navbar/bindings/navbar_binding.dart b/lib/app/modules/navbar/bindings/navbar_binding.dart index f99c3db..f8fc104 100644 --- a/lib/app/modules/navbar/bindings/navbar_binding.dart +++ b/lib/app/modules/navbar/bindings/navbar_binding.dart @@ -2,7 +2,7 @@ import 'package:get/get.dart'; import '../controllers/navbar_controller.dart'; import '../../home/controllers/home_controller.dart'; import '../../notification/controllers/notification_controller.dart'; -import '../../schedule/controllers/schedule_controller.dart'; +// import '../../schedule/controllers/schedule_controller.dart'; import '../../profile/controllers/profile_controller.dart'; class NavbarBinding extends Bindings { @@ -11,7 +11,7 @@ class NavbarBinding extends Bindings { Get.lazyPut(() => NavbarController()); Get.lazyPut(() => HomeController()); Get.lazyPut(() => NotificationController()); - Get.lazyPut(() => ScheduleController()); + // Get.lazyPut(() => ScheduleController()); Get.lazyPut(() => ProfileController()); } } diff --git a/lib/app/modules/navbar/views/navbar_view.dart b/lib/app/modules/navbar/views/navbar_view.dart index 2d3c1f3..aec8848 100644 --- a/lib/app/modules/navbar/views/navbar_view.dart +++ b/lib/app/modules/navbar/views/navbar_view.dart @@ -3,7 +3,7 @@ import 'package:get/get.dart'; import '../controllers/navbar_controller.dart'; import '../../home/views/home_view.dart'; import '../../notification/views/notification_view.dart'; -import '../../schedule/views/schedule_view.dart'; +// import '../../schedule/views/schedule_view.dart'; import '../../profile/views/profile_view.dart'; class NavbarView extends StatelessWidget { @@ -16,7 +16,7 @@ class NavbarView extends StatelessWidget { final pages = const [ HomeView(), NotificationView(), - ScheduleView(), + // ScheduleView(), ProfileView(), ]; @@ -52,8 +52,8 @@ class NavbarView extends StatelessWidget { children: [ _navItem(controller, Icons.home, 0, 'Home'), _navItem(controller, Icons.chat_bubble_outline, 1, 'Notification'), - _navItem(controller, Icons.calendar_today, 2, 'Schedule'), - _navItem(controller, Icons.person_outline, 3, 'Account'), + // _navItem(controller, Icons.calendar_today, 2, 'Schedule'), + _navItem(controller, Icons.person_outline, 2, 'Account'), ], ), ); diff --git a/lib/app/modules/notification/controllers/notification_controller.dart b/lib/app/modules/notification/controllers/notification_controller.dart index 0663046..883de71 100644 --- a/lib/app/modules/notification/controllers/notification_controller.dart +++ b/lib/app/modules/notification/controllers/notification_controller.dart @@ -53,7 +53,7 @@ class NotificationItem { class NotificationController extends GetxController { final FirestoreService _firestoreService = FirestoreService(); - + final selectedTab = 0.obs; final _allNotifications = [].obs; final isLoading = true.obs; @@ -76,47 +76,51 @@ class NotificationController extends GetxController { void _initializeNotifications() { isLoading.value = true; - _notificationsSubscription = _firestoreService - .getNotificationsStream() - .listen((notificationModels) async { - final List items = []; + _notificationsSubscription = _firestoreService.getNotificationsStream().listen( + (notificationModels) async { + final List items = []; - for (var notifModel in notificationModels) { - // Get patient info - final patient = await _firestoreService.getPatientById( - notifModel.patientId, - ); - - // Get room name from patient's roomId - String roomName = 'Unknown Room'; - if (patient != null && patient.roomId.isNotEmpty) { - final rooms = await _firestoreService.getRoomsStream().first; - final room = rooms.firstWhereOrNull( - (r) => r.roomName.toLowerCase() == patient.roomId.toLowerCase(), + for (var notifModel in notificationModels) { + // Get patient info + final patient = await _firestoreService.getPatientById( + notifModel.patientId, + ); + + // Get room name from patient's roomId + String roomName = 'Unknown Room'; + if (patient != null && patient.roomId.isNotEmpty) { + final rooms = await _firestoreService.getRoomsStream().first; + final room = rooms.firstWhereOrNull( + (r) => + r.id == + patient + .roomId, // ✅ UBAH INI - cari berdasarkan ID, bukan roomName + ); + roomName = room?.roomName ?? patient.roomId; + } + + items.add( + NotificationItem( + id: notifModel.id, + title: notifModel.title, + message: notifModel.message, + room: roomName, + deviceId: notifModel.deviceId, + patientName: patient?.namePatient ?? 'Unknown Patient', + time: notifModel.createdAt, + isRead: notifModel.isRead, + ), ); - roomName = room?.roomName ?? patient.roomId; } - items.add( - NotificationItem( - id: notifModel.id, - title: notifModel.title, - message: notifModel.message, - room: roomName, - deviceId: notifModel.deviceId, - patientName: patient?.namePatient ?? 'Unknown Patient', - time: notifModel.createdAt, - isRead: notifModel.isRead, - ), - ); - } - - _allNotifications.value = items; - isLoading.value = false; - }, onError: (error) { - print('Error listening to notifications: $error'); - isLoading.value = false; - }); + _allNotifications.value = items; + isLoading.value = false; + }, + onError: (error) { + print('Error listening to notifications: $error'); + isLoading.value = false; + }, + ); } // Computed property untuk notifikasi yang difilter @@ -205,4 +209,4 @@ class NotificationController extends GetxController { print('Error adding notification: $e'); } } -} \ No newline at end of file +} diff --git a/lib/app/modules/notification/views/notification_view.dart b/lib/app/modules/notification/views/notification_view.dart index d05667a..51a58a7 100644 --- a/lib/app/modules/notification/views/notification_view.dart +++ b/lib/app/modules/notification/views/notification_view.dart @@ -87,15 +87,6 @@ class NotificationView extends GetView { notification: notif, onDismissed: () { controller.deleteNotification(notif.id); - Get.snackbar( - 'Notifikasi Dihapus', - 'Notifikasi telah dihapus', - snackPosition: SnackPosition.BOTTOM, - duration: const Duration(seconds: 2), - margin: const EdgeInsets.all(16), - backgroundColor: Colors.grey[800], - colorText: Colors.white, - ); }, onTap: !notif.isRead ? () { controller.markAsRead(notif.id); @@ -105,15 +96,6 @@ class NotificationView extends GetView { controller.changeTab(1); }); - Get.snackbar( - 'Notifikasi Dibaca', - 'Notifikasi telah dipindahkan ke sudah dibaca', - snackPosition: SnackPosition.BOTTOM, - duration: const Duration(seconds: 2), - margin: const EdgeInsets.all(16), - backgroundColor: const Color(0xFF0091EA), - colorText: Colors.white, - ); } : null, ); }, diff --git a/lib/app/modules/profile/controllers/profile_controller.dart b/lib/app/modules/profile/controllers/profile_controller.dart index 5df3f21..1970227 100644 --- a/lib/app/modules/profile/controllers/profile_controller.dart +++ b/lib/app/modules/profile/controllers/profile_controller.dart @@ -369,7 +369,7 @@ class ProfileController extends GetxController { }, 'monitoring': { 'device_status': 'connected', - 'drop_rate': 0, + 'drop_rate': 1, 'last_update': DateTime.now().toIso8601String(), }, 'settings': {'servo_angle_open': 90, 'servo_angle_close': 0}, diff --git a/lib/app/modules/schedule/controllers/schedule_controller.dart b/lib/app/modules/schedule/controllers/schedule_controller.dart index 3b2e979..bbebf03 100644 --- a/lib/app/modules/schedule/controllers/schedule_controller.dart +++ b/lib/app/modules/schedule/controllers/schedule_controller.dart @@ -5,7 +5,8 @@ import '../../../models/schedule_model.dart'; import '../../../widgets/app_snackbar.dart'; class PatientSchedule { - final String id; + final String id; + final String patientId; final String name; final String room; final String deviceId; @@ -15,6 +16,7 @@ class PatientSchedule { PatientSchedule({ required this.id, + required this.patientId, required this.name, required this.room, required this.deviceId, @@ -61,45 +63,48 @@ class ScheduleController extends GetxController { _schedulesSubscription = _firestoreService .getSchedulesStream(date: selectedDate.value) .listen( - (scheduleModels) async { - try { - final List items = []; - - for (var scheduleModel in scheduleModels) { + (scheduleModels) async { try { - final patient = await _firestoreService.getPatientById(scheduleModel.patientId); + final List items = []; - if (patient != null) { - final roomName = await _getRoomName(patient.roomId); - items.add( - PatientSchedule( - id: scheduleModel.id, - name: patient.namePatient, - room: roomName, - deviceId: patient.deviceId, - medicineTime: scheduleModel.medicineDetail, - fluidTime: scheduleModel.fluidDetail, - timeOfDay: scheduleModel.timeOfDay, - ), - ); + for (var scheduleModel in scheduleModels) { + try { + final patient = await _firestoreService.getPatientById( + scheduleModel.patientId, + ); + + if (patient != null) { + final roomName = await _getRoomName(patient.roomId); + items.add( + PatientSchedule( + id: scheduleModel.id, + patientId: scheduleModel.patientId, + name: patient.namePatient, + room: roomName, + deviceId: patient.deviceId, + medicineTime: scheduleModel.medicineDetail, + fluidTime: scheduleModel.fluidDetail, + timeOfDay: scheduleModel.timeOfDay, + ), + ); + } + } catch (e) { + continue; + } } - } catch (e) { - continue; - } - } - allSchedules.value = items; - _applyTimeFilter(); - isLoading.value = false; - } catch (e) { - isLoading.value = false; - } - }, - onError: (error) { - isLoading.value = false; - AppSnackbar.error('Gagal memuat jadwal'); - }, - ); + allSchedules.value = items; + _applyTimeFilter(); + isLoading.value = false; + } catch (e) { + isLoading.value = false; + } + }, + onError: (error) { + isLoading.value = false; + AppSnackbar.error('Gagal memuat jadwal'); + }, + ); } void _applyTimeFilter() { @@ -113,7 +118,8 @@ class ScheduleController extends GetxController { try { final rooms = await _firestoreService.getRoomsStream().first; final room = rooms.firstWhereOrNull( - (r) => r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(), + (r) => + r.id == roomId || r.roomName.toLowerCase() == roomId.toLowerCase(), ); return room?.roomName ?? roomId; } catch (e) { @@ -135,16 +141,14 @@ class ScheduleController extends GetxController { _loadSchedules(); } - Future addSchedule(PatientSchedule schedule, DateTime scheduleDate) async { + Future addSchedule( + PatientSchedule schedule, + DateTime scheduleDate, + ) async { try { - final patients = await _firestoreService.getPatientsStream().first; - final patient = patients.firstWhereOrNull((p) => p.id == schedule.id); - - if (patient == null) throw Exception('Pasien tidak ditemukan'); - final scheduleModel = ScheduleModel( id: '', - patientId: patient.id, + patientId: schedule.patientId, medicineDetail: schedule.medicineTime, fluidDetail: schedule.fluidTime, scheduleDate: scheduleDate, @@ -155,20 +159,19 @@ class ScheduleController extends GetxController { await _firestoreService.addSchedule(scheduleModel); AppSnackbar.success('Jadwal berhasil ditambahkan'); } catch (e) { - AppSnackbar.error('Gagal menambahkan jadwal'); + AppSnackbar.error('Gagal menambahkan jadwal: ${e.toString()}'); } } - Future updateSchedule(String id, PatientSchedule updatedSchedule, DateTime scheduleDate) async { + Future updateSchedule( + String scheduleId, + PatientSchedule updatedSchedule, + DateTime scheduleDate, + ) async { try { - final patients = await _firestoreService.getPatientsStream().first; - final patient = patients.firstWhereOrNull((p) => p.id == updatedSchedule.id); - - if (patient == null) throw Exception('Pasien tidak ditemukan'); - final scheduleModel = ScheduleModel( - id: id, - patientId: patient.id, + id: scheduleId, + patientId: updatedSchedule.patientId, medicineDetail: updatedSchedule.medicineTime, fluidDetail: updatedSchedule.fluidTime, scheduleDate: scheduleDate, @@ -176,10 +179,10 @@ class ScheduleController extends GetxController { createdAt: DateTime.now(), ); - await _firestoreService.updateSchedule(id, scheduleModel); + await _firestoreService.updateSchedule(scheduleId, scheduleModel); AppSnackbar.success('Jadwal berhasil diperbarui'); } catch (e) { - AppSnackbar.error('Gagal memperbarui jadwal'); + AppSnackbar.error('Gagal memperbarui jadwal: ${e.toString()}'); } } @@ -188,7 +191,7 @@ class ScheduleController extends GetxController { await _firestoreService.deleteSchedule(id); AppSnackbar.success('Jadwal berhasil dihapus'); } catch (e) { - AppSnackbar.error('Gagal menghapus jadwal'); + AppSnackbar.error('Gagal menghapus jadwal: ${e.toString()}'); } } @@ -196,9 +199,13 @@ class ScheduleController extends GetxController { try { final patients = await _firestoreService.getPatientsStream().first; final List> result = []; + final Set seenIds = {}; for (var patient in patients) { try { + // Skip jika ID sudah pernah ditambahkan (mencegah duplikasi) + if (seenIds.contains(patient.id)) continue; + final roomName = await _getRoomName(patient.roomId); result.add({ 'id': patient.id, @@ -206,6 +213,8 @@ class ScheduleController extends GetxController { 'room': roomName, 'device': patient.deviceId, }); + + seenIds.add(patient.id); } catch (e) { continue; } @@ -216,4 +225,4 @@ class ScheduleController extends GetxController { return []; } } -} \ No newline at end of file +} diff --git a/lib/app/modules/splash-screen/controllers/splash_screen_controller.dart b/lib/app/modules/splash-screen/controllers/splash_screen_controller.dart index 616649e..3e8dea4 100644 --- a/lib/app/modules/splash-screen/controllers/splash_screen_controller.dart +++ b/lib/app/modules/splash-screen/controllers/splash_screen_controller.dart @@ -1,9 +1,11 @@ import 'package:get/get.dart'; import 'package:firebase_auth/firebase_auth.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; import '../../../routes/app_pages.dart'; class SplashScreenController extends GetxController { final FirebaseAuth _auth = FirebaseAuth.instance; + final FirebaseFirestore _firestore = FirebaseFirestore.instance; @override void onInit() { @@ -13,16 +15,44 @@ class SplashScreenController extends GetxController { void _checkAuthStatus() async { await Future.delayed(const Duration(seconds: 4)); - - // Cek apakah user sudah login + User? user = _auth.currentUser; - + if (user != null) { - // User sudah login, langsung ke home/navbar - Get.offAllNamed(Routes.NAVBAR); + try { + final userDoc = await _firestore + .collection('users') + .doc(user.uid) + .get(); + + if (userDoc.exists) { + final userData = userDoc.data()!; + final deviceId = userData['device_id'] as String?; + final userName = userData['name'] as String? ?? 'User'; + + if (deviceId != null && deviceId.isNotEmpty) { + Get.offAllNamed( + Routes.HOME_PATIENT, + arguments: { + 'deviceId': deviceId, + 'userName': userName, + 'userEmail': user.email ?? '', + }, + ); + } else { + await _auth.signOut(); + Get.offAllNamed(Routes.LOGIN); + } + } else { + Get.offAllNamed(Routes.NAVBAR); + } + } catch (e) { + print('Error checking user data: $e'); + await _auth.signOut(); + Get.offAllNamed(Routes.LOGIN); + } } else { - // User belum login, ke halaman login Get.offAllNamed(Routes.LOGIN); } } -} \ No newline at end of file +} diff --git a/lib/app/modules/splash-screen/views/splash_screen_view.dart b/lib/app/modules/splash-screen/views/splash_screen_view.dart index 9a7d03a..f63da97 100644 --- a/lib/app/modules/splash-screen/views/splash_screen_view.dart +++ b/lib/app/modules/splash-screen/views/splash_screen_view.dart @@ -37,7 +37,7 @@ class SplashScreenView extends GetView { ), const SizedBox(height: 30), const Text( - 'Smart Infuse', + 'SMART INFUSE', style: TextStyle( color: Colors.white, fontSize: 32, diff --git a/lib/app/services/notification_service.dart b/lib/app/services/notification_service.dart index e69de29..7058d81 100644 --- a/lib/app/services/notification_service.dart +++ b/lib/app/services/notification_service.dart @@ -0,0 +1,261 @@ +// lib/app/services/notification_service.dart +import 'package:flutter/material.dart'; +import 'package:awesome_notifications/awesome_notifications.dart'; +import 'package:cloud_firestore/cloud_firestore.dart'; +import '../models/notification_model.dart' as local; + +class NotificationService { + static final NotificationService _instance = NotificationService._internal(); + factory NotificationService() => _instance; + NotificationService._internal(); + + final FirebaseFirestore _firestore = FirebaseFirestore.instance; + final Map _lastNotificationTime = {}; + final Duration _notificationCooldown = const Duration(minutes: 5); + + Future initialize() async { + await AwesomeNotifications().initialize(null, [ + NotificationChannel( + channelKey: 'infus_alert_channel', + channelName: 'Infus Alerts', + channelDescription: 'Alerts for infusion monitoring', + defaultColor: const Color(0xFF9D50DD), + ledColor: Colors.white, + importance: NotificationImportance.High, + channelShowBadge: true, + playSound: true, + enableVibration: true, + ), + ]); + + await _requestPermissions(); + _setupNotificationListeners(); + } + + Future _requestPermissions() async { + await AwesomeNotifications().isNotificationAllowed().then(( + isAllowed, + ) async { + if (!isAllowed) { + await AwesomeNotifications().requestPermissionToSendNotifications(); + } + }); + } + + void _setupNotificationListeners() { + AwesomeNotifications().setListeners( + onActionReceivedMethod: _onNotificationTapped, + onNotificationCreatedMethod: _onNotificationCreated, + onNotificationDisplayedMethod: _onNotificationDisplayed, + onDismissActionReceivedMethod: _onNotificationDismissed, + ); + } + + @pragma("vm:entry-point") + static Future _onNotificationCreated( + ReceivedNotification receivedNotification, + ) async { + print('Notification created: ${receivedNotification.id}'); + } + + @pragma("vm:entry-point") + static Future _onNotificationDisplayed( + ReceivedNotification receivedNotification, + ) async { + print('Notification displayed: ${receivedNotification.id}'); + } + + @pragma("vm:entry-point") + static Future _onNotificationTapped( + ReceivedAction receivedAction, + ) async { + print('Notification tapped: ${receivedAction.payload}'); + } + + @pragma("vm:entry-point") + static Future _onNotificationDismissed( + ReceivedAction receivedAction, + ) async { + print('Notification dismissed: ${receivedAction.id}'); + } + + Future showDropRateAlert({ + required String patientId, + required String patientName, + required String deviceId, + required String roomName, + }) async { + if (_isInCooldown(deviceId)) { + return; + } + + final title = 'Peringatan Infus ⚠️'; + final message = + 'Infus $patientName di ruang $roomName telah berhenti menetes! Segera lakukan Tindakan!'; + + await AwesomeNotifications().createNotification( + content: NotificationContent( + id: deviceId.hashCode, + channelKey: 'infus_alert_channel', + title: title, + body: message, + notificationLayout: NotificationLayout.Default, + payload: { + 'device_id': deviceId, + 'patient_id': patientId, + 'patient_name': patientName, + 'room_name': roomName, + }, + category: NotificationCategory.Alarm, + wakeUpScreen: true, + fullScreenIntent: true, + autoDismissible: false, + backgroundColor: Colors.red, + color: Colors.red, + ), + actionButtons: [ + NotificationActionButton( + key: 'VIEW', + label: 'Lihat Detail', + actionType: ActionType.Default, + ), + NotificationActionButton( + key: 'DISMISS', + label: 'Tutup', + actionType: ActionType.DismissAction, + isDangerousOption: true, + ), + ], + ); + + await _saveNotificationToFirestore( + patientId: patientId, + deviceId: deviceId, + title: title, + message: message, + ); + + _lastNotificationTime[deviceId] = DateTime.now(); + print('Notification sent for device: $deviceId'); + } + + Future showDropRateAlertForPatient({ + required String patientId, + required String patientName, + required String deviceId, + required String roomName, + }) async { + if (_isInCooldown(deviceId)) { + return; + } + + final title = 'Peringatan Infus ⚠️'; + final message = + 'Infus $patientName di ruang $roomName telah berhenti menetes! Segera hubungi Perawat!'; + + await AwesomeNotifications().createNotification( + content: NotificationContent( + id: deviceId.hashCode, + channelKey: 'infus_alert_channel', + title: title, + body: message, + notificationLayout: NotificationLayout.Default, + payload: { + 'device_id': deviceId, + 'patient_id': patientId, + 'patient_name': patientName, + 'room_name': roomName, + }, + category: NotificationCategory.Alarm, + wakeUpScreen: true, + fullScreenIntent: true, + autoDismissible: false, + backgroundColor: Colors.red, + color: Colors.red, + ), + actionButtons: [ + NotificationActionButton( + key: 'CALL', + label: 'Hubungi Perawat', + actionType: ActionType.Default, + ), + NotificationActionButton( + key: 'DISMISS', + label: 'Tutup', + actionType: ActionType.DismissAction, + isDangerousOption: true, + ), + ], + ); + + await _saveNotificationToFirestore( + patientId: patientId, + deviceId: deviceId, + title: title, + message: message, + ); + + _lastNotificationTime[deviceId] = DateTime.now(); + print('Notification sent for patient device: $deviceId'); + } + + Future _saveNotificationToFirestore({ + required String patientId, + required String deviceId, + required String title, + required String message, + }) async { + try { + final notification = local.NotificationModel( + id: '', + patientId: patientId, + deviceId: deviceId, + title: title, + message: message, + isRead: false, + createdAt: DateTime.now(), + ); + + await _firestore + .collection('notifications') + .add(notification.toFirestore()); + print('Notification saved to Firestore'); + } catch (e) { + print('Error saving notification to Firestore: $e'); + } + } + + bool _isInCooldown(String deviceId) { + if (!_lastNotificationTime.containsKey(deviceId)) { + return false; + } + + final lastTime = _lastNotificationTime[deviceId]!; + final difference = DateTime.now().difference(lastTime); + return difference < _notificationCooldown; + } + + void clearCooldown(String deviceId) { + _lastNotificationTime.remove(deviceId); + } + + void clearAllCooldowns() { + _lastNotificationTime.clear(); + } + + Future cancelNotification(int id) async { + await AwesomeNotifications().cancel(id); + } + + Future cancelNotificationByDeviceId(String deviceId) async { + await AwesomeNotifications().cancel(deviceId.hashCode); + } + + Future cancelAllNotifications() async { + await AwesomeNotifications().cancelAll(); + } + + void dispose() { + _lastNotificationTime.clear(); + } +} diff --git a/lib/app/services/realtime_database_service.dart b/lib/app/services/realtime_database_service.dart index 2252e72..6504eb8 100644 --- a/lib/app/services/realtime_database_service.dart +++ b/lib/app/services/realtime_database_service.dart @@ -9,10 +9,9 @@ class RealtimeDatabaseService { Stream> getRoomDevicesStream( String roomId, ) { - return _database - .child('realtime/rooms/$roomId/devices') - .onValue - .map((event) { + return _database.child('realtime/rooms/$roomId/devices').onValue.map(( + event, + ) { final Map devices = {}; if (event.snapshot.value != null) { @@ -40,12 +39,16 @@ class RealtimeDatabaseService { .child('realtime/rooms/$roomId/devices/$deviceId') .onValue .map((event) { - if (event.snapshot.value != null) { - final data = event.snapshot.value as Map; - return RealtimeMonitoringModel.fromRealtimeDB(deviceId, roomId, data); - } - return null; - }); + if (event.snapshot.value != null) { + final data = event.snapshot.value as Map; + return RealtimeMonitoringModel.fromRealtimeDB( + deviceId, + roomId, + data, + ); + } + return null; + }); } Stream> getAllDevicesStream() { @@ -83,8 +86,9 @@ class RealtimeDatabaseService { String deviceId, ) async { try { - final snapshot = - await _database.child('realtime/rooms/$roomId/devices/$deviceId').get(); + final snapshot = await _database + .child('realtime/rooms/$roomId/devices/$deviceId') + .get(); if (snapshot.value != null) { final data = snapshot.value as Map; @@ -102,9 +106,9 @@ class RealtimeDatabaseService { await _database .child('realtime/rooms/$roomId/devices/$deviceId/controlling') .update({ - 'servo_open': open, - 'last_command': DateTime.now().toIso8601String(), - }); + 'servo_open': open, + 'last_command': DateTime.now().toIso8601String(), + }); } Future updateDropRate( @@ -115,9 +119,9 @@ class RealtimeDatabaseService { await _database .child('realtime/rooms/$roomId/devices/$deviceId/monitoring') .update({ - 'drop_rate': dropRate, - 'last_update': DateTime.now().toIso8601String(), - }); + 'drop_rate': dropRate, + 'last_update': DateTime.now().toIso8601String(), + }); } Future updateDeviceStatus( @@ -128,9 +132,9 @@ class RealtimeDatabaseService { await _database .child('realtime/rooms/$roomId/devices/$deviceId/monitoring') .update({ - 'device_status': status, - 'last_update': DateTime.now().toIso8601String(), - }); + 'device_status': status, + 'last_update': DateTime.now().toIso8601String(), + }); } Future updateServoSettings( @@ -142,9 +146,9 @@ class RealtimeDatabaseService { await _database .child('realtime/rooms/$roomId/devices/$deviceId/settings') .update({ - 'servo_angle_open': angleOpen, - 'servo_angle_close': angleClose, - }); + 'servo_angle_open': angleOpen, + 'servo_angle_close': angleClose, + }); } // ==================== UTILITY ==================== @@ -158,11 +162,25 @@ class RealtimeDatabaseService { } } + Future controlBuzzer( + String roomId, + String deviceId, + bool active, + ) async { + await _database + .child('realtime/rooms/$roomId/devices/$deviceId/controlling') + .update({ + 'buzzer_active': active, + 'last_command': DateTime.now().toIso8601String(), + }); + } + Future getDeviceDropRate(String roomId, String deviceId) async { try { final snapshot = await _database .child( - 'realtime/rooms/$roomId/devices/$deviceId/monitoring/drop_rate') + 'realtime/rooms/$roomId/devices/$deviceId/monitoring/drop_rate', + ) .get(); if (snapshot.value != null) { @@ -173,4 +191,4 @@ class RealtimeDatabaseService { return 0.0; } } -} \ No newline at end of file +} diff --git a/lib/app/widgets/history_grafik_card.dart b/lib/app/widgets/history_grafik_card.dart index 0881a70..f0cf244 100644 --- a/lib/app/widgets/history_grafik_card.dart +++ b/lib/app/widgets/history_grafik_card.dart @@ -37,11 +37,10 @@ class HistoryGrafikCard extends StatelessWidget { ); } - final hours = dataPoints.map((p) => p.hour.toDouble()).toList(); final drops = dataPoints.map((p) => p.dropsPerMinute.toDouble()).toList(); - final minX = hours.reduce((a, b) => a < b ? a : b); - final maxX = hours.reduce((a, b) => a > b ? a : b); + final minX = 0.0; + final maxX = (dataPoints.length - 1).toDouble(); final minY = (drops.reduce((a, b) => a < b ? a : b) - 5.0).clamp( 0.0, double.infinity, @@ -113,10 +112,14 @@ class HistoryGrafikCard extends StatelessWidget { reservedSize: 30, interval: dataPoints.length > 5 ? 2.0 : 1.0, getTitlesWidget: (double value, TitleMeta meta) { + final index = value.toInt(); + if (index < 0 || index >= dataPoints.length) { + return const SizedBox.shrink(); + } return Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( - '${value.toInt()}:00', + dataPoints[index].timeLabel, style: TextStyle( color: Colors.grey[600], fontWeight: FontWeight.w500, @@ -158,14 +161,12 @@ class HistoryGrafikCard extends StatelessWidget { maxY: maxY, lineBarsData: [ LineChartBarData( - spots: dataPoints - .map( - (point) => FlSpot( - point.hour.toDouble(), - point.dropsPerMinute.toDouble(), - ), - ) - .toList(), + spots: List.generate(dataPoints.length, (index) { + return FlSpot( + index.toDouble(), + dataPoints[index].dropsPerMinute.toDouble(), + ); + }), isCurved: true, gradient: const LinearGradient( colors: [Color(0xFF2196F3), Color(0xFF1976D2)], @@ -208,8 +209,12 @@ class HistoryGrafikCard extends StatelessWidget { tooltipMargin: 8, getTooltipItems: (List touchedBarSpots) { return touchedBarSpots.map((barSpot) { + final index = barSpot.x.toInt(); + final label = (index >= 0 && index < dataPoints.length) + ? dataPoints[index].timeLabel + : ''; return LineTooltipItem( - '${barSpot.y.toInt()} tpm\n${barSpot.x.toInt()}:00', + '${barSpot.y.toInt()} tpm\n$label', const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, diff --git a/lib/app/widgets/modal_schedule_add_edit.dart b/lib/app/widgets/modal_schedule_add_edit.dart index 9078a36..6df73a9 100644 --- a/lib/app/widgets/modal_schedule_add_edit.dart +++ b/lib/app/widgets/modal_schedule_add_edit.dart @@ -30,13 +30,17 @@ class _ModalScheduleAddEditState extends State { @override void initState() { super.initState(); - _medicineController = TextEditingController(text: widget.schedule?.medicineTime ?? ''); - _fluidController = TextEditingController(text: widget.schedule?.fluidTime ?? ''); + _medicineController = TextEditingController( + text: widget.schedule?.medicineTime ?? '', + ); + _fluidController = TextEditingController( + text: widget.schedule?.fluidTime ?? '', + ); _selectedDate = DateTime.now(); if (widget.isEdit && widget.schedule != null) { - _selectedPatientId = widget.schedule!.id; _selectedTime = widget.schedule!.timeOfDay; + // _selectedPatientId akan diset setelah _loadPatients() selesai } _loadPatients(); @@ -46,11 +50,16 @@ class _ModalScheduleAddEditState extends State { try { final controller = Get.find(); final patientsList = await controller.getPatientsList(); - + if (mounted) { setState(() { _patients = patientsList; _isLoadingPatients = false; + + // Set selected patient ID untuk mode edit + if (widget.isEdit && widget.schedule != null) { + _selectedPatientId = widget.schedule!.patientId; + } }); } } catch (e) { @@ -101,7 +110,9 @@ class _ModalScheduleAddEditState extends State { ? const Center( child: Padding( padding: EdgeInsets.all(40), - child: CircularProgressIndicator(color: Color(0xFF0091EA)), + child: CircularProgressIndicator( + color: Color(0xFF0091EA), + ), ), ) : SingleChildScrollView( @@ -140,7 +151,10 @@ class _ModalScheduleAddEditState extends State { }, ), const SizedBox(height: 20), - _buildLabel('Detail Cairan & Tetes', Icons.water_drop), + _buildLabel( + 'Detail Cairan & Tetes', + Icons.water_drop, + ), const SizedBox(height: 8), _buildTextField( controller: _fluidController, @@ -168,7 +182,9 @@ class _ModalScheduleAddEditState extends State { return Container( padding: const EdgeInsets.all(24), decoration: const BoxDecoration( - gradient: LinearGradient(colors: [Color(0xFF0091EA), Color(0xFF0277BD)]), + gradient: LinearGradient( + colors: [Color(0xFF0091EA), Color(0xFF0277BD)], + ), borderRadius: BorderRadius.only( topLeft: Radius.circular(24), topRight: Radius.circular(24), @@ -182,7 +198,11 @@ class _ModalScheduleAddEditState extends State { color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), - child: const Icon(Icons.calendar_today, color: Colors.white, size: 24), + child: const Icon( + Icons.calendar_today, + color: Colors.white, + size: 24, + ), ), const SizedBox(width: 16), Expanded( @@ -258,16 +278,25 @@ class _ModalScheduleAddEditState extends State { ), child: Row( children: [ - const Icon(Icons.calendar_today, size: 18, color: Color(0xFF0091EA)), + const Icon( + Icons.calendar_today, + size: 18, + color: Color(0xFF0091EA), + ), const SizedBox(width: 12), Text( _selectedDate != null - ? DateFormat('EEEE, dd MMMM yyyy', 'id_ID').format(_selectedDate!) + ? DateFormat( + 'EEEE, dd MMMM yyyy', + 'id_ID', + ).format(_selectedDate!) : 'Pilih tanggal...', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: _selectedDate != null ? const Color(0xFF424242) : Colors.grey[400], + color: _selectedDate != null + ? const Color(0xFF424242) + : Colors.grey[400], ), ), const Spacer(), @@ -296,6 +325,7 @@ class _ModalScheduleAddEditState extends State { return DropdownButtonFormField( value: _selectedPatientId, + dropdownColor: Colors.white, decoration: InputDecoration( hintText: 'Cari dan pilih pasien...', hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14), @@ -317,7 +347,10 @@ class _ModalScheduleAddEditState extends State { borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.redAccent), ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), ), icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)), isExpanded: true, @@ -334,11 +367,13 @@ class _ModalScheduleAddEditState extends State { ), ); }).toList(), - onChanged: (value) { - setState(() { - _selectedPatientId = value; - }); - }, + onChanged: widget.isEdit + ? null + : (value) { + setState(() { + _selectedPatientId = value; + }); + }, validator: (value) { if (value == null) { return 'Pilih pasien terlebih dahulu'; @@ -388,7 +423,10 @@ class _ModalScheduleAddEditState extends State { Row( children: [ Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), decoration: BoxDecoration( color: const Color(0xFFE3F2FD), borderRadius: BorderRadius.circular(6), @@ -404,7 +442,10 @@ class _ModalScheduleAddEditState extends State { ), const SizedBox(width: 8), Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(6), @@ -445,6 +486,7 @@ class _ModalScheduleAddEditState extends State { Widget _buildTimeDropdown() { return DropdownButtonFormField( value: _selectedTime, + dropdownColor: Colors.white, decoration: InputDecoration( hintText: 'Pilih waktu...', hintStyle: TextStyle(color: Colors.grey[400], fontSize: 14), @@ -466,7 +508,10 @@ class _ModalScheduleAddEditState extends State { borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.redAccent), ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), ), icon: const Icon(Icons.arrow_drop_down, color: Color(0xFF0091EA)), isExpanded: true, @@ -561,7 +606,10 @@ class _ModalScheduleAddEditState extends State { borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.redAccent, width: 2), ), - contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 14, + ), ), ); } @@ -584,7 +632,9 @@ class _ModalScheduleAddEditState extends State { style: OutlinedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 16), side: BorderSide(color: Colors.grey.shade300, width: 2), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), child: Text( 'Batal', @@ -618,7 +668,9 @@ class _ModalScheduleAddEditState extends State { backgroundColor: Colors.transparent, shadowColor: Colors.transparent, padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), ), child: Text( widget.isEdit ? 'Simpan' : 'Tambah', @@ -649,19 +701,25 @@ class _ModalScheduleAddEditState extends State { } final controller = Get.find(); + final patientData = _selectedPatientData!; final schedule = PatientSchedule( - id: _selectedPatientId!, - name: _selectedPatientData!['name']!, - room: _selectedPatientData!['room']!, - deviceId: _selectedPatientData!['device']!, + id: widget.isEdit ? widget.schedule!.id : '', + patientId: _selectedPatientId!, + name: patientData['name']!, + room: patientData['room']!, + deviceId: patientData['device']!, medicineTime: _medicineController.text.trim(), fluidTime: _fluidController.text.trim(), timeOfDay: _selectedTime!, ); if (widget.isEdit && widget.schedule != null) { - controller.updateSchedule(widget.schedule!.id, schedule, _selectedDate!); + controller.updateSchedule( + widget.schedule!.id, + schedule, + _selectedDate!, + ); } else { controller.addSchedule(schedule, _selectedDate!); } @@ -669,4 +727,4 @@ class _ModalScheduleAddEditState extends State { Get.back(); } } -} \ No newline at end of file +} diff --git a/lib/app/widgets/monitoring_card.dart b/lib/app/widgets/monitoring_card.dart index 0dd9e62..16121e4 100644 --- a/lib/app/widgets/monitoring_card.dart +++ b/lib/app/widgets/monitoring_card.dart @@ -5,11 +5,7 @@ class MonitoringCard extends StatelessWidget { final InfusMonitoring monitoring; final VoidCallback? onTap; - const MonitoringCard({ - super.key, - required this.monitoring, - this.onTap, - }); + const MonitoringCard({super.key, required this.monitoring, this.onTap}); @override Widget build(BuildContext context) { @@ -158,11 +154,7 @@ class MonitoringCard extends StatelessWidget { const Color(0xFFE3F2FD), const Color(0xFF2196F3), ), - _buildBadge( - monitoring.room, - Colors.grey[100]!, - Colors.grey[700]!, - ), + _buildBadge(monitoring.room, Colors.grey[100]!, Colors.grey[700]!), _buildBadge( isConnected ? 'Online' : 'Offline', isConnected ? const Color(0xFFE8F5E9) : Colors.red[50]!, @@ -196,4 +188,4 @@ class MonitoringCard extends StatelessWidget { if (diff.inHours < 24) return '${diff.inHours}h ago'; return '${diff.inDays}d ago'; } -} \ No newline at end of file +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f6f23bf..4c2edbf 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,9 +6,13 @@ #include "generated_plugin_registrant.h" +#include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) awesome_notifications_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "AwesomeNotificationsPlugin"); + awesome_notifications_plugin_register_with_registrar(awesome_notifications_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f16b4c3..1845e78 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + awesome_notifications url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 21dd2cc..61ac1ff 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import awesome_notifications import cloud_firestore import firebase_auth import firebase_core @@ -13,6 +14,7 @@ import path_provider_foundation import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + AwesomeNotificationsPlugin.register(with: registry.registrar(forPlugin: "AwesomeNotificationsPlugin")) FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) diff --git a/pubspec.lock b/pubspec.lock index 9ccebb0..868e390 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.0" + awesome_notifications: + dependency: "direct main" + description: + name: awesome_notifications + sha256: "0d5fa4457f2ba4e536adc3ef6af709cdcecf4a05a1f3035981e9afa2f899b2a8" + url: "https://pub.dev" + source: hosted + version: "0.10.1" boolean_selector: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0a25aaf..d17e8ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,7 @@ dependencies: firebase_auth: ^6.1.4 firebase_database: ^12.1.2 cloud_firestore: ^6.1.2 + awesome_notifications: ^0.10.1 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index a3c4d16..0922b2d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + AwesomeNotificationsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("AwesomeNotificationsPluginCApi")); CloudFirestorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); FirebaseAuthPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index c215e65..43e6e66 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + awesome_notifications cloud_firestore firebase_auth firebase_core