From 5ab72678a3d87d590462a39f34b1a9767f941423 Mon Sep 17 00:00:00 2001 From: Alex Ardhian Date: Wed, 2 Sep 2026 14:48:32 +0700 Subject: [PATCH] Gabung app Android Flutter 'Saat Invest' ke repo saat-app (folder flutter/) Satu repo untuk backend Odoo (addon saat_invest) + app Android. - flutter/ berisi project Flutter (lib/, android/, pubspec, test) - Login Odoo JSON-RPC, tab Portofolio+Equity, tab Transaksi, catat jual/beli otomatis sync - Build .gitignore nested dipakai sehingga artefak build/.dart_tool tak ter-track Sejarah: gabung flat (satu sejarah backend+app). --- flutter/.gitignore | 45 ++ flutter/.metadata | 30 ++ flutter/README.md | 17 + flutter/analysis_options.yaml | 28 + flutter/android/.gitignore | 14 + flutter/android/app/build.gradle.kts | 46 ++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 47 ++ .../saat_invest_mobile/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + flutter/android/build.gradle.kts | 24 + flutter/android/gradle.properties | 6 + .../gradle/wrapper/gradle-wrapper.properties | 5 + flutter/android/settings.gradle.kts | 26 + flutter/lib/add_transaksi_screen.dart | 126 +++++ flutter/lib/api/odoo_client.dart | 77 +++ flutter/lib/home_screen.dart | 299 +++++++++++ flutter/lib/main.dart | 179 +++++++ flutter/lib/models/portfolio.dart | 41 ++ flutter/lib/models/saham_transaksi.dart | 63 +++ flutter/lib/services/session.dart | 35 ++ flutter/lib/services/stock_service.dart | 83 +++ flutter/pubspec.lock | 506 ++++++++++++++++++ flutter/pubspec.yaml | 92 ++++ flutter/test/widget_test.dart | 13 + 34 files changed, 1881 insertions(+) create mode 100644 flutter/.gitignore create mode 100644 flutter/.metadata create mode 100644 flutter/README.md create mode 100644 flutter/analysis_options.yaml create mode 100644 flutter/android/.gitignore create mode 100644 flutter/android/app/build.gradle.kts create mode 100644 flutter/android/app/src/debug/AndroidManifest.xml create mode 100644 flutter/android/app/src/main/AndroidManifest.xml create mode 100644 flutter/android/app/src/main/kotlin/com/kosbarokah/saat_invest_mobile/MainActivity.kt create mode 100644 flutter/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 flutter/android/app/src/main/res/drawable/launch_background.xml create mode 100644 flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 flutter/android/app/src/main/res/values-night/styles.xml create mode 100644 flutter/android/app/src/main/res/values/styles.xml create mode 100644 flutter/android/app/src/profile/AndroidManifest.xml create mode 100644 flutter/android/build.gradle.kts create mode 100644 flutter/android/gradle.properties create mode 100644 flutter/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 flutter/android/settings.gradle.kts create mode 100644 flutter/lib/add_transaksi_screen.dart create mode 100644 flutter/lib/api/odoo_client.dart create mode 100644 flutter/lib/home_screen.dart create mode 100644 flutter/lib/main.dart create mode 100644 flutter/lib/models/portfolio.dart create mode 100644 flutter/lib/models/saham_transaksi.dart create mode 100644 flutter/lib/services/session.dart create mode 100644 flutter/lib/services/stock_service.dart create mode 100644 flutter/pubspec.lock create mode 100644 flutter/pubspec.yaml create mode 100644 flutter/test/widget_test.dart diff --git a/flutter/.gitignore b/flutter/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/flutter/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/flutter/.metadata b/flutter/.metadata new file mode 100644 index 0000000..cc161b7 --- /dev/null +++ b/flutter/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "6b182d2c7585eba26d4edce0f97630effd256c33" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + - platform: android + create_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + base_revision: 6b182d2c7585eba26d4edce0f97630effd256c33 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/flutter/README.md b/flutter/README.md new file mode 100644 index 0000000..a25e6e5 --- /dev/null +++ b/flutter/README.md @@ -0,0 +1,17 @@ +# saat_invest_mobile + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter) +- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/flutter/analysis_options.yaml b/flutter/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/flutter/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/flutter/android/.gitignore b/flutter/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/flutter/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/flutter/android/app/build.gradle.kts b/flutter/android/app/build.gradle.kts new file mode 100644 index 0000000..4347eba --- /dev/null +++ b/flutter/android/app/build.gradle.kts @@ -0,0 +1,46 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.kosbarokah.saat_invest_mobile" + compileSdk = 37 // flutter.compileSdkVersion ditimpa: lib flutter_secure_storage v11 butuh API 37 + // compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.kosbarokah.saat_invest_mobile" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/flutter/android/app/src/debug/AndroidManifest.xml b/flutter/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/flutter/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/flutter/android/app/src/main/AndroidManifest.xml b/flutter/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..47b69ed --- /dev/null +++ b/flutter/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/flutter/android/app/src/main/kotlin/com/kosbarokah/saat_invest_mobile/MainActivity.kt b/flutter/android/app/src/main/kotlin/com/kosbarokah/saat_invest_mobile/MainActivity.kt new file mode 100644 index 0000000..24b03b3 --- /dev/null +++ b/flutter/android/app/src/main/kotlin/com/kosbarokah/saat_invest_mobile/MainActivity.kt @@ -0,0 +1,5 @@ +package com.kosbarokah.saat_invest_mobile + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/flutter/android/app/src/main/res/drawable-v21/launch_background.xml b/flutter/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/flutter/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/flutter/android/app/src/main/res/drawable/launch_background.xml b/flutter/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/flutter/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/flutter/android/app/src/main/res/values-night/styles.xml b/flutter/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/flutter/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/flutter/android/app/src/main/res/values/styles.xml b/flutter/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/flutter/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/flutter/android/app/src/profile/AndroidManifest.xml b/flutter/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/flutter/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/flutter/android/build.gradle.kts b/flutter/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/flutter/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/flutter/android/gradle.properties b/flutter/android/gradle.properties new file mode 100644 index 0000000..e96108c --- /dev/null +++ b/flutter/android/gradle.properties @@ -0,0 +1,6 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/flutter/android/gradle/wrapper/gradle-wrapper.properties b/flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/flutter/android/settings.gradle.kts b/flutter/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/flutter/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/flutter/lib/add_transaksi_screen.dart b/flutter/lib/add_transaksi_screen.dart new file mode 100644 index 0000000..da58cff --- /dev/null +++ b/flutter/lib/add_transaksi_screen.dart @@ -0,0 +1,126 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +import '../models/saham_transaksi.dart'; +import '../services/stock_service.dart'; + +class AddTransaksiScreen extends StatefulWidget { + const AddTransaksiScreen({super.key, required this.service}); + final StockService service; + + @override + State createState() => _AddTransaksiScreenState(); +} + +class _AddTransaksiScreenState extends State { + final _tanggal = TextEditingController( + text: DateFormat('yyyy-MM-dd').format(DateTime.now())); + final _volume = TextEditingController(); + final _harga = TextEditingController(); + final _fee = TextEditingController(text: '0'); + + late Future> _sahamFut; + int? _sahamId; + bool _isBuy = true; + bool _busy = false; + String? _err; + + @override + void initState() { + super.initState(); + _sahamFut = widget.service.listSaham(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Catat Transaksi')), + body: Padding( + padding: const EdgeInsets.all(16), + child: ListView( + children: [ + SegmentedButton( + segments: const [ + ButtonSegment(value: true, label: Text('Beli'), icon: Icon(Icons.add)), + ButtonSegment(value: false, label: Text('Jual'), icon: Icon(Icons.remove)), + ], + selected: {_isBuy}, + onSelectionChanged: (s) => setState(() => _isBuy = s.first), + ), + const SizedBox(height: 16), + TextField(controller: _tanggal, + decoration: const InputDecoration(labelText: 'Tanggal (YYYY-MM-DD)', + prefixIcon: Icon(Icons.calendar_today))), + const SizedBox(height: 12), + FutureBuilder>( + future: _sahamFut, + builder: (context, snap) { + if (snap.connectionState != ConnectionState.done) { + return const ListTile( + leading: SizedBox(width:18,height:18,child:CircularProgressIndicator(strokeWidth:2)), + title: Text('Memuat saham...')); + } + if (snap.hasError) { + return Text('Error saham: ${snap.error}', style: const TextStyle(color: Colors.red)); + } + final saham = snap.data ?? []; + return DropdownButtonFormField( + initialValue: _sahamId, + decoration: const InputDecoration(labelText: 'Pilih Saham', + prefixIcon: Icon(Icons.business)), + items: [ + for (final s in saham) + DropdownMenuItem(value: s.id, child: Text('${s.kode} — ${s.perusahaan ?? ''}')), + ], + onChanged: (v) => setState(() => _sahamId = v), + ); + }, + ), + const SizedBox(height: 12), + TextField(controller: _volume, keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: 'Jumlah Lembar', prefixIcon: Icon(Icons.numbers))), + const SizedBox(height: 12), + TextField(controller: _harga, keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration(labelText: 'Harga per Lembar (Rp)', prefixIcon: Icon(Icons.attach_money))), + const SizedBox(height: 12), + TextField(controller: _fee, keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration(labelText: 'Biaya / Fee (Rp)', prefixIcon: Icon(Icons.payments))), + if (_err != null) + Padding(padding: const EdgeInsets.only(top: 8), + child: Text(_err!, style: const TextStyle(color: Colors.red))), + const SizedBox(height: 20), + FilledButton.icon( + onPressed: _busy ? null : _save, + icon: _busy ? const SizedBox(width:18,height:18,child:CircularProgressIndicator(strokeWidth:2)) + : const Icon(Icons.save), + label: const Text('Simpan'), + ), + ], + ), + ), + ); + } + + Future _save() async { + setState(() { _busy = true; _err = null; }); + try { + final date = _tanggal.text.trim(); + final vol = int.tryParse(_volume.text.trim()) ?? 0; + final harga = double.tryParse(_harga.text.replaceAll('.', '').replaceAll(',', '.')) ?? 0; + final feeC = double.tryParse(_fee.text.trim().isEmpty ? '0' : _fee.text.trim()) ?? 0; + if (_sahamId == null) throw Exception('Pilih saham dulu'); + if (vol <= 0) throw Exception('Volume harus > 0'); + if (harga <= 0) throw Exception('Harga harus > 0'); + if (!date.contains('-')) throw Exception('Format tanggal salah'); + + await widget.service.addTransaksi( + sahamId: _sahamId!, tipe: _isBuy ? 'buy' : 'sell', + volume: vol, harga: harga, fee: feeC, tanggal: date); + if (!mounted) return; + Navigator.pop(context, true); + } catch (e) { + if (!mounted) return; + setState(() { _busy = false; _err = e.toString().replaceFirst('Exception: ', ''); }); + } + } +} diff --git a/flutter/lib/api/odoo_client.dart b/flutter/lib/api/odoo_client.dart new file mode 100644 index 0000000..bef1abd --- /dev/null +++ b/flutter/lib/api/odoo_client.dart @@ -0,0 +1,77 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +/// Klien JSON-RPC utk Odoo 17. Authenticate + execute_kw. +class OdooClient { + OdooClient({required this.baseUrl, required this.db, required this.username, required this.password}) + : _http = http.Client(); + + final String baseUrl; // mis: http://192.168.x.x:18069 + final String db; // nama db, default 'odoo' + final String username; + final String password; + + final http.Client _http; + int? _uid; + + Uri get _endpoint => Uri.parse('$baseUrl/jsonrpc'); + + Future authenticate() async { + final result = await _call('common', 'authenticate', [db, username, password, {}]); + if (result is int && result > 0) { + _uid = result; + return result; + } + throw Exception('Login gagal: periksa kredensial / URL Odoo'); + } + + Future executeKwd(String model, String method, + {List args = const [], Map? kwargs}) async { + _uid ??= await authenticate(); + final params = [ + db, + _uid, + password, + model, + method, + args, + if (kwargs != null) kwargs, + ]; + return _call('object', 'execute_kw', params); + } + + Future _call(String service, String method, List args) async { + final body = { + 'jsonrpc': '2.0', + 'method': 'call', + 'params': { + 'service': service, + 'method': method, + 'args': args, + }, + }; + try { + final resp = await _http + .post(_endpoint, + headers: {'Content-Type': 'application/json'}, body: jsonEncode(body)) + .timeout(const Duration(seconds: 25)); + final json = jsonDecode(resp.body) as Map; + if (json.containsKey('error')) { + final err = json['error'] as Map?; + String? message = (err?['data'] as Map?)?['message'] as String? ?? + (err?['message'] as String?) ?? + 'Unknown Odoo error'; + throw Exception('Odoo error: $message'); + } + return json['result']; + } on TimeoutException { + throw Exception('Timeout — pastikan server Odoo:18069 tercapai'); + } on http.ClientException catch (e) { + throw Exception('Koneksi gagal: ${e.message}'); + } + } + + void close() => _http.close(); +} diff --git a/flutter/lib/home_screen.dart b/flutter/lib/home_screen.dart new file mode 100644 index 0000000..98e8bc1 --- /dev/null +++ b/flutter/lib/home_screen.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; + +import '../api/odoo_client.dart'; +import '../models/portfolio.dart'; +import '../models/saham_transaksi.dart'; +import '../services/stock_service.dart'; +import 'add_transaksi_screen.dart'; + +class HomeScreen extends StatelessWidget { + const HomeScreen({ + super.key, + required this.baseUrl, + required this.db, + required this.user, + required this.pass, + required this.onLogout, + }); + + final String baseUrl; + final String db; + final String user; + final String pass; + final VoidCallback onLogout; + + @override + Widget build(BuildContext context) { + final client = OdooClient(baseUrl: baseUrl, db: db, username: user, password: pass); + final service = StockService(client); + return _HomeShell( + service: service, + client: client, + title: 'Portofolio', + onLogout: onLogout, + ); + } +} + +class _HomeShell extends StatefulWidget { + const _HomeShell({required this.service, required this.client, required this.title, required this.onLogout}); + final StockService service; + final OdooClient client; + final String title; + final VoidCallback onLogout; + + @override + State<_HomeShell> createState() => _HomeShellState(); +} + +class _HomeShellState extends State<_HomeShell> { + int _tab = 0; + int _reloadTick = 0; + + void _reload() => setState(() => _reloadTick++); + + Future _openAdd() async { + final added = await Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => AddTransaksiScreen(service: widget.service), + ), + ); + if (added == true) _reload(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Saat Invest'), + actions: [ + IconButton(icon: const Icon(Icons.refresh), tooltip: 'Muat ulang', + onPressed: _reload), + IconButton(icon: const Icon(Icons.logout), tooltip: 'Keluar', onPressed: widget.onLogout), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _openAdd, + tooltip: 'Catat transaksi baru', + child: const Icon(Icons.add), + ), + body: IndexedStack( + index: _tab, + children: [ + _PortfolioTab(key: ValueKey('pf$_reloadTick'), service: widget.service), + _TransaksiTab(key: ValueKey('tx$_reloadTick'), service: widget.service), + ], + ), + bottomNavigationBar: NavigationBar( + selectedIndex: _tab, + onDestinationSelected: (i) => setState(() => _tab = i), + destinations: const [ + NavigationDestination(icon: Icon(Icons.pie_chart), label: 'Portofolio'), + NavigationDestination(icon: Icon(Icons.receipt_long), label: 'Transaksi'), + ], + ), + ); + } +} + +// ---------------- Portofolio / Equity tab ---------------- + +class _PortfolioTab extends StatefulWidget { + const _PortfolioTab({super.key, required this.service}); + final StockService service; + @override + State<_PortfolioTab> createState() => _PortfolioTabState(); +} + +class _PortfolioTabState extends State<_PortfolioTab> { + late Future> _fut; + + @override + void initState() { + super.initState(); + _fut = widget.service.listPortofolio(); + } + + void _reload() => setState(() => _fut = widget.service.listPortofolio()); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _fut, + builder: (context, snap) { + if (snap.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snap.hasError) { + return _ErrorRetry(message: '${snap.error}', onRetry: _reload); + } + final list = snap.data ?? []; + final totalNilai = list.fold( + 0, (a, p) => a + p.nilaiSekarang); + final totalModal = list.fold(0, (a, p) => a + p.avgCost * p.sisaLembar); + final totalPl = list.fold(0, (a, p) => a + p.unrealizedPl + p.realizedPl); + return RefreshIndicator( + onRefresh: () async => _reload(), + child: ListView( + children: [ + _EquityCard(equity: totalNilai, modal: totalModal, pl: totalPl), + if (list.isEmpty) + const Padding( + padding: EdgeInsets.all(32), + child: Text('Belum ada posisi.\nCatat transaksi beli dulu di tab Transaksi.', + textAlign: TextAlign.center), + ), + ...list.map((p) => Card( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + child: ListTile( + title: Text(p.sahamTicker, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: Text('${p.sisaLembar} lbr · avg ${_fmt(p.avgCost)} · hrga ${_fmt(p.hargaSekarang)}'), + trailing: Column( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_fmt(p.nilaiSekarang)), + Text(_rm(p.unrealizedPl), style: TextStyle( + color: p.unrealizedPl >= 0 ? Colors.green : Colors.red)), + ], + ), + ), + )), + const SizedBox(height: 16), + ], + ), + ); + }, + ); + } +} + +class _EquityCard extends StatelessWidget { + const _EquityCard({required this.equity, required this.modal, required this.pl}); + final double equity; + final double modal; + final double pl; + + @override + Widget build(BuildContext context) { + final pct = modal > 0 ? (pl / modal) * 100 : 0.0; + return Card( + margin: const EdgeInsets.all(12), + color: const Color(0xFF0B6E4F), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Total Equity', style: TextStyle(color: Colors.white70, fontSize: 14)), + Text('Rp ${_fmt(equity, force: true)}', + style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold)), + const SizedBox(height: 8), + Text('Modal dikeluarkan: ${_fmt(modal)}', + style: const TextStyle(color: Colors.white70)), + Row(children: [ + Text('P/L: ', style: const TextStyle(color: Colors.white70)), + Text(_rm(pl), + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600)), + const SizedBox(width: 8), + Text('(${pct.toStringAsFixed(2)}%)', style: const TextStyle(color: Colors.white70)), + ]), + ]), + ), + ); + } +} + +// ---------------- Transaksi tab ---------------- + +class _TransaksiTab extends StatefulWidget { + const _TransaksiTab({super.key, required this.service}); + final StockService service; + @override + State<_TransaksiTab> createState() => _TransaksiTabState(); +} + +class _TransaksiTabState extends State<_TransaksiTab> { + late Future> _fut; + + @override + void initState() { + super.initState(); + _fut = widget.service.listTransaksi(); + } + + void _reload() => setState(() => _fut = widget.service.listTransaksi()); + + @override + Widget build(BuildContext context) { + return FutureBuilder>( + future: _fut, + builder: (context, snap) { + if (snap.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + if (snap.hasError) { + return _ErrorRetry(message: '${snap.error}', onRetry: _reload); + } + final list = snap.data ?? []; + return Column(children: [ + Expanded( + child: list.isEmpty + ? const Center(child: Text('Belum ada transaksi.')) + : RefreshIndicator( + onRefresh: () async => _reload(), + child: ListView.builder( + itemCount: list.length, + itemBuilder: (c, i) { + final t = list[i]; + final buy = t.isBuy; + return ListTile( + leading: CircleAvatar( + backgroundColor: buy ? Colors.green : Colors.red, + child: Text(buy ? 'B' : 'J', style: const TextStyle(color: Colors.white)), + ), + title: Text('${t.sahamTicker} · ${t.volume} lbr'), + subtitle: Text('${t.tanggal} @ ${_fmt(t.harga)} · fee ${_fmt(t.fee)}'), + trailing: Text(_fmt(t.total)), + ); + }, + ), + ), + ), + ]); + }, + ); + } +} + +class _ErrorRetry extends StatelessWidget { + const _ErrorRetry({required this.message, required this.onRetry}); + final String message; + final VoidCallback onRetry; + @override + Widget build(BuildContext context) => Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.error_outline, color: Colors.red, size: 40), + const SizedBox(height: 8), + Text(message, textAlign: TextAlign.center), + const SizedBox(height: 12), + OutlinedButton(onPressed: onRetry, child: const Text('Coba lagi')), + ]), + ), + ); +} + +// helpers +String _fmt(double v, {bool force = false}) { + final fmt = force + ? v.toStringAsFixed(0).replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.') + : v.toStringAsFixed(0).replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.'); + return fmt; +} + +String _rm(double v) => + '${v >= 0 ? '+' : ''}Rp ${_formatAbs(v)}'; +String _formatAbs(double v) { + final a = v.abs().toStringAsFixed(0); + return a.replaceAllMapped(RegExp(r'\B(?=(\d{3})+(?!\d))'), (m) => '.'); +} diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart new file mode 100644 index 0000000..f6fd4c6 --- /dev/null +++ b/flutter/lib/main.dart @@ -0,0 +1,179 @@ +import 'package:flutter/material.dart'; + +import '../api/odoo_client.dart'; +import '../services/session.dart'; +import 'home_screen.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + runApp(const SaatInvestApp()); +} + +class SaatInvestApp extends StatelessWidget { + const SaatInvestApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Saat Invest', + debugShowCheckedModeBanner: false, + theme: ThemeData(colorSchemeSeed: const Color(0xFF0B6E4F), useMaterial3: true), + home: const RootScreen(), + ); + } +} + +/// Jika session tersimpan -> Home, jika tidak -> Login. +class RootScreen extends StatefulWidget { + const RootScreen({super.key}); + + @override + State createState() => _RootScreenState(); +} + +class _RootScreenState extends State { + late Future<({String base, String db, String user, String pass})?> _check; + + @override + void initState() { + super.initState(); + _check = Session.load(); + } + + void _onLoginDone({required String base, required String db, required String user, required String pass}) { + setState(() { + _check = Future.value( + (base: base, db: db, user: user, pass: pass), + ); + }); + } + + Future _logout() async { + await Session.clear(); + setState(() => _check = Session.load()); + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _check, + builder: (context, snap) { + if (snap.connectionState != ConnectionState.done) { + return const Scaffold(body: Center(child: CircularProgressIndicator())); + } + final s = snap.data; + if (s == null) { + return LoginScreen(onSuccess: _onLoginDone); + } + return HomeScreen( + baseUrl: s.base, + db: s.db, + user: s.user, + pass: s.pass, + onLogout: _logout, + ); + }, + ); + } +} + +// --------------------------------------------------------------------------- + +class LoginScreen extends StatefulWidget { + const LoginScreen({super.key, required this.onSuccess}); + + final void Function({required String base, required String db, required String user, required String pass}) onSuccess; + + @override + State createState() => _LoginScreenState(); +} + +class _LoginScreenState extends State { + final _url = TextEditingController(); // mis http://:18069 + final _db = TextEditingController(text: 'odoo'); + final _user = TextEditingController(text: 'admin'); + final _pass = TextEditingController(); + bool _busy = false; + String? _error; + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: const Color(0xFF0B6E4F), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Icon(Icons.trending_up, color: Color(0xFF0B6E4F), size: 40), + const SizedBox(height: 8), + Text('Saat Invest', textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineSmall), + Text('Catat Jual-Beli Saham via Odoo', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall), + const SizedBox(height: 20), + TextField(controller: _url, + decoration: const InputDecoration(labelText: 'URL Server Odoo', + hintText: 'http://192.168.1.5:18069', + prefixIcon: Icon(Icons.dns))), + const SizedBox(height: 12), + TextField(controller: _db, + decoration: const InputDecoration(labelText: 'Database', prefixIcon: Icon(Icons.storage))), + const SizedBox(height: 12), + TextField(controller: _user, + decoration: const InputDecoration(labelText: 'Username', prefixIcon: Icon(Icons.person))), + const SizedBox(height: 12), + TextField(controller: _pass, obscureText: true, + decoration: const InputDecoration(labelText: 'Password', prefixIcon: Icon(Icons.lock))), + if (_error != null) ...[ + const SizedBox(height: 12), + Text(_error!, style: const TextStyle(color: Colors.red)), + ], + const SizedBox(height: 20), + FilledButton.icon( + onPressed: _busy ? null : _login, + icon: _busy + ? const SizedBox(width: 18, height: 18, + child: CircularProgressIndicator(strokeWidth: 2)) + : const Icon(Icons.login), + label: const Text('Masuk'), + ), + ], + ), + ), + ), + ), + ), + ); + } + + Future _login() async { + setState(() { _busy = true; _error = null; }); + String base = _url.text.trim(); + if (base.isNotEmpty && !base.startsWith('http')) base = 'http://$base'; + try { + final c = OdooClient( + baseUrl: base, db: _db.text.trim(), username: _user.text.trim(), + password: _pass.text, + ); + await c.authenticate(); // hit; lempar bila gagal + await Session.save(base, _db.text.trim(), _user.text.trim(), _pass.text); + if (!mounted) return; + widget.onSuccess(base: base, db: _db.text.trim(), user: _user.text.trim(), pass: _pass.text); + } catch (e) { + if (!mounted) return; + setState(() { + _busy = false; + _error = e.toString().replaceFirst('Exception: ', ''); + }); + } + } +} diff --git a/flutter/lib/models/portfolio.dart b/flutter/lib/models/portfolio.dart new file mode 100644 index 0000000..ae80a3d --- /dev/null +++ b/flutter/lib/models/portfolio.dart @@ -0,0 +1,41 @@ +/// Model Portofolio: ringkasan posisi per saham dari Odoo saat.portofolio. +class Portfolio { + final int id; + final String sahamTicker; + final int sisaLembar; + final double avgCost; + final double hargaSekarang; + final double nilaiSekarang; + final double realizedPl; + final double unrealizedPl; + + Portfolio({ + required this.id, + required this.sahamTicker, + required this.sisaLembar, + required this.avgCost, + required this.hargaSekarang, + required this.nilaiSekarang, + required this.realizedPl, + required this.unrealizedPl, + }); + + factory Portfolio.fromOdoo(Map m) { + // saham_id adalah tuple/list [id, ticker] dalam search_read + String ticker = ''; + final sahamId = m['saham_id']; + if (sahamId is List && sahamId.isNotEmpty) { + ticker = sahamId.length > 1 ? sahamId[1].toString() : '${sahamId[0]}'; + } + return Portfolio( + id: (m['id'] as num).toInt(), + sahamTicker: ticker, + sisaLembar: (m['sisa_lembar'] as num?)?.toInt() ?? 0, + avgCost: (m['avg_cost'] as num?)?.toDouble() ?? 0, + hargaSekarang: (m['harga_sekarang'] as num?)?.toDouble() ?? 0, + nilaiSekarang: (m['nilai_sekarang'] as num?)?.toDouble() ?? 0, + realizedPl: (m['realized_pl'] as num?)?.toDouble() ?? 0, + unrealizedPl: (m['unrealized_pl'] as num?)?.toDouble() ?? 0, + ); + } +} diff --git a/flutter/lib/models/saham_transaksi.dart b/flutter/lib/models/saham_transaksi.dart new file mode 100644 index 0000000..1da7012 --- /dev/null +++ b/flutter/lib/models/saham_transaksi.dart @@ -0,0 +1,63 @@ +/// Model Saham master dari saat.saham. +class Saham { + final int id; + final String kode; + final String? perusahaan; + final String? sektor; + + Saham({required this.id, required this.kode, this.perusahaan, this.sektor}); + + factory Saham.fromOdoo(Map m) => Saham( + id: (m['id'] as num).toInt(), + kode: (m['name'] as String?) ?? '', + perusahaan: m['perusahaan'] as String?, + sektor: m['sector'] as String?, + ); +} + +/// Model Transaksi dari saat.transaksi. +class Transaksi { + final int id; + final String tanggal; + final String tipe; // buy | sell + final int sahamId; + final String sahamTicker; + final int volume; + final double harga; + final double fee; + final double total; + + Transaksi({ + required this.id, + required this.tanggal, + required this.tipe, + required this.sahamId, + required this.sahamTicker, + required this.volume, + required this.harga, + required this.fee, + required this.total, + }); + + bool get isBuy => tipe == 'buy'; + + factory Transaksi.fromOdoo(Map m) { + String ticker = ''; + final s = m['saham_id']; + if (s is List && s.isNotEmpty) { + ticker = s.length > 1 ? s[1].toString() : '${s[0]}'; + } + final sid = s is List ? (s.first as num).toInt() : (s as num?)?.toInt() ?? 0; + return Transaksi( + id: (m['id'] as num).toInt(), + tanggal: (m['tanggal'] as String?) ?? '', + tipe: (m['tipe'] as String?) ?? 'buy', + sahamId: sid, + sahamTicker: ticker, + volume: (m['volume'] as num?)?.toInt() ?? 0, + harga: (m['harga'] as num?)?.toDouble() ?? 0, + fee: (m['fee'] as num?)?.toDouble() ?? 0, + total: (m['total'] as num?)?.toDouble() ?? 0, + ); + } +} diff --git a/flutter/lib/services/session.dart b/flutter/lib/services/session.dart new file mode 100644 index 0000000..ef7f046 --- /dev/null +++ b/flutter/lib/services/session.dart @@ -0,0 +1,35 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +const _kBase = 'odoo_base'; +const _kDb = 'odoo_db'; +const _kUser = 'odoo_user'; +const _kPass = 'odoo_pass'; + +/// Menyimpan konfigurasi login (url, db, user, password) secara aman +/// + menyediakan OdooClient terautentikasi. +class Session { + static const _storage = FlutterSecureStorage(); + + static Future save(String baseUrl, String db, String user, String pass) async { + await _storage.write(key: _kBase, value: baseUrl); + await _storage.write(key: _kDb, value: db); + await _storage.write(key: _kUser, value: user); + await _storage.write(key: _kPass, value: pass); + } + + static Future<({String base, String db, String user, String pass})?> load() async { + final base = await _storage.read(key: _kBase); + final db = await _storage.read(key: _kDb); + final user = await _storage.read(key: _kUser); + final pass = await _storage.read(key: _kPass); + if (base == null || user == null || pass == null) return null; + return (base: base, db: db ?? 'odoo', user: user, pass: pass); + } + + static Future clear() async { + await _storage.delete(key: _kBase); + await _storage.delete(key: _kDb); + await _storage.delete(key: _kUser); + await _storage.delete(key: _kPass); + } +} diff --git a/flutter/lib/services/stock_service.dart b/flutter/lib/services/stock_service.dart new file mode 100644 index 0000000..167b224 --- /dev/null +++ b/flutter/lib/services/stock_service.dart @@ -0,0 +1,83 @@ +import '../api/odoo_client.dart'; +import '../models/portfolio.dart'; +import '../models/saham_transaksi.dart'; + +/// Layanan domain utk model Odoo saat_invest. +class StockService { + StockService(this.client); + + final OdooClient client; + + // ---- Saham ---- + Future> listSaham() async { + final res = await client.executeKwd( + 'saat.saham', + 'search_read', + args: [ + [], + {'fields': ['name', 'perusahaan', 'sector']}, + ], + ); + return (res as List).map((e) => Saham.fromOdoo(e as Map)).toList(); + } + + // ---- Portofolio ---- + Future> listPortofolio() async { + final res = await client.executeKwd( + 'saat.portofolio', + 'search_read', + args: [ + [], + { + 'fields': [ + 'saham_id', 'sisa_lembar', 'avg_cost', 'harga_sekarang', + 'nilai_sekarang', 'realized_pl', 'unrealized_pl', + ] + }, + ], + ); + return (res as List).map((e) => Portfolio.fromOdoo(e as Map)).toList(); + } + + // ---- Transaksi ---- + Future> listTransaksi() async { + final res = await client.executeKwd( + 'saat.transaksi', + 'search_read', + args: [ + [], + { + 'fields': [ + 'tanggal', 'tipe', 'saham_id', 'volume', 'harga', 'fee', 'total' + ], + 'order': 'tanggal desc, id desc', + }, + ], + ); + return (res as List).map((e) => Transaksi.fromOdoo(e as Map)).toList(); + } + + Future addTransaksi({ + required int sahamId, + required String tipe, // buy | sell + required int volume, + required double harga, + required double fee, + String? tanggal, // 'YYYY-MM-DD' + }) async { + final vals = { + 'saham_id': sahamId, + 'tipe': tipe, + 'volume': volume, + 'harga': harga, + 'fee': fee, + if (tanggal != null) 'tanggal': tanggal, + }; + final res = await client.executeKwd('saat.transaksi', 'create', args: [vals]); + return (res as num).toInt(); + } + + Future hapusTransaksi(List ids) async { + await client.executeKwd('saat.transaksi', 'unlink', args: [ids]); + } +} diff --git a/flutter/pubspec.lock b/flutter/pubspec.lock new file mode 100644 index 0000000..3a5844c --- /dev/null +++ b/flutter/pubspec.lock @@ -0,0 +1,506 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + url: "https://pub.dev" + source: hosted + version: "1.4.1" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "15e8c8fe269fdf7d469b23008ab3df521c8b826ed345820532364c31bdebace6" + url: "https://pub.dev" + source: hosted + version: "11.0.0" + flutter_secure_storage_darwin: + dependency: transitive + description: + name: flutter_secure_storage_darwin + sha256: ac6d76a752de0cd738334eb4b21743fc4943f449f5b6e308f18838b048c02ac0 + url: "https://pub.dev" + source: hosted + version: "0.4.0" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "76fa9c841b3b1619fc5b5bc36efc7d158fa2356f223b6caeb1d0c80a54168546" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: "788060052712555182aba55ecb5f8b6e5cb9cfe8f776c83249a61fe3ce877db4" + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: "073a62b3aeb866ab4ce795f960413948e51e5a42a9b0c8333b6daf5bb3208a1c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: "471951813a97006d899db4948acc654a4f28c440083ea08178935ce20b173ec1" + url: "https://pub.dev" + source: hosted + version: "4.2.2" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + hooks: + dependency: transitive + description: + name: hooks + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + http: + dependency: "direct main" + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867" + url: "https://pub.dev" + source: hosted + version: "0.20.3" + jni: + dependency: transitive + description: + name: jni + sha256: f038e58b4dc2c9037f50e233175086337e0b305e356d28211bf55f21c504cbd3 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: b2310cdd4c18c65c081ab141a41efa94aa26c65431803703ece51996f174f351 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + jni_util: + dependency: transitive + description: + name: jni_util + sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df" + url: "https://pub.dev" + source: hosted + version: "6.1.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + url: "https://pub.dev" + source: hosted + version: "0.12.19" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + url: "https://pub.dev" + source: hosted + version: "0.13.0" + meta: + dependency: transitive + description: + name: meta + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + url: "https://pub.dev" + source: hosted + version: "1.18.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: b7fb95a6d9a4f009edd63dc5ac69f07420b23a16161c6dd8660290b59c602e8e + url: "https://pub.dev" + source: hosted + version: "9.5.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: ffcf4cf3d6c0b74ac43708d9f56625506e8a68aa935abe9d267a7330f320eb5d + url: "https://pub.dev" + source: hosted + version: "3.0.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 + url: "https://pub.dev" + source: hosted + version: "2.1.6" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" + url: "https://pub.dev" + source: hosted + version: "2.6.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" + url: "https://pub.dev" + source: hosted + version: "2.2.2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "261236774e8b1d69cfc6b9eabbc96c40f25e7a2d6b171f3385d4f65d5734fb24" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + record_use: + dependency: transitive + description: + name: record_use + sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed" + url: "https://pub.dev" + source: hosted + version: "0.6.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + url: "https://pub.dev" + source: hosted + version: "0.7.11" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0" + url: "https://pub.dev" + source: hosted + version: "15.3.0" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + win32: + dependency: transitive + description: + name: win32 + sha256: a0b93865d5644f11cf6a8c3f6db909f1ec168958b5805f6cc684adea957cd63d + url: "https://pub.dev" + source: hosted + version: "6.4.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: f67cdd8e07d3c6329146aaef1ba043542b3134c12489f553ca9a7435d1068aea + url: "https://pub.dev" + source: hosted + version: "3.1.4" +sdks: + dart: ">=3.12.2 <4.0.0" + flutter: ">=3.38.4" diff --git a/flutter/pubspec.yaml b/flutter/pubspec.yaml new file mode 100644 index 0000000..a21eea6 --- /dev/null +++ b/flutter/pubspec.yaml @@ -0,0 +1,92 @@ +name: saat_invest_mobile +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ^3.12.2 + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + http: ^1.6.0 + intl: ^0.20.3 + flutter_secure_storage: ^11.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^6.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/flutter/test/widget_test.dart b/flutter/test/widget_test.dart new file mode 100644 index 0000000..10a4427 --- /dev/null +++ b/flutter/test/widget_test.dart @@ -0,0 +1,13 @@ +// Smoke test: aplikasi bisa dibangun & menampilkan layer login (tanpa session, +// RootScreen saat load akan menampilkan LoginScreen karena tidak ada cred). +import 'package:flutter_test/flutter_test.dart'; + +import 'package:saat_invest_mobile/main.dart'; + +void main() { + testWidgets('App boot shows Saat Invest login', (tester) async { + await tester.pumpWidget(const SaatInvestApp()); + await tester.pumpAndSettle(); + expect(find.text('Masuk'), findsOneWidget); + }); +}