├── lib ├── app │ ├── .#itemform.dart │ ├── .#sellingform.dart │ ├── authenticate │ │ ├── authenticate.dart │ │ ├── register.dart │ │ └── sign_in.dart │ ├── mainview.dart │ ├── wrapper.dart │ ├── transactions │ │ ├── monthHistory.dart │ │ ├── dueTransactions.dart │ │ ├── salesOverview.dart │ │ └── transactionList.dart │ ├── itemlist.dart │ ├── settings.dart │ └── forms │ │ ├── stockEntryForm.dart │ │ └── salesEntryForm.dart ├── models │ ├── .#appsetting.dart │ ├── .#transaction.dart │ ├── user.dart │ ├── transaction.dart │ └── item.dart ├── main.dart ├── utils │ ├── date.dart │ ├── loading.dart │ ├── utils.dart │ ├── cache.dart │ ├── scaffold.dart │ ├── form.dart │ └── window.dart └── services │ ├── auth.dart │ └── crud.dart ├── ios ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Runner.xcworkspace │ └── contents.xcworkspacedata ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── contents.xcworkspacedata │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme └── .gitignore ├── images ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png ├── 16.png └── 17.png ├── android ├── gradle.properties ├── .gitignore ├── app │ ├── src │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── drawable │ │ │ │ │ └── launch_background.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── androidx_template │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ └── build.gradle ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── settings.gradle └── build.gradle ├── CHANGELOG.txt ├── .metadata ├── .gitignore ├── test └── widget_test.dart ├── .github └── workflows │ └── main.yml ├── pubspec.yaml ├── README.md └── pubspec.lock /lib/app/.#itemform.dart: -------------------------------------------------------------------------------- 1 | h@SHARMAJI-PC.929 -------------------------------------------------------------------------------- /lib/app/.#sellingform.dart: -------------------------------------------------------------------------------- 1 | h@SHARMAJI-PC.962 -------------------------------------------------------------------------------- /lib/models/.#appsetting.dart: -------------------------------------------------------------------------------- 1 | h@SHARMAJI-PC.3870 -------------------------------------------------------------------------------- /lib/models/.#transaction.dart: -------------------------------------------------------------------------------- 1 | h@SHARMAJI-PC.5693 -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" -------------------------------------------------------------------------------- /images/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/1.png -------------------------------------------------------------------------------- /images/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/2.png -------------------------------------------------------------------------------- /images/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/3.png -------------------------------------------------------------------------------- /images/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/4.png -------------------------------------------------------------------------------- /images/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/5.png -------------------------------------------------------------------------------- /images/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/6.png -------------------------------------------------------------------------------- /images/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/7.png -------------------------------------------------------------------------------- /images/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/8.png -------------------------------------------------------------------------------- /images/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/9.png -------------------------------------------------------------------------------- /images/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/10.png -------------------------------------------------------------------------------- /images/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/11.png -------------------------------------------------------------------------------- /images/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/12.png -------------------------------------------------------------------------------- /images/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/13.png -------------------------------------------------------------------------------- /images/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/14.png -------------------------------------------------------------------------------- /images/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/15.png -------------------------------------------------------------------------------- /images/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/16.png -------------------------------------------------------------------------------- /images/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/images/17.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.enableR8=true 3 | android.useAndroidX=true 4 | android.enableJetifier=true 5 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:bk_app/app/mainview.dart'; 3 | 4 | void main() => runApp(MainView()); 5 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hemanta212/Flutter-Inventory-App/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 7 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Version 0.9.41 2 | 3 | # Book Keeping App 4 | 5 | Fixes: 6 | * Fix item cache refresh after saving settings. 7 | * Fix the unhelpful empty message in transaction profit viewer. 8 | * Removed the dead verify account button and *Unverifed text in side bar 9 | * Moved the app settings at top 10 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 0b8abb4724aa590dd0f429683339b1e045a1594d 8 | channel: stable 9 | 10 | project_type: app 11 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/utils/date.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | class DateUtils{ 4 | static bool isNotOfToday(String date) { 5 | DateTime givenDate = DateFormat.yMMMd().add_jms().parse(date); 6 | DateTime current = DateTime.now(); 7 | return givenDate.year != current.year || 8 | givenDate.month != current.month || 9 | givenDate.day != current.day; 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /lib/utils/loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_spinkit/flutter_spinkit.dart'; 3 | 4 | class Loading extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return Container( 8 | color: Colors.blue[100], 9 | child: Center( 10 | child: SpinKitChasingDots( 11 | color: Colors.blue, 12 | size: 50.0, 13 | ), 14 | ), 15 | ); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/example/androidx_template/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.bk_app 2 | 3 | import androidx.annotation.NonNull; 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import io.flutter.plugins.GeneratedPluginRegistrant 7 | 8 | class MainActivity: FlutterActivity() { 9 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 10 | GeneratedPluginRegistrant.registerWith(flutterEngine); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | *.mode1v3 2 | *.mode2v3 3 | *.moved-aside 4 | *.pbxuser 5 | *.perspectivev3 6 | **/*sync/ 7 | .sconsign.dblite 8 | .tags* 9 | **/.vagrant/ 10 | **/DerivedData/ 11 | Icon? 12 | **/Pods/ 13 | **/.symlinks/ 14 | profile 15 | xcuserdata 16 | **/.generated/ 17 | Flutter/App.framework 18 | Flutter/Flutter.framework 19 | Flutter/Flutter.podspec 20 | Flutter/Generated.xcconfig 21 | Flutter/app.flx 22 | Flutter/app.zip 23 | Flutter/flutter_assets/ 24 | Flutter/flutter_export_environment.sh 25 | ServiceDefinitions.json 26 | Runner/GeneratedPluginRegistrant.* 27 | 28 | # Exceptions to above rules. 29 | !default.mode1v3 30 | !default.mode2v3 31 | !default.pbxuser 32 | !default.perspectivev3 33 | -------------------------------------------------------------------------------- /lib/app/authenticate/authenticate.dart: -------------------------------------------------------------------------------- 1 | import 'package:bk_app/app/authenticate/register.dart'; 2 | import 'package:bk_app/app/authenticate/sign_in.dart'; 3 | import 'package:flutter/material.dart'; 4 | 5 | class Authenticate extends StatefulWidget { 6 | @override 7 | _AuthenticateState createState() => _AuthenticateState(); 8 | } 9 | 10 | class _AuthenticateState extends State { 11 | bool showSignIn = true; 12 | void toggleView() { 13 | setState(() => showSignIn = !showSignIn); 14 | } 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | if (showSignIn) { 19 | return SignIn(toggleView: toggleView); 20 | } else { 21 | return Register(toggleView: toggleView); 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.3.50' 3 | repositories { 4 | google() 5 | jcenter() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:3.5.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | classpath "com.google.gms:google-services:4.3.3" 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | google() 18 | jcenter() 19 | } 20 | } 21 | 22 | rootProject.buildDir = '../build' 23 | subprojects { 24 | project.buildDir = "${rootProject.buildDir}/${project.name}" 25 | } 26 | subprojects { 27 | project.evaluationDependsOn(':app') 28 | } 29 | 30 | task clean(type: Delete) { 31 | delete rootProject.buildDir 32 | } 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | .commit 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Exceptions to above rules. 38 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 39 | 40 | #Vim 41 | Session.vim 42 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 8.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/models/user.dart: -------------------------------------------------------------------------------- 1 | class UserData { 2 | /// More private userData to store general info about users 3 | String uid; 4 | String email; 5 | bool verified; 6 | String targetEmail; 7 | Map roles; 8 | bool checkStock; 9 | UserData({this.uid, this.email, this.targetEmail, this.verified, this.roles}); 10 | 11 | Map toMap() { 12 | var map = Map(); 13 | map['targetEmail'] = this.targetEmail; 14 | map['email'] = this.email; 15 | map['uid'] = this.uid; 16 | map['verified'] = this.verified; 17 | map['roles'] = this.roles; 18 | map['checkStock'] = this.checkStock; 19 | return map; 20 | } 21 | 22 | UserData.fromMapObject(Map map) { 23 | this.uid = map['uid']; 24 | this.verified = map['verified']; 25 | this.email = map['email']; 26 | this.targetEmail = map['targetEmail']; 27 | this.roles = map['roles']; 28 | this.checkStock = map['checkStock']; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility that Flutter provides. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:bk_app/app/mainview.dart'; 12 | 13 | void main() { 14 | testWidgets('Counter increments smoke test', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(MainView()); 17 | 18 | // Verify that our counter starts at 0. 19 | expect(find.text('0'), findsOneWidget); 20 | expect(find.text('1'), findsNothing); 21 | 22 | // Tap the '+' icon and trigger a frame. 23 | await tester.tap(find.byIcon(Icons.add)); 24 | await tester.pump(); 25 | 26 | // Verify that our counter has incremented. 27 | expect(find.text('0'), findsNothing); 28 | expect(find.text('1'), findsOneWidget); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/app/mainview.dart: -------------------------------------------------------------------------------- 1 | import 'package:bk_app/models/user.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | import 'package:bk_app/app/wrapper.dart'; 6 | import 'package:bk_app/app/forms/salesEntryForm.dart'; 7 | import 'package:bk_app/app/itemlist.dart'; 8 | import 'package:bk_app/app/transactions/transactionList.dart'; 9 | import 'package:bk_app/app/settings.dart'; 10 | import 'package:bk_app/services/auth.dart'; 11 | 12 | class MainView extends StatelessWidget { 13 | @override 14 | Widget build(BuildContext context) { 15 | return StreamProvider.value( 16 | value: AuthService().user, 17 | child: MaterialApp( 18 | debugShowCheckedModeBanner: false, 19 | title: 'Bookkeeping app', 20 | theme: ThemeData( 21 | primarySwatch: Colors.blue, 22 | ), // ThemeData 23 | routes: { 24 | "/mainForm": (BuildContext context) => 25 | SalesEntryForm(title: "Sales Entry"), 26 | "/itemList": (BuildContext context) => ItemList(), 27 | "/transactionList": (BuildContext context) => TransactionList(), 28 | "/settings": (BuildContext context) => Setting(), 29 | }, 30 | home: Wrapper(), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/app/wrapper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'package:bk_app/app/authenticate/authenticate.dart'; 5 | import 'package:bk_app/app/itemlist.dart'; 6 | import 'package:bk_app/app/settings.dart'; 7 | import 'package:bk_app/services/crud.dart'; 8 | import 'package:bk_app/models/user.dart'; 9 | import 'package:bk_app/utils/cache.dart'; 10 | 11 | class Wrapper extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | // Initialize the cache for app 15 | final user = Provider.of(context); 16 | 17 | final StartupCache startupCache = 18 | StartupCache(userData: user, reload: true); 19 | 20 | // return either the Home or Authenticate widget 21 | if (user == null) { 22 | return Authenticate(); 23 | } else { 24 | _initializeCache(startupCache); 25 | _checkForTargetPermission(user); 26 | return ItemList(); 27 | } 28 | } 29 | 30 | void _initializeCache(startupCache) async { 31 | await startupCache.itemMap; 32 | } 33 | 34 | void _checkForTargetPermission(UserData userData) async { 35 | bool permitted = await SettingState.validateTargetEmail(userData); 36 | if (!permitted) { 37 | userData.targetEmail = userData.email; 38 | await CrudHelper().updateUserData(userData); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/utils/utils.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bk_app/models/user.dart'; 3 | import 'package:provider/provider.dart'; 4 | import 'package:bk_app/models/transaction.dart'; 5 | import 'package:bk_app/services/crud.dart'; 6 | import 'package:bk_app/utils/date.dart'; 7 | 8 | class AppUtils { 9 | static Future getTransactionsForToday(context) async { 10 | UserData userData = Provider.of(context); 11 | CrudHelper crudHelper = CrudHelper(userData: userData); 12 | 13 | Map itemTransactionMap = Map(); 14 | List transactions = await crudHelper.getItemTransactions(); 15 | if (transactions.isEmpty) { 16 | return itemTransactionMap; 17 | } 18 | transactions.forEach((transaction) { 19 | Map transactionMap = transaction.toMap(); 20 | String date = transactionMap['date']; 21 | if (DateUtils.isNotOfToday(date)) { 22 | return; 23 | } 24 | itemTransactionMap[transactionMap['id']] = { 25 | 'type': transactionMap['type'], 26 | 'itemId': transactionMap['item_id'], 27 | 'amount': transactionMap['amount'] / transactionMap['items'], 28 | 'costPrice': transactionMap['cost_price'], 29 | 'dueAmount': transactionMap['due_amount'], 30 | 'items': transactionMap['items'], 31 | 'date': date, 32 | 'description': transactionMap['description'] 33 | }; 34 | }); 35 | return itemTransactionMap; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/utils/cache.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:bk_app/models/item.dart'; 4 | import 'package:bk_app/models/user.dart'; 5 | import 'package:flutter/material.dart'; 6 | import 'package:bk_app/services/crud.dart'; 7 | 8 | class StartupCache { 9 | static StartupCache _startupCache; 10 | static Map _itemMap; 11 | bool reload; 12 | UserData userData; 13 | 14 | StartupCache._createInstance(); 15 | 16 | factory StartupCache({bool reload, UserData userData}) { 17 | // This will execute only once, singleton obj 18 | if (_startupCache == null) { 19 | _startupCache = StartupCache._createInstance(); 20 | } 21 | _startupCache.reload = reload ?? false; 22 | _startupCache.userData = userData; 23 | return _startupCache; 24 | } 25 | 26 | Future get itemMap async { 27 | if (_itemMap == null || this.reload) { 28 | debugPrint('reload is ${this.reload}'); 29 | _itemMap = await initializeItemMap(); 30 | } 31 | return _itemMap; 32 | } 33 | 34 | Future initializeItemMap() async { 35 | debugPrint("Initializing item map cache"); 36 | Map itemMap = Map(); 37 | CrudHelper crudHelper = CrudHelper(userData: this.userData); 38 | List items = await crudHelper.getItems(); 39 | if (items.isEmpty) { 40 | return itemMap; 41 | } 42 | items.forEach((Item item) { 43 | itemMap[item.id] = [ 44 | item.name, 45 | item.nickName, 46 | ]; 47 | }); 48 | debugPrint("Done $itemMap"); 49 | return itemMap; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 8 | 12 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | bk_app 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | on: 3 | push: 4 | 5 | jobs: 6 | linux: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: Checkout bookkeeping app 10 | uses: actions/checkout@v2 11 | with: 12 | path: 'app' 13 | 14 | - name: Checkout credentials repo 15 | uses: actions/checkout@v2 16 | with: 17 | repository: hemanta212/personal 18 | token: ${{ secrets.REPO_ACCESS_TOKEN }} 19 | ref: "master" 20 | path: 'personal' 21 | 22 | - run: | 23 | git clone https://github.com/flutter/flutter.git 24 | cd flutter 25 | - run: echo ::add-path::`pwd`"/flutter/bin" 26 | 27 | - uses: actions/setup-java@v1 28 | with: 29 | java-version: '12.x' 30 | 31 | - run: | 32 | cd app 33 | cp ../personal/credentials/inventory_flutter/google-services.json android/app/ 34 | 35 | flutter pub get 36 | flutter build apk 37 | mkdir build/app/outputs/apk/app_release 38 | mv build/app/outputs/apk/release/*.apk build/app/outputs/apk/app_release/ 39 | flutter build apk --target-platform android-arm,android-arm64,android-x64 --split-per-abi 40 | 41 | - name: Release 42 | uses: softprops/action-gh-release@v1 43 | if: startsWith(github.ref, 'refs/tags/') 44 | with: 45 | files: | 46 | app/build/app/outputs/apk/release/app-arm64-v8a-release.apk 47 | app/build/app/outputs/apk/release/app-armeabi-v7a-release.apk 48 | app/build/app/outputs/apk/release/app-x86_64-release.apk 49 | app/build/app/outputs/apk/app_release/app-release.apk 50 | body_path: app/CHANGELOG.txt 51 | tag_name: ${{ github.event.client_payload.version }} 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | -------------------------------------------------------------------------------- /lib/utils/scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:bk_app/models/user.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | class CustomScaffold { 6 | static Widget setDrawer(context) { 7 | UserData userData = Provider.of(context); 8 | debugPrint("ROLESS NOW IN SCAFFOLD !!! ${userData.roles}"); 9 | 10 | return Drawer( 11 | child: ListView(padding: EdgeInsets.zero, children: [ 12 | UserAccountsDrawerHeader( 13 | accountName: Text(userData.email), 14 | accountEmail: Text(''), 15 | currentAccountPicture: CircleAvatar( 16 | backgroundColor: Theme.of(context).platform == TargetPlatform.iOS 17 | ? Colors.blue 18 | : Colors.white, 19 | child: Text( 20 | "H", 21 | style: TextStyle(fontSize: 40.0), 22 | ), 23 | ), 24 | ), 25 | ListTile( 26 | leading: Icon(Icons.home), 27 | title: Text("Home"), 28 | onTap: () { 29 | Navigator.of(context).pop(); 30 | Navigator.of(context).pushNamed("/mainForm"); 31 | }), 32 | ListTile( 33 | leading: Icon(Icons.shopping_cart), 34 | title: Text('Items'), 35 | onTap: () { 36 | Navigator.of(context).pop(); 37 | Navigator.of(context).pushNamed("/itemList"); 38 | }), 39 | ListTile( 40 | leading: Icon(Icons.card_travel), 41 | title: Text('Transactions'), 42 | onTap: () { 43 | Navigator.of(context).pop(); 44 | Navigator.of(context).pushNamed("/transactionList"); 45 | }), 46 | ListTile( 47 | leading: Icon(Icons.settings), 48 | title: Text('Settings'), 49 | onTap: () { 50 | Navigator.of(context).pop(); 51 | Navigator.of(context).pushNamed("/settings"); 52 | }, 53 | ), 54 | ])); 55 | } 56 | 57 | static Widget setAppBar(title, context) { 58 | return AppBar( 59 | title: Text(title), 60 | ); 61 | } 62 | 63 | static Widget setScaffold(BuildContext context, String title, var getBody, 64 | {appBar = setAppBar}) { 65 | return Scaffold( 66 | appBar: appBar(title, context), 67 | drawer: setDrawer(context), 68 | body: getBody(context), 69 | ); // Scaffold 70 | } 71 | } // Custom Scaffold 72 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion 28 30 | 31 | sourceSets { 32 | main.java.srcDirs += 'src/main/kotlin' 33 | } 34 | 35 | lintOptions { 36 | disable 'InvalidPackage' 37 | } 38 | 39 | defaultConfig { 40 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 41 | applicationId "com.example.bk_app" 42 | minSdkVersion 16 43 | targetSdkVersion 28 44 | versionCode flutterVersionCode.toInteger() 45 | versionName flutterVersionName 46 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 47 | } 48 | 49 | buildTypes { 50 | release { 51 | // TODO: Add your own signing config for the release build. 52 | // Signing with the debug keys for now, so `flutter run --release` works. 53 | signingConfig signingConfigs.debug 54 | 55 | minifyEnabled true 56 | } 57 | 58 | debug{ 59 | minifyEnabled true 60 | } 61 | } 62 | } 63 | 64 | flutter { 65 | source '../..' 66 | } 67 | 68 | dependencies { 69 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 70 | testImplementation 'junit:junit:4.12' 71 | androidTestImplementation 'androidx.test:runner:1.1.1' 72 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' 73 | } 74 | 75 | apply plugin:'com.google.gms.google-services' 76 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /lib/services/auth.dart: -------------------------------------------------------------------------------- 1 | import 'package:bk_app/models/user.dart'; 2 | import 'package:bk_app/services/crud.dart'; 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | 5 | class AuthService { 6 | final FirebaseAuth _firebaseAuth = FirebaseAuth.instance; 7 | 8 | Future _userDataFromUser(FirebaseUser user) async { 9 | if (user == null) { 10 | return null; 11 | } 12 | UserData userData = await CrudHelper().getUserDataByUid(user.uid); 13 | if (userData == null) { 14 | // The userdatabyid method will return null when its data is null. 15 | // This is only case when user is just registered and doesnot happen othertimes 16 | // Since now user is not null (only userData supposedly is) we should allow it to happen 17 | return UserData( 18 | uid: user.uid, 19 | email: user.email, 20 | verified: user.isEmailVerified, 21 | targetEmail: user.email); 22 | } 23 | return userData; 24 | } 25 | 26 | // auth change user stream 27 | Stream get user { 28 | return _firebaseAuth.onAuthStateChanged.asyncMap(_userDataFromUser); 29 | } 30 | 31 | // sign in with email and password 32 | Future signInWithEmailAndPassword(String email, String password) async { 33 | try { 34 | AuthResult result = await _firebaseAuth.signInWithEmailAndPassword( 35 | email: email, password: password); 36 | FirebaseUser user = result.user; 37 | return user; 38 | } catch (error) { 39 | print(error.toString()); 40 | return null; 41 | } 42 | } 43 | 44 | // register with email and password 45 | Future register(String email, String password) async { 46 | try { 47 | AuthResult result = await _firebaseAuth.createUserWithEmailAndPassword( 48 | email: email, password: password); 49 | FirebaseUser user = result.user; 50 | // create a new document for the user with the uid 51 | UserData duplicate = 52 | await CrudHelper().getUserData('email', user.email); 53 | if (duplicate != null) { 54 | print("duplicate email"); 55 | return null; 56 | } 57 | 58 | UserData userData = UserData( 59 | uid: user.uid, 60 | targetEmail: user.email, 61 | email: user.email, 62 | verified: user.isEmailVerified, 63 | roles: Map()); 64 | 65 | await CrudHelper().updateUserData(userData); 66 | return user; 67 | } catch (error) { 68 | print(error.toString()); 69 | return null; 70 | } 71 | } 72 | 73 | // sign out 74 | Future signOut() async { 75 | try { 76 | return await _firebaseAuth.signOut(); 77 | } catch (error) { 78 | print(error.toString()); 79 | return null; 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: bk_app 2 | description: A new Flutter project. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # In Android, build-name is used as versionName while build-number used as versionCode. 10 | # Read more about Android versioning at https://developer.android.com/studio/publish/versioning 11 | # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. 12 | # Read more about iOS versioning at 13 | # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 14 | version: 0.9.41+1 15 | 16 | environment: 17 | sdk: ">=2.1.0 <3.0.0" 18 | 19 | dependencies: 20 | flutter: 21 | sdk: flutter 22 | 23 | cloud_firestore: 24 | firebase_auth: 25 | flutter_spinkit: 26 | provider: ^3.0.0 27 | intl: ^0.16.1 28 | flutter_typeahead: ^1.8.0 29 | 30 | 31 | dev_dependencies: 32 | flutter_test: 33 | sdk: flutter 34 | 35 | # For information on the generic Dart part of this file, see the 36 | # following page: https://dart.dev/tools/pub/pubspec 37 | 38 | # The following section is specific to Flutter. 39 | flutter: 40 | 41 | # The following line ensures that the Material Icons font is 42 | # included with your application, so that you can use the icons in 43 | # the material Icons class. 44 | uses-material-design: true 45 | 46 | # To add assets to your application, add an assets section, like this: 47 | # assets: 48 | # - images/a_dot_burr.jpeg 49 | # - images/a_dot_ham.jpeg 50 | 51 | # An image asset can refer to one or more resolution-specific "variants", see 52 | # https://flutter.dev/assets-and-images/#resolution-aware. 53 | 54 | # For details regarding adding assets from package dependencies, see 55 | # https://flutter.dev/assets-and-images/#from-packages 56 | 57 | # To add custom fonts to your application, add a fonts section here, 58 | # in this "flutter" section. Each entry in this list should have a 59 | # "family" key with the font family name, and a "fonts" key with a 60 | # list giving the asset and other descriptors for the font. For 61 | # example: 62 | # fonts: 63 | # - family: Schyler 64 | # fonts: 65 | # - asset: fonts/Schyler-Regular.ttf 66 | # - asset: fonts/Schyler-Italic.ttf 67 | # style: italic 68 | # - family: Trajan Pro 69 | # fonts: 70 | # - asset: fonts/TrajanPro.ttf 71 | # - asset: fonts/TrajanPro_Bold.ttf 72 | # weight: 700 73 | # 74 | # For details regarding fonts from package dependencies, 75 | # see https://flutter.dev/custom-fonts/#from-packages 76 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/models/transaction.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | 3 | class ItemTransaction { 4 | String _id; 5 | int _type; 6 | double _amount; 7 | double _costPrice; 8 | double _dueAmount; 9 | String _itemId; 10 | double _items; 11 | String _date; 12 | String _description; 13 | int _createdAt; 14 | String _signature; 15 | 16 | ItemTransaction( 17 | this._type, this._itemId, this._amount, this._items, this._date, 18 | [this._description, this._costPrice, this._signature]); 19 | 20 | String get id => _id; 21 | 22 | String get itemId => _itemId; 23 | 24 | int get type => _type; 25 | 26 | int get createdAt => _createdAt; 27 | 28 | double get amount => _amount; 29 | 30 | double get costPrice => _costPrice; 31 | 32 | double get dueAmount => _dueAmount; 33 | 34 | String get date => _date; 35 | 36 | double get items => _items; 37 | 38 | String get description => _description; 39 | 40 | String get signature => _signature; 41 | 42 | set itemId(String newItemId) { 43 | this._itemId = newItemId; 44 | } 45 | 46 | set id(String newId) { 47 | this._id = newId; 48 | } 49 | 50 | set type(int newType) { 51 | this._type = newType; 52 | } 53 | 54 | set createdAt(int newCreatedAt) { 55 | this._createdAt = newCreatedAt; 56 | } 57 | 58 | set description(String newDesc) { 59 | this._description = newDesc; 60 | } 61 | 62 | set signature(String newVerified) { 63 | this._signature = newVerified; 64 | } 65 | 66 | set date(String newDate) { 67 | this._date = newDate; 68 | } 69 | 70 | set amount(double newAmount) { 71 | this._amount = newAmount; 72 | } 73 | 74 | set costPrice(double newCostPrice) { 75 | this._costPrice = newCostPrice; 76 | } 77 | 78 | set dueAmount(double newDueAmount) { 79 | this._dueAmount = newDueAmount; 80 | } 81 | 82 | set items(double newItems) { 83 | this._items = newItems; 84 | } 85 | 86 | static List fromQuerySnapshot(QuerySnapshot snapshot) { 87 | List transactions = List(); 88 | snapshot.documents.forEach((DocumentSnapshot doc) { 89 | ItemTransaction transaction = ItemTransaction.fromMapObject(doc.data); 90 | transaction.id = doc.documentID; 91 | transactions.add(transaction); 92 | }); 93 | transactions.sort((a, b) { 94 | return b.createdAt.compareTo(a.createdAt); 95 | }); 96 | return transactions; 97 | } 98 | 99 | Map toMap() { 100 | var map = Map(); 101 | 102 | map['id'] = _id; 103 | map['item_id'] = _itemId; 104 | map['type'] = _type; 105 | map['description'] = _description; 106 | map['due_amount'] = _dueAmount; 107 | map['date'] = _date; 108 | map['amount'] = _amount; 109 | map['items'] = _items; 110 | map['cost_price'] = _costPrice; 111 | map['created_at'] = _createdAt; 112 | map['signature'] = _signature; 113 | return map; 114 | } 115 | 116 | ItemTransaction.fromMapObject(Map map) { 117 | this._id = map['id']; 118 | this._type = map['type']; 119 | this._description = map['description']; 120 | this._dueAmount = map['due_amount']; 121 | this._itemId = map['item_id']; 122 | this._date = map['date']; 123 | this._amount = map['amount']; 124 | this._costPrice = map['cost_price']; 125 | this._items = map['items']; 126 | this._createdAt = map['created_at']; 127 | this._signature = map['signature']; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Inventory management app 2 | A simple approach to inventory management in retail or small business. 3 | This app is focused on making the sales entry, stock entry as fast as practically possible. 4 | 5 | ![Build](https://github.com/hemanta212/inventory_app/workflows/Build/badge.svg) 6 | 7 | ## Features/Concepts Highlights 8 | * Advanced state management solutions like provider, 9 | * Use of singleton objects for caching and accessing the instance throughout the code. 10 | * Implementation of Navigation Drawers, tabs, search bars, autocompletion and fuzzy like search algorithms. 11 | * Use of Streams for live UI updates, Datatables for better data view, routes for screens. 12 | * Using batches for failproof db operations, Role base db modifications. 13 | * Testing in flutter application. 14 | * Data Export/Import from CSV dumps. 15 | * Online and Offline No SQL data storage with firebase's cloud_firestore. 16 | * Integration of Authentication like google signin, email authentication with firebase_auth. 17 | * Continuous integration for streamlined devflow 18 | 19 | ## App overview 20 | 21 | 22 | 23 | 24 | 25 | 26 | ## Project Installation: 27 | 28 | ### 1. Install and try out the apk! 29 | Head over to the release page to try out the apps. [APK Releases](https://github.com/hemanta212/inventory_app/releases/latest) 30 | 31 | ### 2. Building yourself 32 | 33 | #### 1. Get Flutter 34 | * Install flutter : [Flutter Installation](https://flutter.dev/docs/get-started/install) 35 | 36 | #### 2. Clone this repo 37 | ``` 38 | $ git clone https://github.com/hemanta212/inventory_app.git 39 | $ cd inventory_app 40 | ``` 41 | 42 | #### 3. Setup the firebase app 43 | 44 | 1. You'll need to create a Firebase instance. Follow the instructions at https://console.firebase.google.com. 45 | 2. Once your Firebase instance is created, you'll need to enable Google authentication. 46 | 47 | * Go to the Firebase Console for your new instance. 48 | * Click "Authentication" in the left-hand menu 49 | * Click the "sign-in method" tab 50 | * Click "Google" and enable it 51 | 52 | 3. Enable the Firebase Database 53 | * Go to the Firebase Console 54 | * Click "Database" in the left-hand menu 55 | * Click the Cloudstore "Create Database" button 56 | * Select "Start in test mode" and "Enable" 57 | 58 | 4. (skip if not running on Android) 59 | * Create an app within your Firebase instance for Android, with package name com.yourcompany.news 60 | * Run the following command to get your SHA-1 key: 61 | 62 | ``` 63 | keytool -exportcert -list -v \ 64 | -alias androiddebugkey -keystore ~/.android/debug.keystore 65 | ``` 66 | 67 | * In the Firebase console, in the settings of your Android app, add your SHA-1 key by clicking "Add Fingerprint". 68 | * Follow instructions to download google-services.json 69 | * place `google-services.json` into `/android/app/`. 70 | 71 | #### 4. Run 72 | Connect your device 73 | 74 | ``` 75 | $ flutter upgrade 76 | $ flutter pub get 77 | $ flutter run 78 | ``` 79 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 66 | 67 | 73 | 75 | 81 | 82 | 83 | 84 | 86 | 87 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /lib/app/authenticate/register.dart: -------------------------------------------------------------------------------- 1 | import 'package:bk_app/services/auth.dart'; 2 | import 'package:bk_app/utils/loading.dart'; 3 | import 'package:bk_app/utils/window.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class Register extends StatefulWidget { 7 | final Function toggleView; 8 | Register({this.toggleView}); 9 | 10 | @override 11 | _RegisterState createState() => _RegisterState(); 12 | } 13 | 14 | class _RegisterState extends State { 15 | final AuthService _auth = AuthService(); 16 | final _formKey = GlobalKey(); 17 | String error = ''; 18 | bool loading = false; 19 | 20 | TextEditingController userEmailController = TextEditingController(); 21 | TextEditingController userPasswordController = TextEditingController(); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | TextStyle textStyle = Theme.of(context).textTheme.title; 26 | return loading 27 | ? Loading() 28 | : Scaffold( 29 | appBar: AppBar( 30 | elevation: 0.0, 31 | title: Text('Sign up'), 32 | actions: [ 33 | FlatButton.icon( 34 | icon: Icon(Icons.person), 35 | label: Text('Sign In'), 36 | onPressed: () => widget.toggleView(), 37 | ), 38 | ], 39 | ), 40 | body: Container( 41 | padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 50.0), 42 | child: Form( 43 | key: _formKey, 44 | child: ListView( 45 | children: [ 46 | SizedBox(height: 20.0), 47 | WindowUtils.genTextField( 48 | labelText: "Email", 49 | hintText: "example@gmail.com", 50 | textStyle: textStyle, 51 | controller: this.userEmailController, 52 | onChanged: (val) { 53 | setState(() => this.userEmailController.text = val); 54 | }, 55 | ), 56 | WindowUtils.genTextField( 57 | labelText: "Password", 58 | textStyle: textStyle, 59 | controller: this.userPasswordController, 60 | obscureText: true, 61 | validator: (val, labelText) => val.length < 6 62 | ? 'Enter a $labelText 6+ chars long' 63 | : null, 64 | onChanged: (val) { 65 | setState(() => this.userPasswordController.text = val); 66 | }, 67 | ), 68 | RaisedButton( 69 | color: Theme.of(context).accentColor, 70 | child: Text( 71 | 'Register', 72 | style: TextStyle(color: Colors.white), 73 | ), 74 | onPressed: () async { 75 | if (_formKey.currentState.validate()) { 76 | setState(() => loading = true); 77 | String email = this.userEmailController.text; 78 | String password = this.userPasswordController.text; 79 | dynamic result = 80 | await _auth.register(email, password); 81 | if (result == null) { 82 | setState(() { 83 | loading = false; 84 | error = 'Please supply a valid email'; 85 | }); 86 | } 87 | } 88 | }), 89 | SizedBox(height: 12.0), 90 | Text( 91 | error, 92 | style: TextStyle(color: Colors.red, fontSize: 14.0), 93 | ) 94 | ], 95 | ), 96 | ), 97 | ), 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/app/authenticate/sign_in.dart: -------------------------------------------------------------------------------- 1 | import 'package:bk_app/services/auth.dart'; 2 | import 'package:bk_app/utils/loading.dart'; 3 | import 'package:bk_app/utils/window.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | class SignIn extends StatefulWidget { 7 | final Function toggleView; 8 | SignIn({this.toggleView}); 9 | 10 | @override 11 | _SignInState createState() => _SignInState(); 12 | } 13 | 14 | class _SignInState extends State { 15 | final AuthService _auth = AuthService(); 16 | final _formKey = GlobalKey(); 17 | String error = ''; 18 | bool loading = false; 19 | 20 | TextEditingController userEmailController = TextEditingController(); 21 | TextEditingController userPasswordController = TextEditingController(); 22 | 23 | @override 24 | Widget build(BuildContext context) { 25 | TextStyle textStyle = Theme.of(context).textTheme.title; 26 | return loading 27 | ? Loading() 28 | : Scaffold( 29 | appBar: AppBar( 30 | elevation: 0.0, 31 | title: Text('Sign in'), 32 | actions: [ 33 | FlatButton.icon( 34 | icon: Icon(Icons.person), 35 | label: Text('Register'), 36 | onPressed: () => widget.toggleView(), 37 | ), 38 | ], 39 | ), 40 | body: Container( 41 | padding: EdgeInsets.symmetric(vertical: 20.0, horizontal: 50.0), 42 | child: Form( 43 | key: _formKey, 44 | child: ListView( 45 | children: [ 46 | SizedBox(height: 20.0), 47 | 48 | // No of items 49 | WindowUtils.genTextField( 50 | labelText: "Email", 51 | hintText: "example@gmail.com", 52 | textStyle: textStyle, 53 | controller: this.userEmailController, 54 | onChanged: (val) { 55 | setState(() => this.userEmailController.text = val); 56 | }, 57 | ), 58 | 59 | // No of items 60 | WindowUtils.genTextField( 61 | labelText: "Password", 62 | textStyle: textStyle, 63 | controller: this.userPasswordController, 64 | obscureText: true, 65 | validator: (val, labelText) => val.length < 6 66 | ? 'Enter a $labelText 6+ chars long' 67 | : null, 68 | onChanged: (val) { 69 | setState(() => this.userPasswordController.text = val); 70 | }, 71 | ), 72 | 73 | SizedBox(height: 20.0), 74 | RaisedButton( 75 | color: Theme.of(context).accentColor, 76 | child: Text( 77 | 'Sign In', 78 | style: TextStyle(color: Colors.white), 79 | ), 80 | onPressed: () async { 81 | if (_formKey.currentState.validate()) { 82 | setState(() => loading = true); 83 | String email = this.userEmailController.text; 84 | String password = this.userPasswordController.text; 85 | dynamic result = await _auth 86 | .signInWithEmailAndPassword(email, password); 87 | if (result == null) { 88 | setState(() { 89 | loading = false; 90 | error = 91 | 'Could not sign in with those credentials'; 92 | }); 93 | } 94 | } 95 | }), 96 | SizedBox(height: 12.0), 97 | Text( 98 | error, 99 | style: TextStyle(color: Colors.red, fontSize: 14.0), 100 | ), 101 | ], 102 | ), 103 | ), 104 | ), 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lib/app/transactions/monthHistory.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | import 'package:intl/intl.dart'; 4 | 5 | import 'package:bk_app/models/user.dart'; 6 | import 'package:bk_app/models/transaction.dart'; 7 | import 'package:bk_app/services/crud.dart'; 8 | import 'package:bk_app/utils/loading.dart'; 9 | import 'package:bk_app/utils/scaffold.dart'; 10 | import 'package:bk_app/utils/form.dart'; 11 | import 'package:bk_app/utils/window.dart'; 12 | 13 | class MonthlyHistory extends StatefulWidget { 14 | @override 15 | _MonthlyHistoryState createState() => _MonthlyHistoryState(); 16 | } 17 | 18 | class _MonthlyHistoryState extends State { 19 | static CrudHelper crudHelper; 20 | static UserData userData; 21 | 22 | Map currentMonthHistory = Map(); 23 | 24 | @override 25 | void didChangeDependencies() { 26 | super.didChangeDependencies(); 27 | userData = Provider.of(context); 28 | if (userData != null) { 29 | crudHelper = CrudHelper(userData: userData); 30 | _initializeCurrentMonthHistory(); 31 | } 32 | } 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar( 38 | title: Text("Monthly History"), 39 | ), 40 | drawer: CustomScaffold.setDrawer(context), 41 | body: this.showTransactionHistoryForCurrentMonth(), 42 | bottomNavigationBar: _buildBottomAppBar(context), 43 | ); 44 | } 45 | 46 | BottomAppBar _buildBottomAppBar(BuildContext context) { 47 | return BottomAppBar( 48 | shape: CircularNotchedRectangle(), 49 | color: Theme.of(context).primaryColor, 50 | child: Row( 51 | children: [ 52 | IconButton( 53 | icon: Icon(Icons.card_giftcard), 54 | onPressed: () => WindowUtils.navigateToPage(context, 55 | target: 'Transactions', caller: 'Month History')), 56 | SizedBox(width: 20.0), 57 | IconButton( 58 | icon: Icon(Icons.access_alarm), 59 | onPressed: () => WindowUtils.navigateToPage(context, 60 | target: 'Due Transactions', caller: 'Month History')), 61 | SizedBox(width: 150.0), 62 | IconButton( 63 | icon: Icon(Icons.history), 64 | onPressed: () => WindowUtils.navigateToPage(context, 65 | target: 'Month History', caller: 'Month History')) 66 | ], 67 | ), 68 | ); 69 | } 70 | 71 | Widget showTransactionHistoryForCurrentMonth() { 72 | if (this.currentMonthHistory != null) { 73 | return ListView.builder( 74 | itemCount: this.currentMonthHistory.length, 75 | itemBuilder: (BuildContext context, int index) { 76 | String date = this.currentMonthHistory.keys.toList()[index]; 77 | Map data = this.currentMonthHistory[date]; 78 | return Card( 79 | color: Colors.white, 80 | elevation: 2.0, 81 | child: ListTile( 82 | leading: Column( 83 | crossAxisAlignment: CrossAxisAlignment.center, 84 | children: [ 85 | SizedBox(height: 20.0), 86 | Text(date), 87 | ], 88 | ), 89 | title: this._getMonthlyDescription(context, data), 90 | )); 91 | }, 92 | ); 93 | } else { 94 | return Loading(); 95 | } 96 | } 97 | 98 | Widget _getMonthlyDescription(BuildContext context, Map history) { 99 | ThemeData localTheme = Theme.of(context); 100 | String profit = FormUtils.fmtToIntIfPossible( 101 | FormUtils.getShortDouble(history['profit'])); 102 | 103 | String sales = FormUtils.fmtToIntIfPossible( 104 | FormUtils.getShortDouble(history['sales'])); 105 | 106 | return Row(children: [ 107 | Expanded( 108 | flex: 1, 109 | child: Column(children: [ 110 | Text("Sales", style: localTheme.textTheme.subhead), 111 | Text("Rs. $sales"), 112 | ])), 113 | Column( 114 | crossAxisAlignment: CrossAxisAlignment.center, 115 | children: [ 116 | Text("Profit", style: localTheme.textTheme.subhead), 117 | Text("Rs. $profit"), 118 | ], 119 | ), 120 | ]); 121 | } 122 | 123 | void _initializeCurrentMonthHistory() async { 124 | final allTransactions = await crudHelper.getItemTransactions(); 125 | 126 | setState(() { 127 | allTransactions.forEach((ItemTransaction transaction) { 128 | DateTime transactionDate = 129 | DateFormat.yMMMd().add_jms().parse(transaction.date); 130 | DateTime current = DateTime.now(); 131 | 132 | if (transaction.type == 0 && 133 | transactionDate.year == current.year && 134 | transactionDate.month == current.month) { 135 | String day = DateFormat.MMMd().format(transactionDate); 136 | double profit = 137 | transaction.amount - transaction.items * transaction.costPrice; 138 | if (this.currentMonthHistory.containsKey(day) ?? false) { 139 | this.currentMonthHistory[day]['profit'] += profit; 140 | this.currentMonthHistory[day]['sales'] += transaction.amount; 141 | } else { 142 | this.currentMonthHistory[day] = { 143 | 'profit': profit, 144 | 'sales': transaction.amount, 145 | }; 146 | } 147 | } 148 | }); 149 | }); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /lib/app/transactions/dueTransactions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'package:bk_app/app/wrapper.dart'; 5 | import 'package:bk_app/app/transactions/transactionList.dart'; 6 | import 'package:bk_app/models/transaction.dart'; 7 | import 'package:bk_app/models/user.dart'; 8 | import 'package:bk_app/utils/scaffold.dart'; 9 | import 'package:bk_app/utils/form.dart'; 10 | import 'package:bk_app/utils/window.dart'; 11 | import 'package:bk_app/utils/cache.dart'; 12 | import 'package:bk_app/utils/loading.dart'; 13 | import 'package:bk_app/services/crud.dart'; 14 | 15 | class DueTransaction extends StatefulWidget { 16 | @override 17 | State createState() { 18 | return DueTransactionState(); 19 | } 20 | } 21 | 22 | class DueTransactionState extends State { 23 | static CrudHelper crudHelper; 24 | Map itemMapCache = Map(); 25 | List payableTransactions; 26 | List receivableTransactions; 27 | bool loading = true; 28 | static UserData userData; 29 | 30 | @override 31 | void initState() { 32 | _initializeItemMapCache(); 33 | super.initState(); 34 | } 35 | 36 | @override 37 | void didChangeDependencies() { 38 | super.didChangeDependencies(); 39 | userData = Provider.of(context); 40 | if (userData != null) { 41 | crudHelper = CrudHelper(userData: userData); 42 | _updateListView(); 43 | } 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | if (userData == null) { 49 | return Wrapper(); 50 | } 51 | List viewTabs = [ 52 | Tab(text: "To receive"), 53 | Tab(text: "To pay"), 54 | ]; 55 | return DefaultTabController( 56 | length: viewTabs.length, 57 | child: Scaffold( 58 | appBar: AppBar( 59 | title: Text("Due Transactions"), 60 | bottom: TabBar(tabs: viewTabs), 61 | ), 62 | drawer: CustomScaffold.setDrawer(context), 63 | body: TabBarView(children: [ 64 | getDueTransactionView(type: 'receivable'), 65 | getDueTransactionView(type: 'payable'), 66 | ]), 67 | bottomNavigationBar: buildBottomAppBar(context), 68 | )); 69 | } 70 | 71 | static BottomAppBar buildBottomAppBar(BuildContext context) { 72 | return BottomAppBar( 73 | shape: CircularNotchedRectangle(), 74 | color: Theme.of(context).primaryColor, 75 | child: Row( 76 | children: [ 77 | IconButton( 78 | icon: Icon(Icons.card_travel), 79 | onPressed: () => WindowUtils.navigateToPage(context, 80 | target: 'Transactions', caller: 'Due Transactions')), 81 | SizedBox(width: 20.0), 82 | IconButton( 83 | icon: Icon(Icons.access_alarm), 84 | onPressed: () => WindowUtils.navigateToPage(context, 85 | target: 'Due Transactions', caller: 'Due Transactions')), 86 | SizedBox(width: 150.0), 87 | IconButton( 88 | icon: Icon(Icons.history), 89 | onPressed: () => WindowUtils.navigateToPage(context, 90 | target: 'Month History', caller: 'Due Transactions')) 91 | ], 92 | ), 93 | ); 94 | } 95 | 96 | Widget getDueTransactionView({type}) { 97 | Map transactionMap = { 98 | 'payable': this.payableTransactions, 99 | 'receivable': this.receivableTransactions, 100 | }; 101 | List transactions = transactionMap[type]; 102 | return transactions != null 103 | ? ListView.builder( 104 | itemCount: transactions.length, 105 | itemBuilder: (BuildContext context, int index) { 106 | ItemTransaction transaction = transactions[index]; 107 | return Card( 108 | color: Colors.white, 109 | elevation: 2.0, 110 | child: ListTile( 111 | leading: Column( 112 | crossAxisAlignment: CrossAxisAlignment.center, 113 | children: [ 114 | SizedBox(height: 10.0), 115 | Text("Amount"), 116 | Text( 117 | "Rs. ${FormUtils.fmtToIntIfPossible(transaction.amount)}"), 118 | ], 119 | ), 120 | title: TransactionListState.getDescription( 121 | context, transaction, this.itemMapCache), 122 | onTap: () { 123 | TransactionListState.navigateToDetail( 124 | context, transaction, 'Edit Item', 125 | updateListView: this._updateListView); 126 | }, 127 | )); 128 | }, 129 | ) 130 | : Loading(); 131 | } 132 | 133 | void _updateListView() async { 134 | List dueTransactions = 135 | await crudHelper.getDueTransactions(); 136 | 137 | setState(() { 138 | this.receivableTransactions = dueTransactions.sublist(0); 139 | debugPrint( 140 | "all transactions ${dueTransactions.length} and recievables ${this.receivableTransactions}"); 141 | this.receivableTransactions.retainWhere((ItemTransaction transaction) { 142 | if (transaction.type == 0) 143 | return true; 144 | else 145 | return false; 146 | }); 147 | 148 | this.payableTransactions = dueTransactions.sublist(0); 149 | this.payableTransactions.retainWhere((ItemTransaction transaction) { 150 | if (transaction.type == 1) 151 | return true; 152 | else 153 | return false; 154 | }); 155 | }); 156 | } 157 | 158 | void _initializeItemMapCache() async { 159 | this.itemMapCache = await StartupCache().itemMap; 160 | setState(() { 161 | this.loading = false; 162 | }); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /lib/utils/form.dart: -------------------------------------------------------------------------------- 1 | import 'package:bk_app/models/user.dart'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'package:intl/intl.dart'; 4 | import 'package:bk_app/models/item.dart'; 5 | import 'package:bk_app/models/transaction.dart'; 6 | 7 | class FormUtils { 8 | static String fmtToIntIfPossible(double value) { 9 | if (value == null) { 10 | return ''; 11 | } 12 | 13 | String intString = '${value.ceil()}'; 14 | if (double.parse(intString) == value) { 15 | return intString; 16 | } else { 17 | return '$value'; 18 | } 19 | } 20 | 21 | static double getShortDouble(double value, {round = 2}) { 22 | return double.parse(value.toStringAsFixed(round)); 23 | } 24 | 25 | static bool isDatabaseOwner(UserData userData) { 26 | return userData.targetEmail == userData.email; 27 | } 28 | 29 | static bool isTransactionOwner( 30 | UserData userData, ItemTransaction transaction) { 31 | return transaction.signature == userData.email; 32 | } 33 | 34 | static Future saveTransactionAndUpdateItem( 35 | ItemTransaction transaction, Item item, 36 | {UserData userData}) async { 37 | Firestore db = Firestore.instance; 38 | String message = ''; 39 | 40 | String targetEmail = userData.targetEmail; 41 | WriteBatch batch = db.batch(); 42 | 43 | try { 44 | if (transaction.id == null) { 45 | // Insert operation 46 | transaction.createdAt = DateTime.now().millisecondsSinceEpoch; 47 | transaction.date = DateFormat.yMMMd().add_jms().format(DateTime.now()); 48 | if (transaction.type == 1) item.lastStockEntry = transaction.date; 49 | transaction.signature = userData.email; 50 | batch.setData(db.collection('$targetEmail-transactions').document(), 51 | transaction.toMap()); 52 | } else { 53 | // Update operation 54 | if (!isDatabaseOwner(userData) && 55 | !isTransactionOwner(userData, transaction)) { 56 | return "Permission Denied: You don't have editing access"; 57 | } else { 58 | transaction.signature = userData.email; 59 | batch.updateData( 60 | db 61 | .collection('$targetEmail-transactions') 62 | .document(transaction.id), 63 | transaction.toMap()); 64 | } 65 | } 66 | 67 | item.used += 1; 68 | 69 | if (isDatabaseOwner(userData)) { 70 | // Item should only be modified by the owner of database. When some one other that owner creates or 71 | // updates the transactins items is not changed and only transaction change with the sign of them 72 | // Later when owner accepts it (by just resaving) this condition is passed and item is changed 73 | print("db owner so updating item $item"); 74 | batch.updateData(db.collection('$targetEmail-items').document(item.id), 75 | item.toMap()); 76 | } 77 | 78 | batch.commit(); 79 | } catch (e) { 80 | message = 'Error updating transaction info! Try again.'; 81 | } 82 | return message; 83 | } 84 | 85 | static void deleteTransactionAndUpdateItem(Function callback, 86 | ItemTransaction transaction, Item item, UserData userData) async { 87 | // Sync newly updated item and delete transaction from db in batch 88 | Firestore db = Firestore.instance; 89 | String message = ''; 90 | String targetEmail = userData.targetEmail; 91 | WriteBatch batch = db.batch(); 92 | 93 | if (!isDatabaseOwner(userData) && 94 | !isTransactionOwner(userData, transaction)) { 95 | callback("Permission Denied: You don't have deleting access"); 96 | return; 97 | } 98 | 99 | if (item == null || !isDatabaseOwner(userData)) { 100 | //Condition 1: 101 | // Those transaction whose relating item are deleted are orphan transactions 102 | // The item associated can't be modified so we can just delete transaction only 103 | 104 | //Condition 2: 105 | // The case is similar for transactions not created by owner they are classified as 106 | // draft and item associated to them should not change until owner verfies/owns it. 107 | 108 | batch.delete( 109 | db.collection('$targetEmail-transactions').document(transaction.id)); 110 | batch.commit(); 111 | callback(message); 112 | return; 113 | } 114 | 115 | item.used += 1; 116 | 117 | if (transaction.type == 0) { 118 | item.increaseStock(transaction.items); 119 | } else { 120 | item.decreaseStock(transaction.items); 121 | } 122 | try { 123 | batch.delete( 124 | db.collection('$targetEmail-transactions').document(transaction.id)); 125 | batch.updateData( 126 | db.collection('$targetEmail-items').document(item.id), item.toMap()); 127 | 128 | batch.commit(); 129 | } catch (e) { 130 | message = "Error deleting transaction info! Try again."; 131 | } 132 | 133 | callback(message); 134 | } 135 | 136 | static genFuzzySuggestionsForItem(String sampleString, List sourceList) { 137 | if (sourceList.isEmpty) { 138 | return sourceList; 139 | } 140 | List result = sourceList.where((map) { 141 | String itemName = map['name'].toLowerCase(); 142 | String itemNickName = map['nickName']?.toLowerCase() ?? ''; 143 | 144 | // Takes: user given string | constructs -> regexPattern 145 | // e.g: "zam" -> ".*z.*a.*m.*" 146 | List strsWithWildCards = "$sampleString" 147 | .split("") 148 | .map((letter) => ".*$letter") 149 | .toList(); // Makes "zam" -> ".*z.*a.*m" 150 | strsWithWildCards.add('.*'); // ".*z.*a.*m" -> ".*z.*a.*m.*" 151 | String regexPattern = strsWithWildCards.join(''); 152 | 153 | // \ escape char is replaced by \\ to simulate raw string. 154 | regexPattern = regexPattern.replaceAll(r"\", r"\\"); 155 | print("escaped regexPattern $regexPattern"); 156 | 157 | RegExp regExp = new RegExp( 158 | "$regexPattern", 159 | caseSensitive: false, 160 | multiLine: false, 161 | ); 162 | 163 | return regExp.hasMatch("$itemName") || regExp.hasMatch("$itemNickName"); 164 | }).toList(); 165 | print("got ${result.length} FUZZY SEARCH results"); 166 | return result; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /lib/models/item.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | 3 | class Item { 4 | String _id; 5 | String _name; 6 | String _nickName; 7 | String _description; 8 | double _costPrice; 9 | String _markedPrice; 10 | double _totalStock = 0.0; 11 | String _lastStockEntry; 12 | int _used = 0; 13 | Map _units; 14 | 15 | Item( 16 | this._name, [ 17 | this._nickName, 18 | this._costPrice, 19 | this._markedPrice, 20 | this._description, 21 | ]); 22 | 23 | String get id => _id; 24 | 25 | String get name => _name; 26 | 27 | String get nickName => _nickName; 28 | 29 | String get lastStockEntry => _lastStockEntry; 30 | 31 | double get costPrice => _costPrice; 32 | 33 | String get description => _description; 34 | 35 | String get markedPrice => _markedPrice; 36 | 37 | double get totalStock => _totalStock; 38 | 39 | int get used => _used; 40 | Map get units => _units; 41 | 42 | set id(String newId) { 43 | this._id = newId; 44 | } 45 | 46 | set name(String newName) { 47 | if (newName.length <= 140) { 48 | this._name = newName; 49 | } 50 | } 51 | 52 | set nickName(String newNickName) { 53 | if (newNickName.length <= 40) { 54 | this._nickName = newNickName; 55 | } 56 | } 57 | 58 | set description(String newDesc) { 59 | this._description = newDesc; 60 | } 61 | 62 | set lastStockEntry(String newLastStockEntryId) { 63 | this._lastStockEntry = newLastStockEntryId; 64 | } 65 | 66 | set used(int newUsed) { 67 | this._used = newUsed; 68 | } 69 | 70 | set costPrice(double newCostPrice) { 71 | this._costPrice = newCostPrice; 72 | } 73 | 74 | set markedPrice(String newMarkedPrice) { 75 | this._markedPrice = newMarkedPrice; 76 | } 77 | 78 | set totalStock(double newTotalStock) { 79 | this._totalStock = newTotalStock; 80 | } 81 | 82 | set units(Map newUnits) { 83 | this._units = newUnits; 84 | } 85 | 86 | void increaseStock(double addedStock) { 87 | this._totalStock += addedStock; 88 | } 89 | 90 | void decreaseStock(double soldStock) { 91 | this._totalStock -= soldStock; 92 | } 93 | 94 | List getNewCostPriceAndStock( 95 | double totalCostPriceOfTransaction, double noOfItemsInTransaction) { 96 | /// We average the two cp thus calculating a new equivalent cp 97 | 98 | /// Suppose we buy 10 items 'A' in 100 and 5 'B' in 200. 99 | /// We have two CP's but SP is always one at a time here we will 100 | /// sell in 250 here profit becomes 15 * 250 - (10 * 100 + 5 * 200) 101 | /// Can we get a combined cp that gives the same profit? 102 | /// Just get avg. Avg cp = (100 * 10 + 200 * 5 ) / (10 + 5 ) 103 | 104 | // If new item just register first transaction as is. 105 | if (this._costPrice == null) 106 | return [ 107 | totalCostPriceOfTransaction / noOfItemsInTransaction, 108 | noOfItemsInTransaction 109 | ]; 110 | 111 | double currentCp = this._costPrice; 112 | double totalCurrentCpOfStocks = currentCp * this._totalStock; 113 | double totalCp = totalCurrentCpOfStocks + totalCostPriceOfTransaction; 114 | double totalItems = this._totalStock + noOfItemsInTransaction; 115 | double newCp = totalCp / totalItems; 116 | return [newCp, totalItems]; 117 | } 118 | 119 | void modifyLatestStockEntry(transaction, double newNoOfItemsInTransaction, 120 | double newTotalCostPriceOfTransaction) { 121 | /// Retrieves and redo the current costPrice calculation 122 | /// So when 6 units costing $10 each and 4 units costing 20 each are 123 | /// Combined by above cp method, it gives combined CP of $14 for (6 + 4 = 10) units. 124 | /// When user comes later to say that he was mistaken and it was 5 units 125 | /// costing $25 each (not 4 & $20) then we have to recover the already done calculation 126 | /// and redo it. This function does it. 127 | /// It takes the faulty stock entry transaction ( 4 & $20), then from current total 128 | /// stock of 10 retrieves the old totalstock (6) and through equation 14 = (4*20 + 6*x) / (6+4) 129 | /// which was used to take out the avg CP($14), takes the unknown X (oldCp before faulty transaction) 130 | /// Now since we know old 6 and $10 and new correct 5 and $25 we can reapply the formula to get CP. 131 | 132 | // confirm the transaction type as incoming or stock entry (1) 133 | assert(transaction.type == 1); 134 | // Retrieve old cp and totalStock before faulty transaction 135 | double oldTransactionItems = transaction.items; 136 | double oldTransactionCostPrice = transaction.amount / oldTransactionItems; 137 | print( 138 | "Got old transaction cp $oldTransactionCostPrice and items $oldTransactionItems"); 139 | double oldTotalStock = this._totalStock - oldTransactionItems; 140 | double oldTotalCostPrice = ((this._costPrice * this._totalStock) - 141 | (oldTransactionItems * oldTransactionCostPrice)) 142 | .abs(); 143 | 144 | print( 145 | "Got total costprice of old $oldTotalCostPrice & items $oldTotalStock"); 146 | // Reapply to get new cp from new updated transaction info 147 | double totalCp = oldTotalCostPrice + newTotalCostPriceOfTransaction; 148 | double totalItems = oldTotalStock + newNoOfItemsInTransaction; 149 | double newCp = totalCp / totalItems; 150 | this._costPrice = newCp; 151 | this._totalStock = totalItems; 152 | } 153 | 154 | static List fromQuerySnapshot(QuerySnapshot snapshot) { 155 | List items = List(); 156 | snapshot.documents.forEach((DocumentSnapshot doc) { 157 | Item item = Item.fromMapObject(doc.data); 158 | item.id = doc.documentID; 159 | items.add(item); 160 | }); 161 | items.sort((a, b) { 162 | return b.used.compareTo(a.used); 163 | }); 164 | return items; 165 | } 166 | 167 | Map toMap() { 168 | var map = Map(); 169 | 170 | map['id'] = _id; 171 | map['name'] = _name; 172 | map['nick_name'] = _nickName; 173 | map['description'] = _description; 174 | map['cost_price'] = _costPrice; 175 | map['marked_price'] = _markedPrice; 176 | map['total_stock'] = _totalStock; 177 | map['last_stock_entry'] = _lastStockEntry; 178 | map['used'] = _used; 179 | map['units'] = _units; 180 | return map; 181 | } 182 | 183 | Item.fromMapObject(Map map) { 184 | this._id = map['id']; 185 | this._description = map['description']; 186 | this._name = map['name']; 187 | this._nickName = map['nick_name']; 188 | this._costPrice = map['cost_price']; 189 | this._markedPrice = map['marked_price']; 190 | this._totalStock = map['total_stock']; 191 | this._lastStockEntry = map['last_stock_entry']; 192 | this._used = map['used']; 193 | this._units = map['units']; 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /lib/utils/window.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_typeahead/flutter_typeahead.dart'; 3 | 4 | import 'package:bk_app/utils/form.dart'; 5 | import 'package:bk_app/app/forms/itemEntryForm.dart'; 6 | import 'package:bk_app/app/forms/salesEntryForm.dart'; 7 | import 'package:bk_app/app/forms/stockEntryForm.dart'; 8 | import 'package:bk_app/app/transactions/monthHistory.dart'; 9 | import 'package:bk_app/app/transactions/transactionList.dart'; 10 | import 'package:bk_app/app/transactions/dueTransactions.dart'; 11 | 12 | class WindowUtils { 13 | static Widget getCard(String label, {color = Colors.white}) { 14 | return Expanded( 15 | child: Card( 16 | color: color, 17 | elevation: 5.0, 18 | child: Center( 19 | heightFactor: 2, 20 | child: Text(label), 21 | ))); 22 | } 23 | 24 | static void navigateToPage(BuildContext context, 25 | {String caller, String target}) async { 26 | Map _stringToForm = { 27 | 'Item Entry': ItemEntryForm(title: target), 28 | 'Sales Entry': SalesEntryForm(title: target), 29 | 'Stock Entry': StockEntryForm(title: target), 30 | 'Month History': MonthlyHistory(), 31 | 'Transactions': TransactionList(), 32 | 'Due Transactions': DueTransaction(), 33 | }; 34 | 35 | if (caller == target) { 36 | return; 37 | } 38 | 39 | var getForm = _stringToForm[target]; 40 | await Navigator.push(context, MaterialPageRoute(builder: (context) { 41 | return getForm; 42 | })); 43 | } 44 | 45 | static moveToLastScreen(BuildContext context, {bool modified = false}) { 46 | debugPrint("I am called. Going back screen"); 47 | Navigator.pop(context, modified); 48 | } 49 | 50 | static void showSnackBar(BuildContext context, String message) { 51 | final snackBar = SnackBar(content: Text(message)); 52 | Scaffold.of(context).showSnackBar(snackBar); 53 | } 54 | 55 | static void showAlertDialog( 56 | BuildContext context, String title, String message, 57 | {onPressed}) { 58 | showDialog( 59 | context: context, 60 | builder: (BuildContext context) { 61 | return AlertDialog( 62 | title: new Text( 63 | title, 64 | ), 65 | content: Padding( 66 | padding: const EdgeInsets.all(8.0), 67 | child: new Text( 68 | message, 69 | ), 70 | ), 71 | actions: [ 72 | new FlatButton( 73 | child: new Text( 74 | "OK", 75 | style: TextStyle(color: Colors.white), 76 | ), 77 | onPressed: () { 78 | moveToLastScreen(context); 79 | if (onPressed != null) { 80 | onPressed(context); 81 | } 82 | }, 83 | color: Theme.of(context).accentColor, 84 | ), 85 | ], 86 | ); 87 | }, 88 | ); 89 | } 90 | 91 | static Widget genButton(BuildContext context, String name, var onPressed) { 92 | return Expanded( 93 | child: RaisedButton( 94 | color: Theme.of(context).accentColor, 95 | textColor: Colors.white, // Theme.of(context).primaryColorLight, 96 | child: Text(name, textScaleFactor: 1.5), 97 | onPressed: onPressed) // RaisedButton Calculate 98 | ); //Expanded 99 | } 100 | 101 | static String _formValidator(String value, String labelText) { 102 | if (value.isEmpty) { 103 | return "Please enter $labelText"; 104 | } 105 | return null; 106 | } 107 | 108 | static Widget genTextField( 109 | {String labelText, 110 | String hintText, 111 | TextStyle textStyle, 112 | TextEditingController controller, 113 | TextInputType keyboardType = TextInputType.text, 114 | int maxLines = 1, 115 | bool obscureText = false, 116 | var onChanged, 117 | var validator = _formValidator, 118 | bool enabled = true}) { 119 | final double _minimumPadding = 5.0; 120 | 121 | return Padding( 122 | padding: EdgeInsets.only(top: _minimumPadding, bottom: _minimumPadding), 123 | child: TextFormField( 124 | enabled: enabled, 125 | keyboardType: keyboardType, 126 | style: textStyle, 127 | maxLines: maxLines, 128 | controller: controller, 129 | obscureText: obscureText, 130 | validator: (String value) { 131 | return validator(value, labelText); 132 | }, 133 | onChanged: (value) { 134 | onChanged(); 135 | }, 136 | decoration: InputDecoration( 137 | labelText: labelText, 138 | labelStyle: textStyle, 139 | hintText: hintText, 140 | errorStyle: TextStyle(color: Colors.redAccent, fontSize: 15.0), 141 | border: 142 | OutlineInputBorder(borderRadius: BorderRadius.circular(5.0))), 143 | ), // Textfield 144 | ); 145 | } // genTextField function 146 | 147 | static Widget genAutocompleteTextField( 148 | {String labelText, 149 | String hintText, 150 | TextStyle textStyle, 151 | TextEditingController controller, 152 | TextInputType keyboardType = TextInputType.text, 153 | BuildContext context, 154 | List suggestions, 155 | bool enabled, 156 | var validator = _formValidator, 157 | var onChanged, 158 | var getSuggestions}) { 159 | return TypeAheadFormField( 160 | textFieldConfiguration: TextFieldConfiguration( 161 | enabled: enabled, 162 | autofocus: true, 163 | style: textStyle, 164 | controller: controller, 165 | decoration: InputDecoration( 166 | labelText: labelText, 167 | labelStyle: textStyle, 168 | hintText: hintText, 169 | errorStyle: TextStyle(color: Colors.redAccent, fontSize: 15.0), 170 | border: 171 | OutlineInputBorder(borderRadius: BorderRadius.circular(5.0))), 172 | ), 173 | validator: (String value) { 174 | return validator(value, labelText); 175 | }, 176 | suggestionsCallback: (givenString) { 177 | onChanged(); 178 | if (suggestions.isEmpty) { 179 | suggestions = getSuggestions(); 180 | } 181 | return FormUtils.genFuzzySuggestionsForItem(givenString, suggestions); 182 | }, 183 | itemBuilder: (context, suggestion) { 184 | return Container( 185 | padding: EdgeInsets.all(5.0), 186 | child: Row( 187 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 188 | children: [ 189 | Text( 190 | suggestion['name'], 191 | style: TextStyle(fontSize: 16.0), 192 | ), 193 | SizedBox(width: 10), 194 | Text( 195 | suggestion['nickName'] ?? '', 196 | ), 197 | ], 198 | )); 199 | }, 200 | onSuggestionSelected: (suggestion) { 201 | controller.text = suggestion['name']; 202 | onChanged(); 203 | }, 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /lib/services/crud.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'package:bk_app/models/user.dart'; 3 | import 'package:cloud_firestore/cloud_firestore.dart'; 4 | 5 | import 'package:bk_app/models/item.dart'; 6 | import 'package:bk_app/models/transaction.dart'; 7 | import 'package:bk_app/services/auth.dart'; 8 | 9 | class CrudHelper { 10 | AuthService auth = AuthService(); 11 | final userData; 12 | CrudHelper({this.userData}); 13 | 14 | // Item 15 | Future addItem(Item item) async { 16 | String targetEmail = this.userData.targetEmail; 17 | if (targetEmail == this.userData.email) { 18 | await Firestore.instance 19 | .collection('$targetEmail-items') 20 | .add(item.toMap()) 21 | .catchError((e) { 22 | print(e); 23 | return 0; 24 | }); 25 | return 1; 26 | } else { 27 | return 0; 28 | } 29 | } 30 | 31 | Future updateItem(Item newItem) async { 32 | String targetEmail = this.userData.targetEmail; 33 | if (targetEmail == this.userData.email) { 34 | await Firestore.instance 35 | .collection('$targetEmail-items') 36 | .document(newItem.id) 37 | .updateData(newItem.toMap()) 38 | .catchError((e) { 39 | print(e); 40 | return 0; 41 | }); 42 | return 1; 43 | } else { 44 | return 0; 45 | } 46 | } 47 | 48 | Future deleteItem(String itemId) async { 49 | String targetEmail = this.userData.targetEmail; 50 | if (targetEmail == this.userData.email) { 51 | await Firestore.instance 52 | .collection('$targetEmail-items') 53 | .document(itemId) 54 | .delete() 55 | .catchError((e) { 56 | print(e); 57 | return 0; 58 | }); 59 | return 1; 60 | } else { 61 | return 0; 62 | } 63 | } 64 | 65 | Stream> getItemStream() { 66 | String email = this.userData.targetEmail; 67 | print("Stream current target email $email"); 68 | return Firestore.instance 69 | .collection('$email-items') 70 | .orderBy('used', descending: true) 71 | .snapshots() 72 | .map(Item.fromQuerySnapshot); 73 | } 74 | 75 | Future getItem(String field, String value) async { 76 | String email = this.userData.targetEmail; 77 | QuerySnapshot itemSnapshots = await Firestore.instance 78 | .collection('$email-items') 79 | .where(field, isEqualTo: value) 80 | .getDocuments() 81 | .catchError((e) { 82 | return null; 83 | }); 84 | 85 | if (itemSnapshots.documents.isEmpty) { 86 | return null; 87 | } 88 | DocumentSnapshot itemSnapshot = itemSnapshots.documents.first; 89 | 90 | if (itemSnapshot.data.isNotEmpty) { 91 | Item item = Item.fromMapObject(itemSnapshot.data); 92 | item.id = itemSnapshot.documentID; 93 | return item; 94 | } else { 95 | return null; 96 | } 97 | } 98 | 99 | Future getItemById(String id) async { 100 | String email = this.userData.targetEmail; 101 | DocumentSnapshot itemSnapshot = await Firestore.instance 102 | .document('$email-items/$id') 103 | .get() 104 | .catchError((e) { 105 | return null; 106 | }); 107 | if (itemSnapshot.data?.isNotEmpty ?? false) { 108 | Item item = Item.fromMapObject(itemSnapshot.data); 109 | item.id = itemSnapshot.documentID; 110 | return item; 111 | } else { 112 | return null; 113 | } 114 | } 115 | 116 | Future> getItems() async { 117 | String email = this.userData.targetEmail; 118 | QuerySnapshot snapshots = await Firestore.instance 119 | .collection('$email-items') 120 | .orderBy('used', descending: true) 121 | .getDocuments(); 122 | List items = List(); 123 | snapshots.documents.forEach((DocumentSnapshot snapshot) { 124 | Item item = Item.fromMapObject(snapshot.data); 125 | item.id = snapshot.documentID; 126 | items.add(item); 127 | }); 128 | return items; 129 | } 130 | 131 | // Item Transactions 132 | Stream> getItemTransactionStream() { 133 | String email = this.userData.targetEmail; 134 | return Firestore.instance 135 | .collection('$email-transactions') 136 | .where('signature', isEqualTo: email) 137 | .snapshots() 138 | .map(ItemTransaction.fromQuerySnapshot); 139 | } 140 | 141 | Future> getItemTransactions() async { 142 | String email = this.userData.targetEmail; 143 | QuerySnapshot snapshots = await Firestore.instance 144 | .collection('$email-transactions') 145 | .where('signature', isEqualTo: email) 146 | .getDocuments(); 147 | return ItemTransaction.fromQuerySnapshot(snapshots); 148 | } 149 | 150 | Future> getPendingTransactions() async { 151 | String email = this.userData.targetEmail; 152 | UserData user = await this.getUserData('email', email); 153 | List roles = user.roles?.keys?.toList() ?? List(); 154 | print("roles $roles"); 155 | if (roles.isEmpty) return List(); 156 | QuerySnapshot snapshots = await Firestore.instance 157 | .collection('$email-transactions') 158 | .where('signature', whereIn: roles) 159 | .getDocuments(); 160 | return ItemTransaction.fromQuerySnapshot(snapshots); 161 | } 162 | 163 | Future> getDueTransactions() async { 164 | String email = this.userData.targetEmail; 165 | QuerySnapshot snapshots = await Firestore.instance 166 | .collection('$email-transactions') 167 | .where('due_amount', isGreaterThan: 0.0) 168 | .getDocuments(); 169 | return ItemTransaction.fromQuerySnapshot(snapshots); 170 | } 171 | 172 | // Users 173 | Future getUserData(String field, String value) async { 174 | QuerySnapshot userDataSnapshots = await Firestore.instance 175 | .collection('users') 176 | .where(field, isEqualTo: value) 177 | .getDocuments() 178 | .catchError((e) { 179 | return null; 180 | }); 181 | if (userDataSnapshots.documents.isEmpty) { 182 | return null; 183 | } 184 | DocumentSnapshot userDataSnapshot = userDataSnapshots.documents.first; 185 | if (userDataSnapshot.data.isNotEmpty) { 186 | UserData userData = UserData.fromMapObject(userDataSnapshot.data); 187 | userData.uid = userDataSnapshot.documentID; 188 | return userData; 189 | } else { 190 | return null; 191 | } 192 | } 193 | 194 | Future getUserDataByUid(String uid) async { 195 | DocumentSnapshot _userData = 196 | await Firestore.instance.document('users/$uid').get().catchError((e) { 197 | print("error getting userdata $e"); 198 | return null; 199 | }); 200 | 201 | if (_userData.data == null) { 202 | print("error getting userdata is $uid"); 203 | return null; 204 | } 205 | 206 | UserData userData = UserData.fromMapObject(_userData.data); 207 | print("here we go $userData & roles ${userData.roles}"); 208 | return userData; 209 | } 210 | 211 | Future updateUserData(UserData userData) async { 212 | print("got userData and roles ${userData.toMap}"); 213 | await Firestore.instance 214 | .collection('users') 215 | .document(userData.uid) 216 | .setData(userData.toMap()) 217 | .catchError((e) { 218 | print(e); 219 | return 0; 220 | }); 221 | return 1; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /lib/app/transactions/salesOverview.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | 4 | import 'package:bk_app/utils/window.dart'; 5 | import 'package:bk_app/utils/form.dart'; 6 | import 'package:bk_app/utils/utils.dart'; 7 | 8 | class SalesOverview { 9 | static Map salesTransactionInfo; 10 | static Map overViewMap; 11 | static TextStyle nameStyle; 12 | static BuildContext context; 13 | 14 | static void showTransactions(appContext, infoMap) async { 15 | context = appContext; 16 | nameStyle = Theme.of(context).textTheme.subhead; 17 | salesTransactionInfo = infoMap; 18 | debugPrint("sales transaction info recieved $infoMap"); 19 | overViewMap = Map()..addAll(salesTransactionInfo); 20 | overViewMap.removeWhere((key, value) { 21 | if (!['Name', 'Profit', 'Item'].contains(key)) { 22 | return true; 23 | } else { 24 | return false; 25 | } 26 | }); 27 | 28 | await showDialog( 29 | context: context, 30 | builder: (BuildContext context) { 31 | return SimpleDialog( 32 | contentPadding: EdgeInsets.zero, 33 | children: [getTransactionOverview(overViewMap)], 34 | ); 35 | }); 36 | } 37 | 38 | static Widget getTransactionOverview(Map overViewMap) { 39 | return Column( 40 | children: [ 41 | SingleChildScrollView( 42 | scrollDirection: Axis.horizontal, 43 | child: DataTable( 44 | columnSpacing: 1.0, 45 | columns: getDataColumns(overViewMap), 46 | rows: getDataRows(overViewMap))), 47 | displayProfitAndDueAmount(), 48 | ], 49 | ); 50 | } 51 | 52 | static List getDataRows(overViewMap) { 53 | List dataRows = List(); 54 | List rowCells = List(); 55 | int rowLength; 56 | int order = 0; 57 | 58 | overViewMap.forEach((key, value) { 59 | rowLength = value.length; 60 | for (int i = 0; i < rowLength; i++) { 61 | if (order == 0) { 62 | rowCells.add([ 63 | DataCell( 64 | Container(width: 100, child: Text("${value[i]}")), 65 | onTap: () => _showTransactionInfoDialog(i), 66 | ) 67 | ]); 68 | } else { 69 | rowCells[i].add(DataCell(Text("${value[i]}", style: nameStyle), 70 | onTap: () => _showTransactionInfoDialog(i))); 71 | } 72 | } 73 | order += 1; 74 | }); 75 | 76 | rowCells.forEach((row) { 77 | dataRows.add(DataRow( 78 | cells: row, 79 | )); 80 | }); 81 | return dataRows; //_sortDataRows(dataRows); 82 | } 83 | 84 | static List getDataColumns(overViewMap) { 85 | List dataCols = List(); 86 | overViewMap.forEach((key, value) { 87 | dataCols.add(DataColumn(label: Text(key))); 88 | }); 89 | return dataCols; 90 | } 91 | 92 | static Widget displayProfitAndDueAmount() { 93 | return Column(children: [ 94 | Row( 95 | children: [ 96 | WindowUtils.getCard("Profit", color: Theme.of(context).cardColor), 97 | WindowUtils.getCard("${calculateProfit()}", 98 | color: Theme.of(context).cardColor), 99 | ], 100 | ), 101 | Visibility( 102 | visible: isEmptyDouble(calculateDueAmount()) ? false : true, 103 | child: Row( 104 | children: [ 105 | WindowUtils.getCard("Due Amount", 106 | color: Theme.of(context).cardColor), 107 | WindowUtils.getCard("${calculateDueAmount()}", 108 | color: Theme.of(context).cardColor), 109 | ], 110 | )), 111 | ]); 112 | } 113 | 114 | static List sortDataRows(List dataRows) { 115 | dataRows.sort((DataRow first, DataRow second) { 116 | DateTime firstDate = getDateTimeFromDataRow(first); 117 | DateTime secondDate = getDateTimeFromDataRow(second); 118 | return firstDate.compareTo(secondDate); 119 | }); 120 | return dataRows; 121 | } 122 | 123 | static DateTime getDateTimeFromDataRow(DataRow row) { 124 | String rawDate = row.cells.last.child.toString(); 125 | String strDate = rawDate.split("\"")[1]; 126 | DateTime date = DateFormat.jm().parseLoose(strDate); 127 | return date; 128 | } 129 | 130 | static double calculateProfit() { 131 | List profits = salesTransactionInfo['Profit']; 132 | double total = profits.reduce((first, second) { 133 | return first + second; 134 | }); 135 | return FormUtils.getShortDouble(total); 136 | } 137 | 138 | static double calculateDueAmount() { 139 | List dueAmounts = salesTransactionInfo['DueAmount']; 140 | debugPrint("dudueAmounts $dueAmounts"); 141 | double total = dueAmounts.reduce((first, second) { 142 | return first + second; 143 | }); 144 | return FormUtils.getShortDouble(total); 145 | } 146 | 147 | static bool isEmptyDouble(double value) { 148 | if (value == 0.0) { 149 | return true; 150 | } else { 151 | return false; 152 | } 153 | } 154 | 155 | static void _showTransactionInfoDialog(int index) async { 156 | ThemeData itemInfoTheme = Theme.of(context); 157 | List dates = salesTransactionInfo['Date']; 158 | String transactionDate = dates[index]; 159 | Map transaction; 160 | Map itemTransactionMap = await AppUtils.getTransactionsForToday(context); 161 | itemTransactionMap.forEach((key, value) { 162 | if (transactionDate == value['date']) { 163 | transaction = value; 164 | } 165 | debugPrint("got transaction $transaction"); 166 | }); 167 | 168 | await showDialog( 169 | context: context, 170 | builder: (BuildContext context) { 171 | return SimpleDialog( 172 | contentPadding: EdgeInsets.zero, 173 | children: [ 174 | Padding( 175 | padding: const EdgeInsets.all(16.0), 176 | child: Column( 177 | crossAxisAlignment: CrossAxisAlignment.stretch, 178 | children: [ 179 | Text("${transaction['date']}", 180 | style: itemInfoTheme.textTheme.subhead), 181 | SizedBox(height: 16.0), 182 | Row( 183 | children: [ 184 | WindowUtils.getCard("Cost Price"), 185 | WindowUtils.getCard(FormUtils.fmtToIntIfPossible( 186 | FormUtils.getShortDouble( 187 | transaction['costPrice']))), 188 | ], 189 | ), 190 | Row( 191 | children: [ 192 | WindowUtils.getCard("Selling Price"), 193 | WindowUtils.getCard(FormUtils.fmtToIntIfPossible( 194 | FormUtils.getShortDouble(transaction['amount']))), 195 | ], 196 | ), 197 | Visibility( 198 | visible: transaction['dueAmount'] == 0.0 || 199 | transaction['dueAmount'] == null 200 | ? false 201 | : true, 202 | child: Row( 203 | children: [ 204 | WindowUtils.getCard("Due Amount"), 205 | WindowUtils.getCard(FormUtils.fmtToIntIfPossible( 206 | FormUtils.getShortDouble( 207 | transaction['dueAmount'] ?? 0.0))), 208 | ], 209 | )), 210 | SizedBox(height: 16.0), 211 | Text("${transaction['description'] ?? ''}", 212 | style: itemInfoTheme.textTheme.subhead), 213 | ]), 214 | ), 215 | ], 216 | ); 217 | }); 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | archive: 5 | dependency: transitive 6 | description: 7 | name: archive 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.0.11" 11 | args: 12 | dependency: transitive 13 | description: 14 | name: args 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "1.5.2" 18 | async: 19 | dependency: transitive 20 | description: 21 | name: async 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "2.4.0" 25 | boolean_selector: 26 | dependency: transitive 27 | description: 28 | name: boolean_selector 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.0.5" 32 | charcode: 33 | dependency: transitive 34 | description: 35 | name: charcode 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.2" 39 | cloud_firestore: 40 | dependency: "direct main" 41 | description: 42 | name: cloud_firestore 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "0.13.4" 46 | cloud_firestore_platform_interface: 47 | dependency: transitive 48 | description: 49 | name: cloud_firestore_platform_interface 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.1.0" 53 | cloud_firestore_web: 54 | dependency: transitive 55 | description: 56 | name: cloud_firestore_web 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "0.1.1" 60 | collection: 61 | dependency: transitive 62 | description: 63 | name: collection 64 | url: "https://pub.dartlang.org" 65 | source: hosted 66 | version: "1.14.11" 67 | convert: 68 | dependency: transitive 69 | description: 70 | name: convert 71 | url: "https://pub.dartlang.org" 72 | source: hosted 73 | version: "2.1.1" 74 | crypto: 75 | dependency: transitive 76 | description: 77 | name: crypto 78 | url: "https://pub.dartlang.org" 79 | source: hosted 80 | version: "2.1.3" 81 | firebase: 82 | dependency: transitive 83 | description: 84 | name: firebase 85 | url: "https://pub.dartlang.org" 86 | source: hosted 87 | version: "7.2.1" 88 | firebase_auth: 89 | dependency: "direct main" 90 | description: 91 | name: firebase_auth 92 | url: "https://pub.dartlang.org" 93 | source: hosted 94 | version: "0.15.5+2" 95 | firebase_auth_platform_interface: 96 | dependency: transitive 97 | description: 98 | name: firebase_auth_platform_interface 99 | url: "https://pub.dartlang.org" 100 | source: hosted 101 | version: "1.1.7" 102 | firebase_auth_web: 103 | dependency: transitive 104 | description: 105 | name: firebase_auth_web 106 | url: "https://pub.dartlang.org" 107 | source: hosted 108 | version: "0.1.2" 109 | firebase_core: 110 | dependency: transitive 111 | description: 112 | name: firebase_core 113 | url: "https://pub.dartlang.org" 114 | source: hosted 115 | version: "0.4.4+2" 116 | firebase_core_platform_interface: 117 | dependency: transitive 118 | description: 119 | name: firebase_core_platform_interface 120 | url: "https://pub.dartlang.org" 121 | source: hosted 122 | version: "1.0.4" 123 | firebase_core_web: 124 | dependency: transitive 125 | description: 126 | name: firebase_core_web 127 | url: "https://pub.dartlang.org" 128 | source: hosted 129 | version: "0.1.1+2" 130 | flutter: 131 | dependency: "direct main" 132 | description: flutter 133 | source: sdk 134 | version: "0.0.0" 135 | flutter_keyboard_visibility: 136 | dependency: transitive 137 | description: 138 | name: flutter_keyboard_visibility 139 | url: "https://pub.dartlang.org" 140 | source: hosted 141 | version: "0.7.0" 142 | flutter_spinkit: 143 | dependency: "direct main" 144 | description: 145 | name: flutter_spinkit 146 | url: "https://pub.dartlang.org" 147 | source: hosted 148 | version: "4.1.2" 149 | flutter_test: 150 | dependency: "direct dev" 151 | description: flutter 152 | source: sdk 153 | version: "0.0.0" 154 | flutter_typeahead: 155 | dependency: "direct main" 156 | description: 157 | name: flutter_typeahead 158 | url: "https://pub.dartlang.org" 159 | source: hosted 160 | version: "1.8.0" 161 | flutter_web_plugins: 162 | dependency: transitive 163 | description: flutter 164 | source: sdk 165 | version: "0.0.0" 166 | http: 167 | dependency: transitive 168 | description: 169 | name: http 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "0.12.0+4" 173 | http_parser: 174 | dependency: transitive 175 | description: 176 | name: http_parser 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "3.1.3" 180 | image: 181 | dependency: transitive 182 | description: 183 | name: image 184 | url: "https://pub.dartlang.org" 185 | source: hosted 186 | version: "2.1.4" 187 | intl: 188 | dependency: "direct main" 189 | description: 190 | name: intl 191 | url: "https://pub.dartlang.org" 192 | source: hosted 193 | version: "0.16.1" 194 | js: 195 | dependency: transitive 196 | description: 197 | name: js 198 | url: "https://pub.dartlang.org" 199 | source: hosted 200 | version: "0.6.1+1" 201 | matcher: 202 | dependency: transitive 203 | description: 204 | name: matcher 205 | url: "https://pub.dartlang.org" 206 | source: hosted 207 | version: "0.12.6" 208 | meta: 209 | dependency: transitive 210 | description: 211 | name: meta 212 | url: "https://pub.dartlang.org" 213 | source: hosted 214 | version: "1.1.8" 215 | path: 216 | dependency: transitive 217 | description: 218 | name: path 219 | url: "https://pub.dartlang.org" 220 | source: hosted 221 | version: "1.6.4" 222 | pedantic: 223 | dependency: transitive 224 | description: 225 | name: pedantic 226 | url: "https://pub.dartlang.org" 227 | source: hosted 228 | version: "1.8.0+1" 229 | petitparser: 230 | dependency: transitive 231 | description: 232 | name: petitparser 233 | url: "https://pub.dartlang.org" 234 | source: hosted 235 | version: "2.4.0" 236 | plugin_platform_interface: 237 | dependency: transitive 238 | description: 239 | name: plugin_platform_interface 240 | url: "https://pub.dartlang.org" 241 | source: hosted 242 | version: "1.0.2" 243 | provider: 244 | dependency: "direct main" 245 | description: 246 | name: provider 247 | url: "https://pub.dartlang.org" 248 | source: hosted 249 | version: "3.2.0" 250 | quiver: 251 | dependency: transitive 252 | description: 253 | name: quiver 254 | url: "https://pub.dartlang.org" 255 | source: hosted 256 | version: "2.0.5" 257 | sky_engine: 258 | dependency: transitive 259 | description: flutter 260 | source: sdk 261 | version: "0.0.99" 262 | source_span: 263 | dependency: transitive 264 | description: 265 | name: source_span 266 | url: "https://pub.dartlang.org" 267 | source: hosted 268 | version: "1.5.5" 269 | stack_trace: 270 | dependency: transitive 271 | description: 272 | name: stack_trace 273 | url: "https://pub.dartlang.org" 274 | source: hosted 275 | version: "1.9.3" 276 | stream_channel: 277 | dependency: transitive 278 | description: 279 | name: stream_channel 280 | url: "https://pub.dartlang.org" 281 | source: hosted 282 | version: "2.0.0" 283 | string_scanner: 284 | dependency: transitive 285 | description: 286 | name: string_scanner 287 | url: "https://pub.dartlang.org" 288 | source: hosted 289 | version: "1.0.5" 290 | term_glyph: 291 | dependency: transitive 292 | description: 293 | name: term_glyph 294 | url: "https://pub.dartlang.org" 295 | source: hosted 296 | version: "1.1.0" 297 | test_api: 298 | dependency: transitive 299 | description: 300 | name: test_api 301 | url: "https://pub.dartlang.org" 302 | source: hosted 303 | version: "0.2.11" 304 | typed_data: 305 | dependency: transitive 306 | description: 307 | name: typed_data 308 | url: "https://pub.dartlang.org" 309 | source: hosted 310 | version: "1.1.6" 311 | vector_math: 312 | dependency: transitive 313 | description: 314 | name: vector_math 315 | url: "https://pub.dartlang.org" 316 | source: hosted 317 | version: "2.0.8" 318 | xml: 319 | dependency: transitive 320 | description: 321 | name: xml 322 | url: "https://pub.dartlang.org" 323 | source: hosted 324 | version: "3.5.0" 325 | sdks: 326 | dart: ">=2.7.0-dev <3.0.0" 327 | flutter: ">=1.12.13+hotfix.4 <2.0.0" 328 | -------------------------------------------------------------------------------- /lib/app/itemlist.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'package:bk_app/app/wrapper.dart'; 5 | import 'package:bk_app/app/forms/itemEntryForm.dart'; 6 | import 'package:bk_app/app/forms/salesEntryForm.dart'; 7 | import 'package:bk_app/app/forms/stockEntryForm.dart'; 8 | import 'package:bk_app/models/item.dart'; 9 | import 'package:bk_app/models/user.dart'; 10 | import 'package:bk_app/services/crud.dart'; 11 | import 'package:bk_app/utils/scaffold.dart'; 12 | import 'package:bk_app/utils/form.dart'; 13 | import 'package:bk_app/utils/window.dart'; 14 | import 'package:bk_app/utils/loading.dart'; 15 | 16 | class ItemList extends StatefulWidget { 17 | @override 18 | State createState() { 19 | return ItemListState(); 20 | } 21 | } 22 | 23 | class ItemListState extends State { 24 | static CrudHelper crudHelper; 25 | Stream> items; 26 | static List _itemsList; 27 | List itemsList = List(); 28 | static UserData userData; 29 | bool showSearchBar = false; 30 | 31 | @override 32 | void initState() { 33 | super.initState(); 34 | } 35 | 36 | @override 37 | void didChangeDependencies() { 38 | super.didChangeDependencies(); 39 | userData = Provider.of(context); 40 | if (userData != null) { 41 | crudHelper = CrudHelper(userData: userData); 42 | _updateListView(); 43 | } 44 | } 45 | 46 | @override 47 | Widget build(BuildContext context) { 48 | print("I am calu"); 49 | if (userData == null) { 50 | return Wrapper(); 51 | } 52 | return Scaffold( 53 | appBar: this.showSearchBar 54 | ? null 55 | : AppBar( 56 | title: Text("Items"), 57 | actions: [ 58 | IconButton( 59 | tooltip: "Search", 60 | icon: const Icon(Icons.search), 61 | onPressed: () { 62 | setState(() { 63 | this.showSearchBar = true; 64 | }); 65 | }, 66 | ), 67 | ], 68 | ), 69 | drawer: CustomScaffold.setDrawer(context), 70 | body: this.showSearchBar ? getSearchView() : getItemListView(), 71 | floatingActionButton: FloatingActionButton( 72 | onPressed: () { 73 | navigateToDetail(Item(''), 'Create Item'); 74 | }, 75 | tooltip: 'Add Item', 76 | child: Icon(Icons.add), 77 | ), 78 | ); 79 | } 80 | 81 | Widget getItemListView() { 82 | ThemeData localTheme = Theme.of(context); 83 | return StreamBuilder( 84 | stream: this.items, 85 | builder: (context, snapshot) { 86 | if (snapshot.hasData) { 87 | return ListView.builder( 88 | itemCount: snapshot.data.length, 89 | itemBuilder: (BuildContext context, int index) { 90 | Item item = snapshot.data[index]; 91 | return GestureDetector( 92 | key: Key(item.name), 93 | child: ListTile( 94 | leading: CircleAvatar( 95 | backgroundColor: Colors.red, 96 | child: Icon(Icons.keyboard_arrow_right), 97 | ), 98 | title: _getNameAndPrice(context, item), 99 | subtitle: Text(item.nickName ?? '', 100 | style: localTheme.textTheme.body1), 101 | onTap: () { 102 | this._showItemInfoDialog(item); 103 | }, 104 | onLongPress: () { 105 | navigateToDetail(item, 'Edit Item'); 106 | }, 107 | ), 108 | onHorizontalDragEnd: (DragEndDetails details) { 109 | if (details.primaryVelocity < 0.0) { 110 | this._initiateTransaction("Stock Entry", item); 111 | } else if (details.primaryVelocity > 0.0) { 112 | this._initiateTransaction("Sales Entry", item); 113 | } 114 | }); 115 | }, 116 | ); 117 | } else { 118 | return Loading(); 119 | } 120 | }); 121 | } 122 | 123 | void _showItemInfoDialog(Item item) async { 124 | ThemeData itemInfoTheme = Theme.of(context); 125 | 126 | await showDialog( 127 | context: context, 128 | builder: (BuildContext context) { 129 | return SimpleDialog( 130 | contentPadding: EdgeInsets.zero, 131 | children: [ 132 | // TODO Image(), 133 | Padding( 134 | padding: const EdgeInsets.all(16.0), 135 | child: Column( 136 | crossAxisAlignment: CrossAxisAlignment.stretch, 137 | children: [ 138 | Text(item.name, style: itemInfoTheme.textTheme.display1), 139 | Text(item.nickName ?? '', 140 | style: itemInfoTheme.textTheme.subhead.copyWith( 141 | fontStyle: FontStyle.italic, 142 | )), 143 | Row( 144 | children: [ 145 | WindowUtils.getCard("Marked Price"), 146 | WindowUtils.getCard("${item.markedPrice ?? "N/A"}"), 147 | ], 148 | ), 149 | Row( 150 | children: [ 151 | WindowUtils.getCard("Current CP"), 152 | WindowUtils.getCard(FormUtils.fmtToIntIfPossible( 153 | FormUtils.getShortDouble(item.costPrice ?? 0.0))), 154 | ], 155 | ), 156 | Visibility( 157 | visible: userData.checkStock ?? true, 158 | child: Row( 159 | children: [ 160 | WindowUtils.getCard("Total Stocks"), 161 | WindowUtils.getCard(FormUtils.fmtToIntIfPossible( 162 | item.totalStock)), 163 | ], 164 | )), 165 | SizedBox(height: 16.0), 166 | Text("${item.description ?? ''}", 167 | style: itemInfoTheme.textTheme.body1), 168 | ]), 169 | ), 170 | ], 171 | ); 172 | }); 173 | } 174 | 175 | void navigateToDetail(Item item, String name) async { 176 | bool result = 177 | await Navigator.push(context, MaterialPageRoute(builder: (context) { 178 | return ItemEntryForm(title: name, item: item, forEdit: true); 179 | })); 180 | 181 | if (result == true) { 182 | this._updateListView(); 183 | } 184 | } 185 | 186 | void _initiateTransaction(String formName, item) async { 187 | String itemName = item.name; 188 | Map formMap = { 189 | 'Sales Entry': SalesEntryForm(swipeData: item, title: "Sell $itemName"), 190 | 'Stock Entry': StockEntryForm(swipeData: item, title: "Buy $itemName") 191 | }; 192 | 193 | await Navigator.push( 194 | context, MaterialPageRoute(builder: (context) => formMap[formName])); 195 | } 196 | 197 | void _updateListView() async { 198 | _itemsList = await crudHelper.getItems(); 199 | setState(() { 200 | this.items = crudHelper.getItemStream(); 201 | this.itemsList = _itemsList; 202 | }); 203 | } 204 | 205 | static Widget _getNameAndPrice(BuildContext context, Item item) { 206 | TextStyle nameStyle = Theme.of(context).textTheme.subhead; 207 | String name = item.name; 208 | String markedPrice = item.markedPrice ?? ''; 209 | String finalMarkedPrice = markedPrice.isEmpty ? "" : "Rs $markedPrice"; 210 | return Row(children: [ 211 | Expanded(flex: 1, child: Text(name, style: nameStyle)), 212 | Visibility( 213 | visible: finalMarkedPrice == '' ? false : true, 214 | child: Column( 215 | crossAxisAlignment: CrossAxisAlignment.center, 216 | children: [ 217 | SizedBox(height: 10.0, width: 1.0), 218 | Text("Price", style: nameStyle.copyWith(fontSize: 14.0)), 219 | Text(finalMarkedPrice, style: nameStyle), 220 | ], 221 | )), 222 | ]); 223 | } 224 | 225 | void _modifyItemList(String val) async { 226 | List itemsMapList = 227 | _itemsList.map((Item item) => item.toMap()).toList(); 228 | List _suggestions = 229 | FormUtils.genFuzzySuggestionsForItem(val, itemsMapList); 230 | this.itemsList = _suggestions.map((Map itemMap) { 231 | return Item.fromMapObject(itemMap); 232 | }).toList(); 233 | } 234 | 235 | Widget getSearchView({type}) { 236 | print("search view && ${this.itemsList}"); 237 | ThemeData localTheme = Theme.of(context); 238 | return Container( 239 | child: Column(children: [ 240 | SizedBox(height: 30.0), 241 | Padding( 242 | padding: const EdgeInsets.only(right: 8.0, left: 8.0), 243 | child: Row(children: [ 244 | Expanded( 245 | child: TextField( 246 | autofocus: true, 247 | onChanged: (value) { 248 | setState(() { 249 | _modifyItemList(value); 250 | }); 251 | }, 252 | decoration: InputDecoration( 253 | contentPadding: EdgeInsets.zero, 254 | hintText: "Search", 255 | prefixIcon: Icon(Icons.search), 256 | border: OutlineInputBorder( 257 | borderRadius: BorderRadius.all(Radius.circular(25.0)))), 258 | )), 259 | IconButton( 260 | icon: Icon(Icons.cancel), 261 | onPressed: () { 262 | setState(() => this.showSearchBar = false); 263 | }), 264 | ])), 265 | Expanded( 266 | child: ListView.builder( 267 | padding: EdgeInsets.zero, 268 | itemCount: this.itemsList.length, 269 | itemBuilder: (BuildContext context, int index) { 270 | Item item = this.itemsList[index]; 271 | return GestureDetector( 272 | key: Key(item.name), 273 | child: ListTile( 274 | leading: CircleAvatar( 275 | backgroundColor: Colors.red, 276 | child: Icon(Icons.keyboard_arrow_right), 277 | ), 278 | title: _getNameAndPrice(context, item), 279 | subtitle: Text(item.nickName ?? '', 280 | style: localTheme.textTheme.body1), 281 | onTap: () { 282 | this._showItemInfoDialog(item); 283 | }, 284 | onLongPress: () { 285 | navigateToDetail(item, 'Edit Item'); 286 | }, 287 | ), 288 | onHorizontalDragEnd: (DragEndDetails details) { 289 | if (details.primaryVelocity < 0.0) { 290 | this._initiateTransaction("Stock Entry", item); 291 | } else if (details.primaryVelocity > 0.0) { 292 | this._initiateTransaction("Sales Entry", item); 293 | } 294 | }); 295 | })) 296 | ])); 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /lib/app/transactions/transactionList.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:intl/intl.dart'; 3 | import 'package:provider/provider.dart'; 4 | 5 | import 'package:bk_app/app/wrapper.dart'; 6 | import 'package:bk_app/app/forms/salesEntryForm.dart'; 7 | import 'package:bk_app/app/forms/stockEntryForm.dart'; 8 | import 'package:bk_app/app/transactions/salesOverview.dart'; 9 | import 'package:bk_app/models/transaction.dart'; 10 | import 'package:bk_app/models/user.dart'; 11 | import 'package:bk_app/utils/scaffold.dart'; 12 | import 'package:bk_app/utils/form.dart'; 13 | import 'package:bk_app/utils/window.dart'; 14 | import 'package:bk_app/utils/cache.dart'; 15 | import 'package:bk_app/utils/loading.dart'; 16 | import 'package:bk_app/utils/utils.dart'; 17 | import 'package:bk_app/services/crud.dart'; 18 | 19 | class TransactionList extends StatefulWidget { 20 | @override 21 | State createState() { 22 | return TransactionListState(); 23 | } 24 | } 25 | 26 | class TransactionListState extends State { 27 | static CrudHelper crudHelper; 28 | Map itemMapCache = Map(); 29 | Stream> transactions; 30 | List pendingTransactions; 31 | bool loading = true; 32 | static UserData userData; 33 | Map currentMonthHistory = Map(); 34 | 35 | @override 36 | void initState() { 37 | _initializeItemMapCache(); 38 | super.initState(); 39 | } 40 | 41 | @override 42 | void didChangeDependencies() { 43 | super.didChangeDependencies(); 44 | userData = Provider.of(context); 45 | if (userData != null) { 46 | crudHelper = CrudHelper(userData: userData); 47 | _updateListView(); 48 | } 49 | } 50 | 51 | @override 52 | Widget build(BuildContext context) { 53 | if (userData == null) { 54 | return Wrapper(); 55 | } 56 | List viewTabs = [ 57 | Tab(text: "History"), 58 | Tab(text: "Pending"), 59 | ]; 60 | return DefaultTabController( 61 | length: viewTabs.length, 62 | child: Scaffold( 63 | appBar: AppBar( 64 | title: Text("Transactions"), 65 | bottom: TabBar(tabs: viewTabs), 66 | ), 67 | drawer: CustomScaffold.setDrawer(context), 68 | body: TabBarView(children: [ 69 | getTransactionListView(), 70 | showPendingTransactions(), 71 | ]), 72 | floatingActionButton: FloatingActionButton( 73 | onPressed: () { 74 | this._showTransactionProfit(); 75 | }, 76 | tooltip: 'Caclulate Profit', 77 | child: Icon(Icons.book), 78 | ), 79 | floatingActionButtonLocation: 80 | FloatingActionButtonLocation.centerDocked, 81 | bottomNavigationBar: buildBottomAppBar(context), 82 | )); 83 | } 84 | 85 | static BottomAppBar buildBottomAppBar(BuildContext context) { 86 | return BottomAppBar( 87 | shape: CircularNotchedRectangle(), 88 | color: Theme.of(context).primaryColor, 89 | child: Row( 90 | children: [ 91 | IconButton( 92 | icon: Icon(Icons.card_travel), 93 | onPressed: () => WindowUtils.navigateToPage(context, 94 | target: 'Transactions', caller: 'Transactions')), 95 | SizedBox(width: 20.0), 96 | IconButton( 97 | icon: Icon(Icons.access_alarm), 98 | onPressed: () => WindowUtils.navigateToPage(context, 99 | target: 'Due Transactions', caller: 'Transactions')), 100 | SizedBox(width: 150.0), 101 | IconButton( 102 | icon: Icon(Icons.history), 103 | onPressed: () => WindowUtils.navigateToPage(context, 104 | target: 'Month History', caller: 'Transactions')) 105 | ], 106 | ), 107 | ); 108 | } 109 | 110 | StreamBuilder getTransactionListView() { 111 | return StreamBuilder( 112 | stream: this.transactions, 113 | builder: (context, snapshot) { 114 | if (snapshot.hasData && !loading) { 115 | return ListView.builder( 116 | itemCount: snapshot.data.length, 117 | itemBuilder: (BuildContext context, int index) { 118 | ItemTransaction transaction = snapshot.data[index]; 119 | return Card( 120 | color: Colors.white, 121 | elevation: 2.0, 122 | child: ListTile( 123 | leading: Column( 124 | crossAxisAlignment: CrossAxisAlignment.center, 125 | children: [ 126 | SizedBox(height: 10.0), 127 | Text("Amount"), 128 | Text( 129 | "Rs. ${FormUtils.fmtToIntIfPossible(transaction.amount)}"), 130 | ], 131 | ), 132 | title: 133 | getDescription(context, transaction, this.itemMapCache), 134 | onTap: () { 135 | navigateToDetail(context, transaction, 'Edit Item', 136 | updateListView: this._updateListView); 137 | }, 138 | )); 139 | }, 140 | ); 141 | } else { 142 | return Loading(); 143 | } 144 | }, 145 | ); 146 | } 147 | 148 | Widget showPendingTransactions() { 149 | if (this.pendingTransactions != null) { 150 | return ListView.builder( 151 | itemCount: this.pendingTransactions.length, 152 | itemBuilder: (BuildContext context, int index) { 153 | ItemTransaction transaction = this.pendingTransactions[index]; 154 | return Card( 155 | color: Colors.white, 156 | elevation: 2.0, 157 | child: ListTile( 158 | leading: Column( 159 | crossAxisAlignment: CrossAxisAlignment.center, 160 | children: [ 161 | SizedBox(height: 10.0), 162 | Text("Amount"), 163 | Text("Rs. ${transaction.amount}"), 164 | ], 165 | ), 166 | title: getDescription(context, transaction, this.itemMapCache), 167 | subtitle: Text(transaction.signature), 168 | onTap: () { 169 | navigateToDetail(context, transaction, 'Edit Item', 170 | updateListView: this._updateListView); 171 | }, 172 | )); 173 | }, 174 | ); 175 | } else { 176 | return Loading(); 177 | } 178 | } 179 | 180 | void _showTransactionProfit() async { 181 | Map itemTransactionMap = await AppUtils.getTransactionsForToday(context); 182 | Map salesTransactions = Map(); 183 | 184 | // transactions of type = 0 means outgoing(sales) 185 | //1 means incoming(stockentry) 186 | itemTransactionMap.forEach((transactionId, value) { 187 | debugPrint("got value $value"); 188 | if (value['type'] == 0) { 189 | salesTransactions[transactionId] = value; 190 | } 191 | }); 192 | 193 | if (salesTransactions.isEmpty) { 194 | WindowUtils.showAlertDialog( 195 | context, "Failed!", "Sales history for today is empty"); 196 | return; 197 | } 198 | 199 | List names = List(); 200 | List items = List(); 201 | List costPrices = List(); 202 | List sellingPrices = List(); 203 | List profits = List(); 204 | List dueAmounts = List(); 205 | List dates = List(); 206 | 207 | Map overViewMap = Map(); 208 | 209 | try { 210 | salesTransactions.forEach((key, value) { 211 | String itemId = value['itemId']; 212 | String name; 213 | try { 214 | name = this.itemMapCache[itemId][0]; 215 | } catch (e) { 216 | return; 217 | } 218 | int noOfItems = value['items'].toInt(); 219 | double costPrice = FormUtils.getShortDouble(value['costPrice']); 220 | double dueAmount = FormUtils.getShortDouble(value['dueAmount'] ?? 0.0); 221 | double sellingPrice = FormUtils.getShortDouble(value['amount']); 222 | String date = value['date']; 223 | double _profit = noOfItems * sellingPrice - noOfItems * costPrice; 224 | double profit = FormUtils.getShortDouble(_profit); 225 | 226 | names.add(name); 227 | items.add(noOfItems); 228 | costPrices.add(costPrice); 229 | sellingPrices.add(sellingPrice); 230 | dates.add(date); 231 | profits.add(profit); 232 | dueAmounts.add(dueAmount); 233 | }); 234 | 235 | overViewMap = { 236 | 'Name': names, 237 | 'Item': items, 238 | 'CP': costPrices, 239 | 'SP': sellingPrices, 240 | 'Profit': profits, 241 | 'DueAmount': dueAmounts, 242 | 'Date': dates 243 | }; 244 | debugPrint("sending overview map $overViewMap"); 245 | } catch (e) { 246 | debugPrint("Profita calc error $e"); 247 | } 248 | SalesOverview.showTransactions(context, overViewMap); 249 | } 250 | 251 | static Widget getDescription( 252 | BuildContext context, ItemTransaction transaction, Map cache) { 253 | ThemeData localTheme = Theme.of(context); 254 | String itemName = getItemName(transaction, cache: cache); 255 | String action = transaction.type.isOdd ? "Bought" : "Sold"; 256 | String itemNo = FormUtils.fmtToIntIfPossible(transaction.items); 257 | 258 | DateTime transactionDate = 259 | DateFormat.yMMMd().add_jms().parse(transaction.date); 260 | String dayYear = DateFormat.yMMMd().format(transactionDate); 261 | String time = DateFormat.jm().format(transactionDate); 262 | 263 | return Row(children: [ 264 | Expanded( 265 | flex: 1, 266 | child: Column(children: [ 267 | Text("$action: $itemNo units\n$itemName", 268 | style: localTheme.textTheme.subhead), 269 | ])), 270 | Column( 271 | crossAxisAlignment: CrossAxisAlignment.center, 272 | children: [ 273 | SizedBox(height: 10.0, width: 1.0), 274 | Text(dayYear, 275 | style: localTheme.textTheme.body1.copyWith(fontSize: 10.0)), 276 | Text(time, style: localTheme.textTheme.body2), 277 | ], 278 | ), 279 | ]); 280 | } 281 | 282 | static void navigateToDetail( 283 | BuildContext context, ItemTransaction transaction, String name, 284 | {updateListView}) async { 285 | var form; 286 | if (transaction.type == 0) { 287 | form = 288 | SalesEntryForm(title: name, transaction: transaction, forEdit: true); 289 | } else { 290 | form = StockEntryForm( 291 | title: name, 292 | transaction: transaction, 293 | forEdit: true, 294 | ); 295 | } 296 | 297 | bool result = 298 | await Navigator.push(context, MaterialPageRoute(builder: (context) { 299 | return form; 300 | })); 301 | 302 | if (result == true) { 303 | updateListView(); 304 | } 305 | } 306 | 307 | void _updateListView() async { 308 | List pendingTransactions = 309 | await crudHelper.getPendingTransactions(); 310 | setState(() { 311 | this.transactions = crudHelper.getItemTransactionStream(); 312 | this.pendingTransactions = pendingTransactions.reversed.toList(); 313 | }); 314 | } 315 | 316 | void _initializeItemMapCache() async { 317 | this.itemMapCache = await StartupCache().itemMap; 318 | setState(() { 319 | this.loading = false; 320 | }); 321 | } 322 | 323 | static String getItemName(ItemTransaction transaction, {cache}) { 324 | if (cache.isEmpty) { 325 | debugPrint("item cache still empty"); 326 | return 'N/A'; 327 | } 328 | 329 | Map map = cache; 330 | List infoList = map[transaction.itemId]; 331 | String itemName = infoList?.first ?? 'N/A'; 332 | return itemName; 333 | } 334 | } 335 | -------------------------------------------------------------------------------- /lib/app/settings.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'package:bk_app/models/user.dart'; 5 | import 'package:bk_app/services/crud.dart'; 6 | import 'package:bk_app/utils/loading.dart'; 7 | import 'package:bk_app/utils/scaffold.dart'; 8 | import 'package:bk_app/utils/window.dart'; 9 | import 'package:bk_app/utils/cache.dart'; 10 | import 'package:bk_app/app/wrapper.dart'; 11 | import 'package:bk_app/services/auth.dart'; 12 | 13 | class Setting extends StatefulWidget { 14 | @override 15 | SettingState createState() => SettingState(); 16 | } 17 | 18 | class SettingState extends State { 19 | final _formKey = GlobalKey(); 20 | 21 | static CrudHelper crudHelper; 22 | UserData userData; 23 | static AuthService _auth = AuthService(); 24 | 25 | Map currentMonthHistory = Map(); 26 | final double _minimumPadding = 5.0; 27 | bool checkStock; 28 | TextEditingController targetEmailController = TextEditingController(); 29 | 30 | @override 31 | void didChangeDependencies() { 32 | super.didChangeDependencies(); 33 | this.userData = Provider.of(context); 34 | if (this.userData != null) { 35 | crudHelper = CrudHelper(userData: this.userData); 36 | this.checkStock = this.userData.checkStock ?? true; 37 | } 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | if (this.userData == null) { 43 | return Wrapper(); 44 | } 45 | return Scaffold( 46 | appBar: AppBar( 47 | leading: Icon(Icons.settings), 48 | title: Text("Settings"), 49 | actions: [ 50 | Row( 51 | children: [ 52 | IconButton( 53 | icon: Icon(Icons.person), 54 | onPressed: () async { 55 | setState(() { 56 | _auth.signOut(); 57 | }); 58 | }), 59 | Padding( 60 | padding: EdgeInsets.only(right: 8.0), 61 | child: Text('Log out')), 62 | ], 63 | ) 64 | ]), 65 | drawer: CustomScaffold.setDrawer(context), 66 | body: this.getSettings(), 67 | ); 68 | } 69 | 70 | Widget getSettings() { 71 | final localTheme = Theme.of(context); 72 | return Material( 73 | child: Padding( 74 | padding: EdgeInsets.only(left: 10.0, right: 10.0), 75 | child: FutureBuilder( 76 | future: crudHelper.getUserDataByUid(this.userData.uid), 77 | builder: (context, snapshot) { 78 | if (snapshot.hasData) { 79 | UserData _userData = snapshot.data; 80 | this.targetEmailController.text = 81 | _userData.targetEmail ?? ''; 82 | return ListView(children: [ 83 | Form( 84 | key: _formKey, 85 | child: Column(children: [ 86 | 87 | Container( 88 | padding: EdgeInsets.all(10.0), 89 | child: Text('App settings', 90 | style: localTheme.textTheme.title), 91 | ), 92 | 93 | Row(children: [ 94 | Text("Enforce stock checking", 95 | style: localTheme.textTheme.subhead), 96 | Checkbox( 97 | value: this.checkStock, 98 | onChanged: (bool val) { 99 | setState(() => this.checkStock = val); 100 | }), 101 | ]), 102 | 103 | Container( 104 | padding: EdgeInsets.all(10.0), 105 | child: Text('Account settings', 106 | style: localTheme.textTheme.title), 107 | ), 108 | SizedBox(height: 20.0), 109 | TextFormField( 110 | decoration: InputDecoration( 111 | labelText: "Target Email", 112 | border: OutlineInputBorder( 113 | borderRadius: BorderRadius.circular(5.0)), 114 | ), 115 | controller: this.targetEmailController, 116 | validator: (val) { 117 | if (val.isEmpty) { 118 | setState(() { 119 | this.targetEmailController.text = 120 | this.userData.email; 121 | }); 122 | } 123 | return null; 124 | }), 125 | SizedBox(height: 10.0), 126 | TextFormField( 127 | enabled: false, 128 | initialValue: _userData.email, 129 | decoration: InputDecoration( 130 | labelText: "Email", 131 | border: OutlineInputBorder( 132 | borderRadius: BorderRadius.circular(5.0)), 133 | )), 134 | 135 | SizedBox(height: 20.0), 136 | Row(children: [ 137 | Expanded( 138 | flex: 1, 139 | child: Container( 140 | child: Text('Roles', 141 | style: localTheme.textTheme.title)), 142 | ), 143 | RaisedButton( 144 | color: Colors.blue[400], 145 | child: Text( 146 | 'Add roles', 147 | style: TextStyle(color: Colors.white), 148 | ), 149 | onPressed: () { 150 | setState(() { 151 | this.showDialogForRoles(); 152 | }); 153 | }), 154 | ]), 155 | this.showRolesMapping(_userData), 156 | SizedBox(height: 20.0), 157 | 158 | // save 159 | Padding( 160 | padding: EdgeInsets.only( 161 | bottom: _minimumPadding * 3, 162 | top: 3 * _minimumPadding), 163 | child: Row(children: [ 164 | WindowUtils.genButton( 165 | context, "Save", this.checkAndSave), 166 | Container( 167 | width: _minimumPadding, 168 | ), 169 | WindowUtils.genButton(context, "Discard", 170 | () => Navigator.pop(context)) 171 | ]) // Row 172 | 173 | ), // Paddin 174 | ]), 175 | ) 176 | ]); 177 | } else { 178 | return Loading(); 179 | } 180 | }))); 181 | } 182 | 183 | void checkAndSave() async { 184 | if (_formKey.currentState.validate()) { 185 | print("currentTargetEmail is ${this.targetEmailController.text}"); 186 | this.userData.targetEmail = this.targetEmailController.text; 187 | this.userData.checkStock = this.checkStock; 188 | if (!await validateTargetEmail(this.userData)) { 189 | WindowUtils.showAlertDialog(context, "Failed", 190 | "You don't have access rights to this target email\n${this.userData.targetEmail}"); 191 | return; 192 | } 193 | print("saving this.userData ${this.userData.roles}"); 194 | crudHelper.updateUserData(this.userData); 195 | await StartupCache(userData: userData).itemMap; 196 | Navigator.pop(context); 197 | } 198 | } 199 | 200 | static Future validateTargetEmail(userData) async { 201 | print("userdata email is ${userData.email} and ${userData.targetEmail}"); 202 | if (userData.email == userData.targetEmail) return true; 203 | 204 | UserData targetUserData = 205 | await CrudHelper().getUserData('email', userData.targetEmail); 206 | if (targetUserData?.roles?.isEmpty ?? true) { 207 | return false; 208 | } else { 209 | if (targetUserData.roles.containsKey(userData.email)) 210 | return true; 211 | else 212 | return false; 213 | } 214 | } 215 | 216 | Widget showRolesMapping(UserData userData) { 217 | double _minimumPadding = 5.0; 218 | return userData.roles?.isNotEmpty ?? false 219 | ? Padding( 220 | padding: EdgeInsets.only(right: 1.0, left: 1.0), 221 | child: ListView.builder( 222 | shrinkWrap: true, 223 | itemCount: userData.roles.keys?.length ?? 0, 224 | itemBuilder: (BuildContext context, int index) { 225 | String email = userData.roles.keys.toList()[index]; 226 | String role = userData.roles[email]; 227 | return Card( 228 | elevation: 5.0, 229 | child: Row(children: [ 230 | Expanded( 231 | flex: 1, 232 | child: Container( 233 | margin: EdgeInsets.only( 234 | top: _minimumPadding * 3, 235 | bottom: _minimumPadding * 3), 236 | padding: EdgeInsets.all(_minimumPadding), 237 | child: Text(email, softWrap: true), 238 | )), 239 | Expanded( 240 | child: Padding( 241 | padding: EdgeInsets.all(_minimumPadding), 242 | child: Text(role, softWrap: true), 243 | )), 244 | GestureDetector( 245 | child: Icon(Icons.edit), 246 | onTap: () { 247 | setState(() { 248 | showDialogForRoles(email: email, role: role); 249 | }); 250 | }), 251 | ])); 252 | })) 253 | : SizedBox(width: 20.0); 254 | } 255 | 256 | void showDialogForRoles({String email, String role}) { 257 | final _roleFormKey = GlobalKey(); 258 | showDialog( 259 | context: context, 260 | builder: (BuildContext context) { 261 | return AlertDialog( 262 | elevation: 5.0, 263 | title: Text( 264 | "Add roles", 265 | ), 266 | content: Padding( 267 | padding: const EdgeInsets.all(8.0), 268 | child: Form( 269 | key: _roleFormKey, 270 | child: ListView(children: [ 271 | TextFormField( 272 | initialValue: email ?? '', 273 | decoration: InputDecoration( 274 | labelText: "Email", 275 | ), 276 | onChanged: (val) => setState(() => email = val), 277 | validator: (val) { 278 | if (val?.isEmpty ?? false) { 279 | return "Please fill this field"; 280 | } else { 281 | return null; 282 | } 283 | }), 284 | SizedBox(width: 20.0), 285 | TextFormField( 286 | initialValue: role ?? '', 287 | decoration: InputDecoration( 288 | labelText: "Role", 289 | ), 290 | onChanged: (val) => setState(() => role = val), 291 | validator: (val) { 292 | if (val?.isEmpty ?? false) { 293 | return "Please fill this field"; 294 | } else { 295 | return null; 296 | } 297 | }), 298 | SizedBox(width: 20.0), 299 | RaisedButton( 300 | color: Colors.blue[400], 301 | child: Text( 302 | 'Add', 303 | style: TextStyle(color: Colors.white), 304 | ), 305 | onPressed: () async { 306 | if (_roleFormKey.currentState.validate()) { 307 | if (this.userData.roles == null) { 308 | this.userData.roles = Map(); 309 | } 310 | this.userData.roles[email] = role; 311 | print( 312 | "updating this.userData ${this.userData.roles}"); 313 | crudHelper.updateUserData(this.userData); 314 | setState(() => Navigator.pop(context)); 315 | } 316 | }), 317 | RaisedButton( 318 | color: Colors.red[400], 319 | child: Text( 320 | 'Delete', 321 | style: TextStyle(color: Colors.white), 322 | ), 323 | onPressed: () async { 324 | if (_roleFormKey.currentState.validate()) { 325 | if (this.userData.roles.containsKey(email)) { 326 | this.userData.roles.remove(email); 327 | crudHelper.updateUserData(this.userData); 328 | } 329 | role = ''; 330 | email = ''; 331 | setState(() => Navigator.pop(context)); 332 | } else { 333 | setState(() => Navigator.pop(context)); 334 | } 335 | }), 336 | ])))); 337 | }); 338 | } 339 | } 340 | -------------------------------------------------------------------------------- /lib/app/forms/stockEntryForm.dart: -------------------------------------------------------------------------------- 1 | import 'package:bk_app/models/user.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | import 'package:bk_app/app/wrapper.dart'; 5 | import 'package:bk_app/utils/window.dart'; 6 | import 'package:bk_app/utils/scaffold.dart'; 7 | import 'package:bk_app/utils/form.dart'; 8 | import 'package:bk_app/utils/cache.dart'; 9 | import 'package:bk_app/models/item.dart'; 10 | import 'package:bk_app/models/transaction.dart'; 11 | import 'package:bk_app/services/crud.dart'; 12 | import 'package:provider/provider.dart'; 13 | 14 | class StockEntryForm extends StatefulWidget { 15 | final String title; 16 | final ItemTransaction transaction; 17 | final bool forEdit; 18 | final Item swipeData; 19 | 20 | StockEntryForm({this.transaction, this.title, this.forEdit, this.swipeData}); 21 | 22 | @override 23 | State createState() { 24 | return _StockEntryFormState(this.title, this.transaction); 25 | } 26 | } 27 | 28 | class _StockEntryFormState extends State { 29 | String title; 30 | ItemTransaction transaction; 31 | _StockEntryFormState(this.title, this.transaction); 32 | 33 | // Variables 34 | var _formKey = GlobalKey(); 35 | final double _minimumPadding = 5.0; 36 | static CrudHelper crudHelper; 37 | static UserData userData; 38 | 39 | List _forms = ['Sales Entry', 'Stock Entry', 'Item Entry']; 40 | String formName; 41 | String disclaimerText = ''; 42 | String stringUnderName = ''; 43 | String _currentFormSelected; 44 | String tempItemId; 45 | List itemNamesAndNicknames = List(); 46 | bool enableAdvancedFields = false; 47 | 48 | List units = List(); 49 | String selectedUnit = ''; 50 | TextEditingController itemNameController = TextEditingController(); 51 | TextEditingController itemNumberController = TextEditingController(); 52 | TextEditingController costPriceController = TextEditingController(); 53 | TextEditingController markedPriceController = TextEditingController(); 54 | TextEditingController duePriceController = TextEditingController(); 55 | TextEditingController descriptionController = TextEditingController(); 56 | 57 | @override 58 | void initState() { 59 | super.initState(); 60 | this.formName = _forms[1]; 61 | this._currentFormSelected = formName; 62 | _initializeItemNamesAndNicknamesMapCache(); 63 | } 64 | 65 | @override 66 | void didChangeDependencies() { 67 | super.didChangeDependencies(); 68 | userData = Provider.of(context); 69 | if (userData != null) { 70 | crudHelper = CrudHelper(userData: userData); 71 | _initiateTransactionData(); 72 | } 73 | } 74 | 75 | void _initiateTransactionData() { 76 | if (this.transaction == null) { 77 | debugPrint("Building own transaction obj"); 78 | this.transaction = ItemTransaction(1, null, 0.0, 0.0, ''); 79 | } 80 | 81 | if (this.widget.swipeData != null) { 82 | Item item = this.widget.swipeData; 83 | this.units = item.units?.keys?.toList() ?? List(); 84 | if (this.units.isNotEmpty) { 85 | this.units.add(''); 86 | } 87 | } 88 | 89 | if (this.transaction.id != null) { 90 | debugPrint("Getting transanction obj"); 91 | this.itemNumberController.text = 92 | FormUtils.fmtToIntIfPossible(this.transaction.items); 93 | this.costPriceController.text = 94 | FormUtils.fmtToIntIfPossible(this.transaction.amount); 95 | this.descriptionController.text = this.transaction.description ?? ''; 96 | this.duePriceController.text = 97 | FormUtils.fmtToIntIfPossible(this.transaction.dueAmount); 98 | if (this.descriptionController.text.isNotEmpty || 99 | this.duePriceController.text.isNotEmpty) { 100 | this.enableAdvancedFields = true; 101 | } 102 | 103 | Future itemFuture = crudHelper.getItemById( 104 | this.transaction.itemId, 105 | ); 106 | itemFuture.then((item) { 107 | if (item == null) { 108 | setState(() { 109 | this.disclaimerText = 110 | 'Orphan Transaction: The item associated with this transaction has been deleted'; 111 | }); 112 | } else { 113 | debugPrint("Got item snapshot data to fill form $item"); 114 | this.itemNameController.text = '${item.name}'; 115 | this.markedPriceController.text = item.markedPrice; 116 | this.tempItemId = item.id; 117 | this._addUnitsIfPresent(item); 118 | } 119 | }); 120 | } 121 | } 122 | 123 | Widget buildForm(BuildContext context) { 124 | TextStyle textStyle = Theme.of(context).textTheme.title; 125 | 126 | return Column(children: [ 127 | DropdownButton( 128 | items: _forms.map((String dropDownStringItem) { 129 | return DropdownMenuItem( 130 | value: dropDownStringItem, 131 | child: Text(dropDownStringItem), 132 | ); // DropdownMenuItem 133 | }).toList(), 134 | 135 | onChanged: (String newValueSelected) { 136 | WindowUtils.navigateToPage(context, 137 | caller: this.formName, target: newValueSelected); 138 | }, //onChanged 139 | 140 | value: _currentFormSelected, 141 | ), // DropdownButton 142 | 143 | Expanded( 144 | child: Form( 145 | key: this._formKey, 146 | child: Padding( 147 | padding: EdgeInsets.all(_minimumPadding * 2), 148 | child: ListView(children: [ 149 | // Any disclaimer for user 150 | Visibility( 151 | visible: this.disclaimerText.isNotEmpty, 152 | child: Padding( 153 | padding: EdgeInsets.all(_minimumPadding), 154 | child: Text(this.disclaimerText)), 155 | ), 156 | 157 | // Item name 158 | Visibility( 159 | visible: this.widget.swipeData == null ? true : false, 160 | child: WindowUtils.genAutocompleteTextField( 161 | labelText: "Item name", 162 | hintText: "Name of item you bought", 163 | textStyle: textStyle, 164 | controller: itemNameController, 165 | getSuggestions: this._getAutoCompleteSuggestions, 166 | onChanged: () { 167 | return setState(() { 168 | this.updateItemName(); 169 | }); 170 | }, 171 | suggestions: this.itemNamesAndNicknames)), 172 | 173 | Visibility( 174 | visible: stringUnderName.isNotEmpty, 175 | child: Padding( 176 | padding: EdgeInsets.all(_minimumPadding), 177 | child: Text(this.stringUnderName)), 178 | ), 179 | 180 | // No of items 181 | Row(children: [ 182 | Expanded( 183 | flex: 2, 184 | child: WindowUtils.genTextField( 185 | labelText: "Quantity", 186 | hintText: "No of items", 187 | textStyle: textStyle, 188 | controller: this.itemNumberController, 189 | keyboardType: TextInputType.number, 190 | validator: (String value, String labelText) { 191 | if (value == '0.0' || 192 | value == '0' || 193 | value.isEmpty) { 194 | return 'Quantity is zero or empty'; 195 | } else { 196 | return null; 197 | } 198 | }, 199 | onChanged: () {}, 200 | )), 201 | 202 | Visibility( 203 | visible: this.units.isNotEmpty, 204 | child: Padding( 205 | padding: EdgeInsets.only(right: 5.0, left: 10.0), 206 | child: DropdownButton( 207 | items: this.units.map((dropDownStringItem) { 208 | return DropdownMenuItem( 209 | value: dropDownStringItem, 210 | child: Text(dropDownStringItem), 211 | ); // DropdownMenuItem 212 | }).toList(), 213 | 214 | onChanged: (String newValueSelected) { 215 | setState(() { 216 | this.selectedUnit = newValueSelected; 217 | }); 218 | }, //onChanged 219 | 220 | value: this.selectedUnit, 221 | ))), // DropdownButton 222 | ]), 223 | 224 | // Cost price 225 | WindowUtils.genTextField( 226 | labelText: "Total cost price", 227 | textStyle: textStyle, 228 | controller: this.costPriceController, 229 | keyboardType: TextInputType.number, 230 | onChanged: () {}, 231 | ), 232 | 233 | // Marked price 234 | WindowUtils.genTextField( 235 | labelText: "Expected selling price", 236 | hintText: "Price per item", 237 | textStyle: textStyle, 238 | controller: this.markedPriceController, 239 | keyboardType: TextInputType.number, 240 | onChanged: () {}, 241 | ), 242 | 243 | // Checkbox 244 | Row( 245 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 246 | children: [ 247 | Checkbox( 248 | onChanged: (value) { 249 | setState(() => this.enableAdvancedFields = value); 250 | }, 251 | value: this.enableAdvancedFields), 252 | Text( 253 | "Show advanced fields", 254 | style: textStyle, 255 | ), 256 | ], 257 | ), 258 | 259 | // Unpaid price 260 | Visibility( 261 | visible: this.enableAdvancedFields, 262 | child: WindowUtils.genTextField( 263 | labelText: "Unpaid amount", 264 | hintText: "Amount remaining to be collected", 265 | textStyle: textStyle, 266 | controller: this.duePriceController, 267 | keyboardType: TextInputType.number, 268 | onChanged: this.updateDuePrice, 269 | validator: (value, labelText) {})), 270 | 271 | // Description 272 | Visibility( 273 | visible: this.enableAdvancedFields, 274 | child: WindowUtils.genTextField( 275 | labelText: "Description", 276 | hintText: "Any notes for this transaction", 277 | textStyle: textStyle, 278 | maxLines: 3, 279 | controller: this.descriptionController, 280 | validator: (value, labelText) {}, 281 | onChanged: () { 282 | return setState(() { 283 | this.updateTransactionDescription(); 284 | }); 285 | })), 286 | 287 | // save 288 | Padding( 289 | padding: EdgeInsets.only( 290 | bottom: 3 * _minimumPadding, 291 | top: 3 * _minimumPadding), 292 | child: Row(children: [ 293 | WindowUtils.genButton( 294 | this.context, "Save", this.checkAndSave), 295 | Container( 296 | width: 5.0, 297 | ), 298 | WindowUtils.genButton( 299 | this.context, "Delete", this._delete) 300 | ]) // Row 301 | 302 | ), // Paddin 303 | ]) //List view 304 | ) // Padding 305 | )) 306 | ]); // Container 307 | } 308 | 309 | @override 310 | Widget build(BuildContext context) { 311 | if (userData == null) { 312 | return Wrapper(); 313 | } 314 | return WillPopScope( 315 | onWillPop: () { 316 | // When user presses the back button write some code to control 317 | return WindowUtils.moveToLastScreen(context); 318 | }, 319 | child: CustomScaffold.setScaffold(context, this.title, buildForm)); 320 | } 321 | 322 | void updateItemName() { 323 | String name = this.itemNameController.text; 324 | Future itemFuture = crudHelper.getItem( 325 | "name", 326 | name, 327 | ); 328 | itemFuture.then((item) { 329 | if (item == null) { 330 | debugPrint("Update item name got snapshot $item"); 331 | this.stringUnderName = 'Unregistered item'; 332 | this.tempItemId = null; 333 | setState(() => this.units = List()); 334 | } else { 335 | this.stringUnderName = ''; 336 | this.tempItemId = item.id; 337 | setState(() => this._addUnitsIfPresent(item)); 338 | } 339 | }, onError: (e) { 340 | debugPrint('UpdateitemName Error:: $e'); 341 | }); 342 | } 343 | 344 | void updateDuePrice() { 345 | double amount = 0.0; 346 | if (this.duePriceController.text.isNotEmpty) { 347 | amount = double.parse(this.duePriceController.text).abs(); 348 | } 349 | this.transaction.dueAmount = amount; 350 | } 351 | 352 | void updateTransactionDescription() { 353 | this.transaction.description = this.descriptionController.text; 354 | } 355 | 356 | void clearFieldsAndTransaction() { 357 | this.itemNameController.text = ''; 358 | this.itemNumberController.text = ''; 359 | this.costPriceController.text = ''; 360 | this.markedPriceController.text = ''; 361 | this.duePriceController.text = ''; 362 | this.descriptionController.text = ''; 363 | this.enableAdvancedFields = false; 364 | this.units = List(); 365 | this.selectedUnit = ''; 366 | this.transaction = ItemTransaction(1, null, 0.0, 0.0, ''); 367 | } 368 | 369 | void _addUnitsIfPresent(item) { 370 | if (item.units != null) { 371 | this.units = item.units.keys.toList(); 372 | this.units.add(''); 373 | } else { 374 | this.units = List(); 375 | } 376 | } 377 | 378 | void checkAndSave() { 379 | if (this._formKey.currentState.validate()) { 380 | this._save(); 381 | } 382 | } 383 | 384 | // Save data to database 385 | void _save() async { 386 | Item item; 387 | 388 | if (this.widget.swipeData != null) { 389 | debugPrint("Using swipeData to save"); 390 | item = this.widget.swipeData; 391 | } else { 392 | item = await crudHelper.getItemById( 393 | this.tempItemId, 394 | ); 395 | } 396 | 397 | if (item == null) { 398 | WindowUtils.showAlertDialog( 399 | this.context, "Failed!", "Item not registered"); 400 | return; 401 | } 402 | 403 | String itemId = item.id; 404 | double unitMultiple = 1.0; 405 | if (this.selectedUnit != '') { 406 | if (item.units?.containsKey(this.selectedUnit) ?? false) { 407 | unitMultiple = item.units[this.selectedUnit]; 408 | } 409 | } 410 | double items = 411 | double.parse(this.itemNumberController.text).abs() * unitMultiple; 412 | double totalCostPrice = double.parse(this.costPriceController.text).abs(); 413 | 414 | if (this.transaction.id != null && 415 | this.transaction.itemId == itemId && 416 | !_beingApproved()) { 417 | // Condition 1st: 418 | // If there is id then its oviously update case 419 | // Condition 2nd: 420 | // Confirm that the updated transaction points to same item otherwise insert case 421 | // Condition 3rd: 422 | // We also label a transaction as new (only here) if transaction by other is being owner approved 423 | // This is because the item is not modified (during db save) when other create/modify it. 424 | 425 | // Update case. 426 | if (item.lastStockEntry == this.transaction.date) { 427 | // For latest transaction 428 | if (userData.checkStock ?? true) { 429 | item.modifyLatestStockEntry(this.transaction, items, totalCostPrice); 430 | } else { 431 | item.costPrice = totalCostPrice / items; 432 | } 433 | } 434 | } else { 435 | // Insert case 436 | if (userData.checkStock ?? true) { 437 | var newCpAndTotalStock = 438 | item.getNewCostPriceAndStock(totalCostPrice, items); 439 | item.costPrice = newCpAndTotalStock[0]; 440 | item.totalStock = newCpAndTotalStock[1]; 441 | } else { 442 | item.costPrice = totalCostPrice / items; 443 | } 444 | } 445 | 446 | this.transaction.itemId = itemId; 447 | this.transaction.items = items; 448 | item.markedPrice = this.markedPriceController.text; 449 | this.transaction.amount = totalCostPrice; 450 | 451 | String message = await FormUtils.saveTransactionAndUpdateItem( 452 | this.transaction, item, 453 | userData: userData); 454 | 455 | this.saveCallback(message); 456 | } 457 | 458 | bool _beingApproved() { 459 | // If current user is database owner and trnsaction is not from him he is approving it. 460 | return FormUtils.isDatabaseOwner(userData) && 461 | !FormUtils.isTransactionOwner(userData, this.transaction); 462 | } 463 | 464 | // Delete item data 465 | void _delete() async { 466 | if (this.transaction.id == null) { 467 | // Case 1: Abandon new item creation 468 | this.clearFieldsAndTransaction(); 469 | WindowUtils.showAlertDialog(context, "Status", 'Item not created'); 470 | return; 471 | } else { 472 | Item item = await crudHelper.getItemById( 473 | this.transaction.itemId, 474 | ); 475 | 476 | // Case 2: Delete item from database after user confirms again 477 | WindowUtils.showAlertDialog(context, "Delete?", 478 | "This action is very dangerous and you may lose vital information. Delete?", 479 | onPressed: (buildContext) { 480 | FormUtils.deleteTransactionAndUpdateItem( 481 | this.saveCallback, this.transaction, item, userData); 482 | }); 483 | } 484 | } 485 | 486 | void saveCallback(String message) { 487 | if (message.isEmpty) { 488 | this.clearFieldsAndTransaction(); 489 | if (this.widget.forEdit ?? false) { 490 | WindowUtils.moveToLastScreen(this.context, modified: true); 491 | } 492 | 493 | // Success 494 | WindowUtils.showAlertDialog( 495 | this.context, "Status", 'Stock updated successfully'); 496 | } else { 497 | // Failure 498 | WindowUtils.showAlertDialog(this.context, 'Failed!', message); 499 | } 500 | } 501 | 502 | void _initializeItemNamesAndNicknamesMapCache() async { 503 | Map itemMap = await StartupCache().itemMap; 504 | List cacheItemAndNickNames = List(); 505 | if (itemMap.isNotEmpty) { 506 | itemMap.forEach((key, value) { 507 | Map nameNickNameMap = {'name': value.first, 'nickName': value.last}; 508 | cacheItemAndNickNames.add(nameNickNameMap); 509 | }); 510 | } 511 | debugPrint("Ok list of items and nicKnames $cacheItemAndNickNames"); 512 | setState(() { 513 | this.itemNamesAndNicknames = cacheItemAndNickNames; 514 | }); 515 | } 516 | 517 | List _getAutoCompleteSuggestions() { 518 | // A way for autocomplete generator to access the itemNamesAndNicknames proprety of this class 519 | // Sometimes at the start of program empty suggestions gets passed and there is no way to update that. 520 | return this.itemNamesAndNicknames; 521 | } 522 | } 523 | -------------------------------------------------------------------------------- /lib/app/forms/salesEntryForm.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:provider/provider.dart'; 3 | 4 | import 'package:bk_app/app/wrapper.dart'; 5 | import 'package:bk_app/models/item.dart'; 6 | import 'package:bk_app/models/transaction.dart'; 7 | import 'package:bk_app/models/user.dart'; 8 | import 'package:bk_app/services/crud.dart'; 9 | import 'package:bk_app/utils/form.dart'; 10 | import 'package:bk_app/utils/window.dart'; 11 | import 'package:bk_app/utils/cache.dart'; 12 | import 'package:bk_app/utils/scaffold.dart'; 13 | import 'package:bk_app/utils/loading.dart'; 14 | 15 | class SalesEntryForm extends StatefulWidget { 16 | final String title; 17 | final ItemTransaction transaction; 18 | final bool forEdit; 19 | // When an item is right swiped from itemList a quick sales form is presented 20 | // This form obiously shouldNot have itemName field so the itemList will pass 21 | // the name of item to this form 22 | final Item swipeData; 23 | 24 | SalesEntryForm({this.title, this.transaction, this.forEdit, this.swipeData}); 25 | 26 | @override 27 | State createState() { 28 | return _SalesEntryFormState(this.title, this.transaction); 29 | } 30 | } 31 | 32 | class _SalesEntryFormState extends State { 33 | // Variables 34 | String title; 35 | ItemTransaction transaction; 36 | _SalesEntryFormState(this.title, this.transaction); 37 | 38 | var _formKey = GlobalKey(); 39 | final double _minimumPadding = 5.0; 40 | List _forms = ['Sales Entry', 'Stock Entry', 'Item Entry']; 41 | String formName; 42 | String _currentFormSelected; 43 | 44 | static CrudHelper crudHelper; 45 | static UserData userData; 46 | List itemNamesAndNicknames = List(); 47 | String disclaimerText = ''; 48 | String stringUnderName = ''; 49 | String tempItemId; 50 | bool enableAdvancedFields = false; 51 | 52 | List units = List(); 53 | String selectedUnit = ''; 54 | TextEditingController itemNameController = TextEditingController(); 55 | TextEditingController itemNumberController = TextEditingController(); 56 | TextEditingController sellingPriceController = TextEditingController(); 57 | TextEditingController duePriceController = TextEditingController(); 58 | TextEditingController descriptionController = TextEditingController(); 59 | TextEditingController costPriceController = TextEditingController(); 60 | 61 | @override 62 | void initState() { 63 | super.initState(); 64 | this.formName = _forms[0]; 65 | this._currentFormSelected = this.formName; 66 | } 67 | 68 | @override 69 | void didChangeDependencies() { 70 | super.didChangeDependencies(); 71 | userData = Provider.of(context); 72 | if (userData != null) { 73 | crudHelper = CrudHelper(userData: userData); 74 | _initiateTransactionData(); 75 | _initializeItemNamesAndNicknamesMapCache(); 76 | } else { 77 | Loading(); 78 | } 79 | } 80 | 81 | void _initiateTransactionData() { 82 | if (this.transaction == null) { 83 | debugPrint("Building own transaction obj"); 84 | this.transaction = ItemTransaction(0, null, 0.0, 0.0, ''); 85 | } 86 | if (this.widget.swipeData != null) { 87 | Item item = this.widget.swipeData; 88 | this.units = item.units?.keys?.toList() ?? List(); 89 | if (this.units.isNotEmpty) { 90 | this.units.add(''); 91 | } 92 | } 93 | 94 | if (this.transaction.id != null) { 95 | debugPrint("Getting transaction obj"); 96 | this.itemNumberController.text = 97 | FormUtils.fmtToIntIfPossible(this.transaction.items); 98 | this.sellingPriceController.text = 99 | FormUtils.fmtToIntIfPossible(this.transaction.amount); 100 | this.costPriceController.text = 101 | FormUtils.fmtToIntIfPossible(this.transaction.costPrice); 102 | this.descriptionController.text = this.transaction.description ?? ''; 103 | this.duePriceController.text = 104 | FormUtils.fmtToIntIfPossible(this.transaction.dueAmount); 105 | if (this.descriptionController.text.isNotEmpty || 106 | (this.transaction.dueAmount ?? 0.0) != 0.0) { 107 | setState(() { 108 | this.enableAdvancedFields = true; 109 | }); 110 | } 111 | 112 | Future itemFuture = crudHelper.getItemById( 113 | this.transaction.itemId, 114 | ); 115 | itemFuture.then((item) { 116 | if (item == null) { 117 | setState(() { 118 | this.disclaimerText = 119 | 'Orphan Transaction: The item associated with this transaction has been deleted'; 120 | }); 121 | } else { 122 | debugPrint("hi this item is $item"); 123 | this.itemNameController.text = '${item.name}'; 124 | this.tempItemId = item.id; 125 | this._addUnitsIfPresent(item); 126 | } 127 | }); 128 | } 129 | } 130 | 131 | Widget buildForm(BuildContext context) { 132 | TextStyle textStyle = Theme.of(context).textTheme.title; 133 | 134 | debugPrint("making build form"); 135 | return Column(children: [ 136 | DropdownButton( 137 | items: _forms.map((String dropDownStringItem) { 138 | return DropdownMenuItem( 139 | value: dropDownStringItem, 140 | child: Text(dropDownStringItem), 141 | ); // DropdownMenuItem 142 | }).toList(), 143 | 144 | onChanged: (String newValueSelected) { 145 | WindowUtils.navigateToPage(context, 146 | caller: this.formName, target: newValueSelected); 147 | }, //onChanged 148 | 149 | value: _currentFormSelected, 150 | ), // DropdownButton 151 | 152 | Expanded( 153 | child: Form( 154 | key: this._formKey, 155 | child: Padding( 156 | padding: EdgeInsets.all(_minimumPadding * 2), 157 | child: ListView(children: [ 158 | // Any disclaimer for user 159 | Visibility( 160 | visible: this.disclaimerText.isNotEmpty, 161 | child: Padding( 162 | padding: EdgeInsets.all(_minimumPadding), 163 | child: Text(this.disclaimerText)), 164 | ), 165 | 166 | // Item name 167 | Visibility( 168 | visible: this.widget.swipeData == null ? true : false, 169 | child: WindowUtils.genAutocompleteTextField( 170 | labelText: "Item name", 171 | hintText: "Name of item sold", 172 | textStyle: textStyle, 173 | controller: itemNameController, 174 | getSuggestions: this._getAutoCompleteSuggestions, 175 | onChanged: () { 176 | return setState(() { 177 | this.updateItemName(); 178 | }); 179 | }, 180 | suggestions: this.itemNamesAndNicknames), 181 | ), 182 | 183 | Visibility( 184 | visible: stringUnderName.isNotEmpty, 185 | child: Padding( 186 | padding: EdgeInsets.all(_minimumPadding), 187 | child: Text(this.stringUnderName)), 188 | ), 189 | 190 | Row(children: [ 191 | // No of items 192 | Expanded( 193 | flex: 2, 194 | child: WindowUtils.genTextField( 195 | labelText: "Quantity", 196 | hintText: "No of items sold", 197 | textStyle: textStyle, 198 | controller: this.itemNumberController, 199 | keyboardType: TextInputType.number, 200 | validator: (String value, String labelText) { 201 | if (value == '0.0' || 202 | value == '0' || 203 | value.isEmpty) { 204 | return "$labelText is empty or zero"; 205 | } else { 206 | return null; 207 | } 208 | }, 209 | onChanged: () {}), 210 | ), 211 | Visibility( 212 | visible: this.units.isNotEmpty, 213 | child: Padding( 214 | padding: EdgeInsets.only(right: 5.0, left: 10.0), 215 | child: DropdownButton( 216 | items: this.units.map((dropDownStringItem) { 217 | return DropdownMenuItem( 218 | value: dropDownStringItem, 219 | child: Text(dropDownStringItem), 220 | ); // DropdownMenuItem 221 | }).toList(), 222 | 223 | onChanged: (String newValueSelected) { 224 | setState(() { 225 | this.selectedUnit = newValueSelected; 226 | }); 227 | }, //onChanged 228 | 229 | value: this.selectedUnit, 230 | ))), // DropdownButton 231 | ]), 232 | 233 | // Selling price 234 | WindowUtils.genTextField( 235 | labelText: "Total selling price", 236 | textStyle: textStyle, 237 | controller: this.sellingPriceController, 238 | keyboardType: TextInputType.number, 239 | onChanged: this.updateSellingPrice, 240 | ), 241 | 242 | // Cost price 243 | Visibility( 244 | visible: 245 | this.transaction.costPrice == null ? false : true, 246 | child: WindowUtils.genTextField( 247 | labelText: "Cost price", 248 | hintText: "Cost price per item", 249 | textStyle: textStyle, 250 | controller: this.costPriceController, 251 | keyboardType: TextInputType.number, 252 | onChanged: this.updateCostPrice, 253 | )), 254 | 255 | Row( 256 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 257 | children: [ 258 | Checkbox( 259 | onChanged: (value) { 260 | setState(() => this.enableAdvancedFields = value); 261 | }, 262 | value: this.enableAdvancedFields), 263 | Text( 264 | "Show advanced fields", 265 | style: textStyle, 266 | ), 267 | ], 268 | ), 269 | 270 | // Unpaid price 271 | Visibility( 272 | visible: this.enableAdvancedFields, 273 | child: WindowUtils.genTextField( 274 | labelText: "Unpaid amount", 275 | hintText: "Amount remaining to be collected", 276 | textStyle: textStyle, 277 | controller: this.duePriceController, 278 | keyboardType: TextInputType.number, 279 | onChanged: this.updateDuePrice, 280 | validator: (value, labelText) {})), 281 | 282 | // Description 283 | Visibility( 284 | visible: this.enableAdvancedFields, 285 | child: WindowUtils.genTextField( 286 | labelText: "Description", 287 | hintText: "Any notes for this transaction", 288 | textStyle: textStyle, 289 | maxLines: 3, 290 | controller: this.descriptionController, 291 | validator: (value, labelText) {}, 292 | onChanged: () { 293 | return setState(() { 294 | this.updateTransactionDescription(); 295 | }); 296 | })), 297 | 298 | // save 299 | Padding( 300 | padding: EdgeInsets.only( 301 | bottom: _minimumPadding * 3, 302 | top: 3 * _minimumPadding), 303 | child: Row(children: [ 304 | WindowUtils.genButton( 305 | context, "Save", this.checkAndSave), 306 | Container( 307 | width: _minimumPadding, 308 | ), 309 | WindowUtils.genButton(context, "Delete", this._delete) 310 | ]) // Row 311 | 312 | ), // Paddin 313 | ]) //List view 314 | ) // Padding 315 | )) 316 | ]); // return 317 | } 318 | 319 | @override 320 | Widget build(BuildContext context) { 321 | if (userData == null) { 322 | return Wrapper(); 323 | } 324 | return WillPopScope( 325 | onWillPop: () { 326 | // When user presses the back button write some code to control 327 | return WindowUtils.moveToLastScreen(context); 328 | }, 329 | child: CustomScaffold.setScaffold(context, title, buildForm)); 330 | } 331 | 332 | void updateSellingPrice() { 333 | this.transaction.amount = 334 | double.parse(this.sellingPriceController.text).abs(); 335 | } 336 | 337 | void updateCostPrice() { 338 | this.transaction.costPrice = 339 | double.parse(this.costPriceController.text).abs(); 340 | } 341 | 342 | void updateDuePrice() { 343 | double amount = 0.0; 344 | if (this.duePriceController.text.isNotEmpty) { 345 | amount = double.parse(this.duePriceController.text).abs(); 346 | } 347 | this.transaction.dueAmount = amount; 348 | debugPrint("Updated the due price to ${this.transaction.dueAmount}"); 349 | } 350 | 351 | void updateTransactionDescription() { 352 | this.transaction.description = this.descriptionController.text; 353 | } 354 | 355 | void updateItemName() { 356 | var name = this.itemNameController.text; 357 | Future itemFuture = crudHelper.getItem( 358 | "name", 359 | name, 360 | ); 361 | itemFuture.then((item) { 362 | if (item == null) { 363 | this.stringUnderName = 'Unregistered name'; 364 | this.tempItemId = null; 365 | setState(() => this.units = List()); 366 | } else { 367 | this.stringUnderName = ''; 368 | this.tempItemId = item.id; 369 | setState(() => this._addUnitsIfPresent(item)); 370 | } 371 | }, onError: (e) { 372 | debugPrint('UpdateitemName Error:: $e'); 373 | }); 374 | } 375 | 376 | void clearFieldsAndTransaction() { 377 | this.itemNameController.text = ''; 378 | this.itemNumberController.text = ''; 379 | this.sellingPriceController.text = ''; 380 | this.costPriceController.text = ''; 381 | this.descriptionController.text = ''; 382 | this.duePriceController.text = ''; 383 | this.enableAdvancedFields = false; 384 | this.units = List(); 385 | this.selectedUnit = ''; 386 | this.transaction = ItemTransaction(0, null, 0.0, 0.0, ''); 387 | } 388 | 389 | void _addUnitsIfPresent(item) { 390 | if (item.units != null) { 391 | this.units = item.units.keys.toList(); 392 | this.units.add(''); 393 | } else { 394 | this.units = List(); 395 | } 396 | } 397 | 398 | void checkAndSave() { 399 | debugPrint("Save button clicked"); 400 | if (this._formKey.currentState.validate()) { 401 | debugPrint("validated"); 402 | this._save(); 403 | } 404 | } 405 | 406 | // Save data to database 407 | void _save() async { 408 | void _alertFail(message) { 409 | WindowUtils.showAlertDialog(context, "Failed!", message); 410 | } 411 | 412 | Item item; 413 | if (this.widget.swipeData != null) { 414 | debugPrint("Using swipeData to save"); 415 | item = this.widget.swipeData; 416 | } else { 417 | item = await crudHelper 418 | .getItemById( 419 | this.tempItemId, 420 | ) 421 | .catchError((e) { 422 | return null; 423 | }); 424 | } 425 | 426 | debugPrint("Saving sales item is $item"); 427 | if (item == null) { 428 | _alertFail("Item not registered"); 429 | return; 430 | } 431 | 432 | String itemId = item.id; 433 | double unitMultiple = 1.0; 434 | if (this.selectedUnit != '') { 435 | if (item.units?.containsKey(this.selectedUnit) ?? false) { 436 | unitMultiple = item.units[this.selectedUnit]; 437 | } 438 | } 439 | double items = 440 | double.parse(this.itemNumberController.text).abs() * unitMultiple; 441 | 442 | // Additional checks. 443 | if ((this.transaction.id == null && this.transaction.itemId != itemId) || 444 | _beingApproved()) { 445 | // Case insert 446 | if ((userData.checkStock ?? true) && item.totalStock < items) { 447 | _alertFail("Empty stock. Cannot sell."); 448 | return; 449 | } 450 | 451 | // Cp of transaction is set only once during insert. 452 | this.transaction.costPrice = item.costPrice; 453 | item.decreaseStock(items); 454 | } else { 455 | // Case update 456 | debugPrint( 457 | "updating transaction and this is current stock ${item.totalStock} of ${item.name}"); 458 | double netAddition = items - this.transaction.items; 459 | 460 | if ((userData.checkStock ?? true) && item.totalStock < netAddition) { 461 | _alertFail("Empty or insufficient stock.\nCannot sell."); 462 | return; 463 | } else { 464 | item.decreaseStock(netAddition); 465 | } 466 | } 467 | 468 | this.transaction.itemId = itemId; 469 | this.transaction.items = items; 470 | 471 | String message = await FormUtils.saveTransactionAndUpdateItem( 472 | this.transaction, item, 473 | userData: userData); 474 | 475 | this.saveCallback(message); 476 | } 477 | 478 | bool _beingApproved() { 479 | // If current user is database owner and trnsaction is not from him he is approving it. 480 | return FormUtils.isDatabaseOwner(userData) && 481 | !FormUtils.isTransactionOwner(userData, this.transaction); 482 | } 483 | 484 | void _delete() async { 485 | if (this.transaction.id == null) { 486 | // Case 1: Abandon new item creation 487 | this.clearFieldsAndTransaction(); 488 | WindowUtils.showAlertDialog(context, "Status", 'Item not created'); 489 | return; 490 | } else { 491 | // Initialize the item to reset it. 492 | Item item = await crudHelper.getItemById(this.transaction.itemId); 493 | 494 | // Case 2: Delete item from database after user confirms again 495 | WindowUtils.showAlertDialog(context, "Delete?", 496 | "This action is very dangerous and you may lose vital information. Delete?", 497 | onPressed: (buildContext) { 498 | FormUtils.deleteTransactionAndUpdateItem( 499 | this.saveCallback, this.transaction, item, userData); 500 | }); 501 | } 502 | } 503 | 504 | void saveCallback(String message) { 505 | if (message.isEmpty) { 506 | this.clearFieldsAndTransaction(); 507 | if (this.widget.forEdit ?? false) { 508 | WindowUtils.moveToLastScreen(this.context, modified: true); 509 | } 510 | 511 | // Success 512 | WindowUtils.showAlertDialog( 513 | this.context, "Status", 'Sales updated successfully'); 514 | } else { 515 | // Failure 516 | WindowUtils.showAlertDialog(this.context, 'Failed!', message); 517 | } 518 | } 519 | 520 | void _initializeItemNamesAndNicknamesMapCache() async { 521 | Map itemMap = await StartupCache().itemMap; 522 | List cacheItemAndNickNames = List(); 523 | if (itemMap.isNotEmpty) { 524 | itemMap.forEach((key, value) { 525 | Map nameNickNameMap = {'name': value.first, 'nickName': value.last}; 526 | cacheItemAndNickNames.add(nameNickNameMap); 527 | }); 528 | } 529 | debugPrint("Ok list of items and nicKnames $cacheItemAndNickNames"); 530 | setState(() { 531 | this.itemNamesAndNicknames = cacheItemAndNickNames; 532 | }); 533 | } 534 | 535 | List _getAutoCompleteSuggestions() { 536 | // A way for autocomplete generator to access the itemNamesAndNicknames proprety of this class 537 | // Sometimes at the start of program empty suggestions gets passed and there is no way to update that. 538 | return this.itemNamesAndNicknames; 539 | } 540 | } 541 | --------------------------------------------------------------------------------