Add full app source

Adds the Flutter inventory app source, cleaned up for a shared repo:
generic package name, no hardcoded ERP endpoints (moved to gitignored
local config), no dead auth code, no debug logging of session data.
This commit is contained in:
BACHIR SOULDI
2026-07-16 14:37:28 +01:00
parent 37b0762921
commit 5a483da01d
110 changed files with 8081 additions and 0 deletions

4
.fvm/fvm_config.json Normal file
View File

@@ -0,0 +1,4 @@
{
"flutterSdkVersion": "3.29.2",
"flavors": {}
}

54
.gitignore vendored Normal file
View File

@@ -0,0 +1,54 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# Contains the real API endpoint (see lib/utils.example.dart for the template)
/lib/utils.dart
# Contains the real ERP domain (see the .xml.example file for the template)
/android/app/src/main/res/xml/network_security_config.xml
# FVM's SDK symlink is a machine-specific absolute path; keep only fvm_config.json
/.fvm/flutter_sdk

33
.metadata Normal file
View File

@@ -0,0 +1,33 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "c23637390482d4cf9598c3ce3f2be31aa7332daf"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: android
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
- platform: ios
create_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
base_revision: c23637390482d4cf9598c3ce3f2be31aa7332daf
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

28
analysis_options.yaml Normal file
View File

@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

View File

@@ -0,0 +1,45 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.inventory_app"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.inventory_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
}
}
}
flutter {
source = "../.."
}

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@@ -0,0 +1,59 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<application
android:label="Inventory App"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:networkSecurityConfig="@xml/network_security_config"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<!-- Place inside the <queries> element. -->
<intent>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" />
</intent>
</queries>
</manifest>

View File

@@ -0,0 +1,5 @@
package com.example.inventory_app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">your-erp-host.example.com</domain>
</domain-config>
</network-security-config>

View File

@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

21
android/build.gradle.kts Normal file
View File

@@ -0,0 +1,21 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip

View File

@@ -0,0 +1,25 @@
pluginManagement {
val flutterSdkPath = run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
}
include(":app")

1
assets/coin.json Normal file

File diff suppressed because one or more lines are too long

BIN
assets/desk.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

BIN
assets/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 KiB

BIN
assets/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

1
assets/wave.json Normal file

File diff suppressed because one or more lines are too long

34
ios/.gitignore vendored Normal file
View File

@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.inventoryApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.inventoryApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.inventoryApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.inventoryApp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.inventoryApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.inventoryApp;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

View File

@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
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.

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

49
ios/Runner/Info.plist Normal file
View File

@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Inventory App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>inventory_app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}

37
lib/auth.dart Normal file
View File

@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:inventory_app/views/splash_screen.dart';
import 'package:inventory_app/my_app.dart';
import 'package:inventory_app/views/login_view.dart';
import 'package:inventory_app/service/axelor_client.dart';
class Auth extends StatelessWidget {
const Auth({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final box = GetStorage();
String? sessionId = box.read('sessionId');
if (sessionId == null) {
return const LoginView();
}
// Session exists on disk — validate it with the server before granting access
return FutureBuilder<bool>(
future: AxelorClient.create().then((client) => client.isSessionValid()),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const SplashScreen();
} else if (snapshot.hasError || !(snapshot.data ?? false)) {
// Session is expired or invalid — clear it and force re-login
box.remove('sessionId');
box.remove('username');
return const LoginView();
} else {
return MyApp();
}
},
);
}
}

35
lib/constants.dart Normal file
View File

@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
const int kInventoryId = 111062;
TextStyle kLoginTitleStyle(Size size, {Color color = Colors.black}) => GoogleFonts.ubuntu(
fontSize: size.height * 0.060,
fontWeight: FontWeight.bold,
color: color
);
TextStyle kLoginSubtitleStyle(Size size) =>
GoogleFonts.ubuntu(fontSize: size.height * 0.030);
TextStyle kLoginTermsAndPrivacyStyle(Size size) =>
GoogleFonts.ubuntu(fontSize: 15, color: Colors.grey, height: 1.5);
TextStyle kHaveAnAccountStyle(Size size) =>
GoogleFonts.ubuntu(fontSize: size.height * 0.022, color: Colors.black);
TextStyle kLoginOrSignUpTextStyle(Size size) => GoogleFonts.ubuntu(
fontSize: size.height * 0.022,
fontWeight: FontWeight.w500,
color: Colors.deepPurpleAccent,
);
TextStyle kTextFormFieldStyle({
Color color = Colors.black54,
fontSize,
fontWeight,
}) => GoogleFonts.ubuntu(
color: color,
fontSize: fontSize,
fontWeight: fontWeight,
);

View File

@@ -0,0 +1,262 @@
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:inventory_app/constants.dart';
import 'package:inventory_app/models/famille_produit.dart';
import 'package:inventory_app/models/depot.dart';
import 'package:inventory_app/models/inventory_line.dart';
import 'package:inventory_app/models/product.dart';
import 'package:inventory_app/models/tracking_number.dart';
import 'package:inventory_app/service/axelor_client.dart';
import 'package:inventory_app/views/login_view.dart';
enum InventoryLineState { newLine, existingLine }
class HomeController extends GetxController {
final outputController = TextEditingController();
final labelController = TextEditingController();
final qtyController = TextEditingController(text: "1");
final unitController = TextEditingController();
final serialNumberTxt = TextEditingController();
var product = Rxn<Product>();
var selectedOption = 'comptage1'.obs;
var depotList = <Depot>[].obs;
var selectedDepot = Rxn<Depot>();
var trackingNumbers = <TrackingNumber>[].obs;
var selectedTrackingNumber = Rxn<TrackingNumber>();
var isLoading = false.obs;
var imageUrl = ''.obs;
var familleProduits = <FamilleProduit>[].obs;
var selectedFamilleProduit = Rxn<FamilleProduit>();
var sousFamilleProduits = <FamilleProduit>[].obs;
var selectedSousFamilleProduit = Rxn<FamilleProduit>();
var zoneCode = ''.obs;
var selectedState = 'Moyen'.obs;
late AxelorClient client;
@override
void onInit() {
super.onInit();
_initClient();
}
@override
void onReady() {
super.onReady();
_handleArgs();
}
Future<void> _initClient() async {
isLoading(true);
client = await AxelorClient.create();
depotList.value = (await client.fetchLocations()) ?? [];
isLoading(false);
}
Future<void> fetchLocations() async {
isLoading(true);
client = await AxelorClient.create();
depotList.value = (await client.fetchLocations()) ?? [];
isLoading(false);
}
Future<void> _loadProduct(String code) async {
try {
isLoading(true);
Product? p = await client.fetchProductByCode(code);
if (p == null) {
Get.snackbar('Erreur', 'Produit introuvable', backgroundColor: Colors.red);
return;
}
product.value = p;
labelController.text = p.name ?? '';
unitController.text = p.unit?.name ?? '';
imageUrl.value = p.imageUrl ?? '';
trackingNumbers.value =
await client.fetchTrackingNumberByProduct(p.id!) ?? [];
Get.snackbar(
'Produit trouvé',
'Produit: ${p.name}',
backgroundColor: Colors.green,
duration: const Duration(seconds: 2),
);
} catch (e) {
Get.snackbar(
'Erreur',
'Impossible de charger le produit',
backgroundColor: Colors.red,
);
} finally {
isLoading(false);
}
}
Future<void> _handleArgs() async {
final args = Get.arguments;
if (args is String) {
outputController.text = args;
await _loadProduct(args);
}
}
void handleScannedCode(String code) async {
outputController.text = code;
trackingNumbers.clear();
selectedTrackingNumber.value = null;
await _loadProduct(code);
}
void handleProductScan(String code) => handleScannedCode(code);
void handleLocationScan(String name) async {
var finalStr = "";
if (name.isNotEmpty) {
finalStr = name.split(":")[0].trim();
}
zoneCode.value = finalStr;
final locationJson = await client.fetchStockLocationByName(name);
if (locationJson == null) {
Get.snackbar(
'Erreur',
'Zone introuvable (Veuillez rafraichir la page et ressayer)',
backgroundColor: Colors.red,
);
return;
}
final depot = Depot.fromJson(locationJson);
selectedDepot.value = depot;
Get.snackbar('Zone trouvée', 'Nom: ${depot.name}', backgroundColor: Colors.blue);
}
Future<void> save() async {
if (outputController.text.isEmpty ||
qtyController.text.isEmpty ||
product.value == null) {
Get.snackbar('Erreur', 'Produit ou quantité invalide', backgroundColor: Colors.red);
return;
}
final now = DateTime.now().toIso8601String();
final qty = double.tryParse(qtyController.text) ?? 1;
final user = await client.fetchUserProfile();
final selected = selectedOption.value;
double first = 0, second = 0, third = 0;
String? firstDate, secondDate, thirdDate;
Map<String, dynamic>? firstUser, secondUser, thirdUser;
switch (selected) {
case "comptage1":
first = qty;
firstDate = now;
firstUser = user;
break;
case "comptage2":
second = qty;
secondDate = now;
secondUser = user;
break;
case "comptage3":
third = qty;
thirdDate = now;
thirdUser = user;
break;
}
final line = InventoryLine(
inventoryId: kInventoryId,
productId: product.value!.id!,
productName: product.value!.name ?? '',
currentQty: qty,
realQty: qty,
unitId: product.value!.unit?.id ?? 2,
description: selectedState.value,
observation: serialNumberTxt.text,
ticketId: "TICKET-${DateTime.now().millisecondsSinceEpoch}",
rack: "A1",
trackingNumberId: selectedTrackingNumber.value?.id,
countingTypeSelect: 1,
stockLocationId: selectedDepot.value?.id ?? 4,
firstCounting: first,
secondCounting: second,
thirdCounting: third,
firstCountingDate: firstDate,
secondCountingDate: secondDate,
thirdCountingDate: thirdDate,
firstCountingByUser: firstUser,
secondCountingByUser: secondUser,
thirdCountingByUser: thirdUser,
);
await client.saveInventoryLine(inventoryLine: line);
_resetForm();
Get.snackbar('Succès', 'Ligne enregistrée avec succès', backgroundColor: Colors.green);
}
void _resetForm() {
outputController.clear();
labelController.clear();
product.value = null;
trackingNumbers.clear();
selectedTrackingNumber.value = null;
serialNumberTxt.text = "";
}
Future<Uint8List?> fetchImageBytes(int imageId) => client.fetchImageBytes(imageId);
Future<void> fetchFamilleProduits() async {
try {
isLoading(true);
final list = await client.getFamilleProduit();
familleProduits.value = list ?? [];
} catch (e) {
familleProduits.clear();
} finally {
isLoading(false);
}
}
Future<void> fetchSousFamilleProduits(int parentId) async {
try {
isLoading(true);
final list = await client.getSousFamilleProduit(parentId);
sousFamilleProduits.value = list ?? [];
} catch (e) {
sousFamilleProduits.clear();
} finally {
isLoading(false);
}
}
void onFamilleSelected(FamilleProduit? famille) async {
selectedFamilleProduit.value = famille;
selectedSousFamilleProduit.value = null;
if (famille?.id != null) {
await fetchSousFamilleProduits(famille!.id!);
} else {
sousFamilleProduits.clear();
}
}
void logout() async {
if (await client.logout()) Get.off(LoginView());
}
}

View File

@@ -0,0 +1,9 @@
import 'package:get/get.dart';
class SimpleUIController extends GetxController {
RxBool isObscure = true.obs;
isObscureActive() {
isObscure.value = !isObscure.value;
}
}

View File

@@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
class ThemeController extends GetxController {
final _box = GetStorage();
final _key = 'isDarkMode';
RxBool isDarkMode = false.obs;
@override
void onInit() {
super.onInit();
isDarkMode.value = _loadThemeFromBox();
Get.changeThemeMode(isDarkMode.value ? ThemeMode.dark : ThemeMode.light);
}
bool _loadThemeFromBox() => _box.read(_key) ?? false;
void _saveThemeToBox(bool value) => _box.write(_key, value);
ThemeMode get theme => isDarkMode.value ? ThemeMode.dark : ThemeMode.light;
void toggleTheme() {
isDarkMode.value = !isDarkMode.value;
_saveThemeToBox(isDarkMode.value);
Get.changeThemeMode(isDarkMode.value ? ThemeMode.dark : ThemeMode.light);
}
}

View File

@@ -0,0 +1,25 @@
import 'package:get/get.dart';
import 'package:inventory_app/service/axelor_client.dart';
class UserController extends GetxController {
var isLoading = false.obs;
var user = Rx<Map<String, dynamic>?>(null);
Future<void> loadUser() async {
try {
isLoading.value = true;
final client = await AxelorClient.create();
final profile = await client.fetchUserProfile();
user.value = profile;
} catch (e) {
print("❌ Failed to load user: $e");
} finally {
isLoading.value = false;
}
}
// Optional: clear user when logging out
void clearUser() {
user.value = null;
}
}

614
lib/home.dart Normal file
View File

@@ -0,0 +1,614 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:inventory_app/models/depot.dart';
import 'package:inventory_app/models/tracking_number.dart';
import 'package:inventory_app/views/qr_view.dart';
import 'package:inventory_app/controllers/home_controller.dart';
import 'package:inventory_app/service/product_image_updater.dart';
import 'package:inventory_app/views/bureau_inventory_page.dart';
import 'package:inventory_app/views/my_scans.dart';
import 'package:inventory_app/views/product_view.dart';
import 'package:inventory_app/widgets/glass_widgets.dart';
import 'package:quickalert/quickalert.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:inventory_app/utils.dart';
import 'controllers/user_controller.dart';
class Home extends StatefulWidget {
const Home({super.key});
@override
State<Home> createState() => _HomeState();
}
class _HomeState extends State<Home> {
late final HomeController controller;
final String _selectedState = 'Neuf';
@override
void initState() {
super.initState();
controller = Get.find<HomeController>(); // IMPORTANT: No new instance
}
@override
Widget build(BuildContext context) {
final primary = Colors.blue;
return Scaffold(
body: RefreshIndicator(
onRefresh: () async {
await controller.fetchLocations();
},
child: Stack(
children: [
const FancyBackground(),
/// Only the loading OR the content rebuild
Obx(() {
if (controller.isLoading.value) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Center(child: CircularProgressIndicator()),
SizedBox(height: 20.0),
OutlinedButton(
onPressed: () async {
await controller.fetchLocations();
},
child: Text("Rafraîchir si c'est trop long"),
),
],
);
}
return _buildContent(context, primary);
}),
],
),
),
floatingActionButton: FancyFAB(
primary: primary,
onTap: () => _showScannerSheet(context, primary),
),
drawer: FancyDrawer(
primary: primary,
onLogout: controller.logout,
onScanBureau: () => _scanAndShowBureauInventory(),
),
);
}
// ---------------- MAIN CONTENT -------------------
Widget _buildContent(BuildContext context, MaterialColor primary) {
return SafeArea(
child: Column(
children: [
GlassAppBar(
title: "Inventaire",
primary: primary,
onLogout: controller.logout,
onCamera:
controller.product.value != null
? () {
final updater = ProductImageUpdater(
baseUrl: Utils.url,
);
updater.updateProductPictureFlow(
context,
controller.product.value!,
);
}
: null,
),
Expanded(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 6, 16, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Counting
SectionHeader(
icon: Icons.tune_rounded,
title: "Type de comptage",
accent: primary,
),
GlassCard(
child: Obx(
() => Column(
children: [
TileRadio(
value: "comptage1",
groupValue: controller.selectedOption.value,
label: 'Comptage 1',
icon: Icons.filter_1,
onChanged:
(v) => controller.selectedOption.value = v!,
),
TileRadio(
value: "comptage3",
groupValue: controller.selectedOption.value,
label: 'Comptage Contrôle',
icon: Icons.shield_rounded,
onChanged:
(v) => controller.selectedOption.value = v!,
),
],
),
),
),
// Depot
SectionHeader(
icon: Icons.store_mall_directory_rounded,
title: "Zone",
accent: primary,
),
GlassCard(
child: Obx(
() => AppDropdown<Depot>(
value: controller.selectedDepot.value,
items: controller.depotList,
hint: "Choisir une zone",
labelBuilder: (d) => d.name ?? "",
onChanged: (d) => controller.selectedDepot.value = d,
),
),
),
// Product Info
SectionHeader(
icon: Icons.inventory_2_rounded,
title: "Information article",
accent: primary,
),
GlassCard(
child: Column(
children: [
AppTextField(
controller: controller.outputController,
label: "Code article",
icon: Icons.qr_code_2_rounded,
enabled: false,
),
const SizedBox(height: 12),
AppTextField(
controller: controller.labelController,
label: "Libellé article",
icon: Icons.label_rounded,
enabled: false,
),
const SizedBox(height: 12),
AppTextField(
controller: controller.unitController,
label: "Unité article",
icon: Icons.scale_rounded,
enabled: false,
),
const SizedBox(height: 16),
SubLabel("N° de suivi", primary),
const SizedBox(height: 8),
Obx(
() => AppDropdown<TrackingNumber>(
value: controller.selectedTrackingNumber.value,
items: controller.trackingNumbers,
hint: "Sélectionner un N° de suivi",
labelBuilder: (t) => t.trackingNumberSeq ?? "",
onChanged:
(t) =>
controller.selectedTrackingNumber.value = t,
),
),
SubLabel("Serial number ", primary),
FancyTextField(
icon: Icons.segment_rounded,
controller: controller.serialNumberTxt,
label: 'Serial number',
hint: 'Serial number',
),
SubLabel("Etat ", primary),
const SizedBox(height: 8),
AppDropdown<String>(
hint: 'Etat',
value: _selectedState,
items: const ['Ancien', 'Moyen', 'Neuf', 'Réformée'],
onChanged: (val) {
if (val != null) {
HapticFeedback.selectionClick();
setState(
() => controller.selectedState.value = val,
);
}
},
labelBuilder: (String t) => t,
),
],
),
),
const SizedBox(height: 16),
// Product image
Obx(() {
final product = controller.product.value;
if (product?.picture != null) {
return AppImage(
future: controller.fetchImageBytes(
product!.picture!.id!,
),
);
}
return const SizedBox.shrink();
}),
const SizedBox(height: 16),
// Quantity
SectionHeader(
icon: Icons.exposure_plus_1_rounded,
title: "Quantité",
accent: primary,
),
GlassCard(
child: AppTextField(
controller: controller.qtyController,
label: "Quantité",
type: TextInputType.number,
icon: Icons.onetwothree,
),
),
const SizedBox(height: 16),
// Depot Image
Obx(() {
final depot = controller.selectedDepot.value;
if (depot?.picture != null) {
return AppImage(
future: controller.fetchImageBytes(depot!.picture!.id!),
);
}
return const SizedBox.shrink();
}),
const SizedBox(height: 24),
_saveButton(primary),
],
),
),
),
],
),
);
}
// ---------------- SAVE BUTTON -------------------
Widget _saveButton(MaterialColor primary) {
return ElevatedButton.icon(
icon: const Icon(Icons.save, color: Colors.white),
label: const Text("Enregistrer", style: TextStyle(color: Colors.white)),
style: ElevatedButton.styleFrom(
backgroundColor: primary,
minimumSize: const Size.fromHeight(50),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
),
onPressed: () async {
if (controller.selectedDepot.value == null) {
QuickAlert.show(
context: context,
type: QuickAlertType.error,
title: "Veuillez selectionnez la zone d'abord",
text: "En cours...",
);
return;
}
print('************************');
print(controller.selectedTrackingNumber.value);
QuickAlert.show(
context: context,
type: QuickAlertType.loading,
title: "Enregistrement",
text: "En cours...",
);
HapticFeedback.mediumImpact();
await controller.save();
Navigator.pop(context);
QuickAlert.show(
context: context,
type: QuickAlertType.success,
title: "Succès",
text: "Transaction enregistrée",
);
},
);
}
// ---------------- SCANNER SHEET -------------------
void _showScannerSheet(BuildContext context, MaterialColor primary) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (_) {
return GlassBottomSheet(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SheetHandle(primary),
ListTile(
leading: const Icon(Icons.qr_code_scanner),
title: const Text("Scanner Article"),
onTap: () async {
Navigator.pop(context);
final r = await Get.to(() => const QRViewExample());
if (r is String) {
controller.handleScannedCode(r);
} else
Get.to(() => const CreateProductPage());
},
),
ListTile(
leading: const Icon(Icons.location_searching),
title: const Text("Scanner Bureau"),
onTap: () async {
Navigator.pop(context);
final r = await Get.to(() => const QRViewExample());
if (r is String) controller.handleLocationScan(r);
},
),
ListTile(
leading: const Icon(Icons.business_center_rounded, color: Colors.teal),
title: const Text("Voir Articles d'un Bureau"),
subtitle: const Text("Scanner le QR du bureau pour afficher ses articles"),
onTap: () async {
Navigator.pop(context);
await _scanAndShowBureauInventory();
},
),
],
),
);
},
);
}
// ---------------- BUREAU INVENTORY SCAN -------------------
Future<void> _scanAndShowBureauInventory() async {
final r = await Get.to(() => const QRViewExample());
if (r is! String) return;
Get.dialog(
const Center(child: CircularProgressIndicator()),
barrierDismissible: false,
);
final locationJson = await controller.client.fetchStockLocationByName(r);
Get.back(); // close loading spinner
if (locationJson == null) {
Get.snackbar(
'Erreur',
'Bureau introuvable. Vérifiez le QR code.',
backgroundColor: Colors.red,
colorText: Colors.white,
);
return;
}
final depot = Depot.fromJson(locationJson);
Get.to(
() => BureauInventoryPage(
locationId: depot.id!,
locationName: depot.name ?? r,
),
);
}
}
// -------------------------------------------------------------
// GLASS APP BAR
// -------------------------------------------------------------
class GlassAppBar extends StatelessWidget {
final String title;
final Color primary;
final VoidCallback onLogout;
final VoidCallback? onCamera;
const GlassAppBar({
super.key,
required this.title,
required this.primary,
required this.onLogout,
this.onCamera,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(12, 6, 12, 12),
child: GlassCard(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
child: Row(
children: [
Icon(Icons.inventory_2_rounded, color: primary),
const SizedBox(width: 10),
Text(
title,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w800,
color: Colors.blue.shade700,
),
),
const Spacer(),
if (onCamera != null)
IconButton(
icon: const Icon(Icons.camera_alt_rounded),
onPressed: onCamera,
),
IconButton(
icon: const Icon(Icons.menu_rounded),
onPressed: () => Scaffold.of(context).openDrawer(),
),
IconButton(
icon: const Icon(Icons.logout_rounded),
onPressed: onLogout,
),
],
),
),
);
}
}
// -------------------------------------------------------------
// FANCY DRAWER (modern glass drawer)
// -------------------------------------------------------------
class FancyDrawer extends StatelessWidget {
final Color primary;
final VoidCallback onLogout;
final VoidCallback? onScanBureau;
const FancyDrawer({
super.key,
required this.primary,
required this.onLogout,
this.onScanBureau,
});
@override
Widget build(BuildContext context) {
final dark = Theme.of(context).brightness == Brightness.dark;
final userController = Get.find<UserController>();
final user = userController.user.value;
return Drawer(
backgroundColor:
dark ? Colors.black.withOpacity(.9) : Colors.white.withOpacity(.9),
child: Column(
children: [
DrawerHeader(
margin: EdgeInsets.zero,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [primary.withOpacity(.9), primary.withOpacity(.6)],
),
),
child: Row(
children: [
const CircleAvatar(
radius: 34,
backgroundImage: NetworkImage(
'https://i.pravatar.cc/150?img=3',
),
),
const SizedBox(width: 12),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
user?["fullName"] ?? "Utilisateur",
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
user?["email"] ?? "email inconnu",
style: TextStyle(color: Colors.white70),
),
],
),
],
),
),
// ---- MENU ----
_drawerTile(
icon: Icons.home_rounded,
label: "Accueil",
onTap: () => Navigator.pop(context),
),
user?["group"]?["code"] == 'INV_OFF'
? SizedBox()
: _drawerTile(
icon: Icons.add_box_rounded,
label: "Créer un produit",
onTap: () => Get.to(() => const CreateProductPage()),
),
_drawerTile(
icon: Icons.list_alt,
label: "Mes scans",
onTap: () => Get.to(() => MyScans()),
),
_drawerTile(
icon: Icons.business_center_rounded,
label: "Articles par Bureau",
onTap: () {
Navigator.pop(context);
onScanBureau?.call();
},
),
_drawerTile(
icon: Icons.report_problem_outlined,
label: "Signaler un problème",
onTap: () async => _launchTicketUrl(),
),
const Spacer(),
// ---- LOGOUT ----
_drawerTile(
icon: Icons.logout_rounded,
label: "Déconnexion",
onTap: onLogout,
),
const SizedBox(height: 12),
],
),
);
}
Widget _drawerTile({
required IconData icon,
required String label,
required VoidCallback onTap,
}) {
return ListTile(leading: Icon(icon), title: Text(label), onTap: onTap);
}
Future<void> _launchTicketUrl() async {
final url = Uri.parse(Utils.ticketUrl);
if (!await launchUrl(url)) {
throw Exception("Cannot open ticket system");
}
}
}

63
lib/main.dart Normal file
View File

@@ -0,0 +1,63 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:inventory_app/auth.dart';
import 'package:inventory_app/controllers/theme_controller.dart';
import 'package:inventory_app/controllers/home_controller.dart';
import 'controllers/user_controller.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await GetStorage.init();
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
debugDisableShadows = true;
final themeController = Get.put(ThemeController()); // inject globally
Get.lazyPut(() => HomeController());
Get.put(UserController()); // Register it ONE TIME globally
runApp(MyApp(themeController: themeController));
}
class MyApp extends StatelessWidget {
final ThemeController themeController;
const MyApp({super.key, required this.themeController});
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Inventory App',
themeMode: themeController.theme,
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.blue,
scaffoldBackgroundColor: const Color(0xFFF6F8FC),
cardColor: Colors.white,
appBarTheme: const AppBarTheme(
backgroundColor: Colors.white,
foregroundColor: Colors.black87,
elevation: 0,
titleTextStyle: TextStyle(color: Colors.black),
),
),
darkTheme: ThemeData(
brightness: Brightness.dark,
primarySwatch: Colors.blue,
scaffoldBackgroundColor: const Color(0xFF121212),
cardColor: const Color(0xFF1E1E1E),
appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF1E1E1E),
foregroundColor: Colors.white,
elevation: 0,
titleTextStyle: TextStyle(color: Colors.white),
),
),
home: const Auth(),
);
}
}

110
lib/models/depot.dart Normal file
View File

@@ -0,0 +1,110 @@
import 'package:inventory_app/utils.dart';
class Depot {
int? id;
String? name;
MetaFile? picture;
Depot({this.id, this.name, this.picture});
Depot.fromJson(Map<dynamic, dynamic> json) {
id = json['id'];
name = json['name'];
picture =
json['picture'] != null ? MetaFile.fromJson(json['picture']) : null;
}
Map<dynamic, dynamic> toJson() {
final Map<dynamic, dynamic> data = {};
data['id'] = this.id;
data['name'] = this.name;
if (picture != null) {
data['picture'] = picture!.toJson();
}
return data;
}
/// 👇 Get full image URL (you can customize baseUrl to be from `AxelorClient.baseUrl`)
String? get imageUrl {
if (picture?.id != null) {
return '${Utils.url}/ws/rest/com.axelor.meta.db.MetaFile/${picture!.id}/content/download';
}
return null;
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Depot && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
}
class Company {
String? code;
String? name;
int? id;
int? version;
Company({this.code, this.name, this.id, this.version});
Company.fromJson(Map<String, dynamic> json) {
code = json['code'];
name = json['name'];
id = json['id'];
version = json['$version'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['code'] = this.code;
data['name'] = this.name;
data['id'] = this.id;
data['$version'] = this.version;
return data;
}
}
class UpdatedBy {
String? code;
String? fullName;
int? id;
int? version;
UpdatedBy({this.code, this.fullName, this.id, this.version});
UpdatedBy.fromJson(Map<String, dynamic> json) {
code = json['code'];
fullName = json['fullName'];
id = json['id'];
version = json['$version'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['code'] = this.code;
data['fullName'] = this.fullName;
data['id'] = this.id;
data['$version'] = this.version;
return data;
}
}
class MetaFile {
String? fileName;
int? id;
int? version;
MetaFile({this.fileName, this.id, this.version});
MetaFile.fromJson(Map<String, dynamic> json) {
fileName = json['fileName'];
id = json['id'];
version = json['$version'];
}
Map<String, dynamic> toJson() {
return {'fileName': fileName, 'id': id, '\$version': version};
}
}

View File

@@ -0,0 +1,31 @@
class FamilleProduit {
int? id;
String? name;
FamilleProduit({this.id, this.name});
FamilleProduit.fromJson(Map<dynamic, dynamic> json) {
id = json['id'];
name = json['name'];
}
Map<dynamic, dynamic> toJson() {
final Map<dynamic, dynamic> data = {};
data['id'] = id;
data['name'] = name;
return data;
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is FamilleProduit &&
runtimeType == other.runtimeType &&
id == other.id;
@override
String toString() => name ?? 'Unknown';
@override
int get hashCode => id.hashCode;
}

View File

@@ -0,0 +1,142 @@
import 'package:inventory_app/models/product.dart';
class InventoryLine {
int? id;
int? version;
final int inventoryId;
final int productId;
final String productName;
final double currentQty;
final double realQty;
final int unitId;
final String? description;
final String? observation;
final String ticketId;
final String? rack;
final int? trackingNumberId;
final int countingTypeSelect;
final int stockLocationId;
double? firstCounting;
double? secondCounting;
double? thirdCounting;
String? firstCountingDate;
String? secondCountingDate;
String? thirdCountingDate;
Map<String, dynamic>? firstCountingByUser;
Map<String, dynamic>? secondCountingByUser;
Map<String, dynamic>? thirdCountingByUser;
Product? product;
InventoryLine({
this.id,
this.version,
required this.inventoryId,
required this.productId,
required this.productName,
required this.currentQty,
required this.realQty,
required this.unitId,
this.description,
this.observation,
required this.ticketId,
this.rack,
this.trackingNumberId,
required this.countingTypeSelect,
required this.stockLocationId,
required this.firstCounting,
required this.secondCounting,
required this.thirdCounting,
this.firstCountingDate,
this.secondCountingDate,
this.thirdCountingDate,
this.firstCountingByUser,
this.secondCountingByUser,
this.thirdCountingByUser,
this.product
});
factory InventoryLine.fromJson(Map<String, dynamic> json) {
double parseDouble(dynamic value) {
if (value == null) return 0.0;
if (value is num) return value.toDouble();
if (value is String) return double.tryParse(value) ?? 0.0;
return 0.0;
}
return InventoryLine(
id: json['id'],
version: json['version'],
inventoryId: json['inventory']?['id'] ?? 0,
productId: json['product']?['id'] ?? 0,
productName: json['productName'] ?? '',
currentQty: parseDouble(json['currentQty']),
realQty: parseDouble(json['realQty']),
unitId: json['unit']?['id'] ?? 0,
description: json['description'],
observation: json['observation'],
ticketId: json['ticketId'] ?? '',
rack: json['rack'],
trackingNumberId: json['trackingNumber']?['id'],
countingTypeSelect: json['countingTypeSelect'] ?? 0,
stockLocationId: json['stockLocation']?['id'] ?? 0,
firstCounting: parseDouble(json['firstCounting']),
secondCounting: parseDouble(json['secondCounting']),
thirdCounting: parseDouble(json['thirdCounting']),
firstCountingDate: json['firstCountingDate'],
secondCountingDate: json['secondCountingDate'],
thirdCountingDate: json['thirdCountingDate'],
firstCountingByUser: json['firstCountingByUser'] != null
? Map<String, dynamic>.from(json['firstCountingByUser'])
: null,
secondCountingByUser: json['secondCountingByUser'] != null
? Map<String, dynamic>.from(json['secondCountingByUser'])
: null,
thirdCountingByUser: json['thirdCountingByUser'] != null
? Map<String, dynamic>.from(json['thirdCountingByUser'])
: null,
product: json['product'] != null ? Product.fromJson(json['product']) : null
);
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> map = {
if (id != null) "id": id,
if (version != null) "version": version,
"inventory": {"id": inventoryId},
"product": {"id": productId},
"productName": productName,
"currentQty": currentQty,
"realQty": realQty,
"unit": {"id": unitId},
"description": description,
"observation": observation,
"ticketId": ticketId,
"rack": rack,
if (trackingNumberId != null) "trackingNumber": {"id": trackingNumberId},
"countingTypeSelect": countingTypeSelect,
"stockLocation": {"id": stockLocationId},
"firstCounting": firstCounting,
"secondCounting": secondCounting,
"thirdCounting": thirdCounting,
};
if (firstCountingDate != null) map['firstCountingDate'] = firstCountingDate;
if (secondCountingDate != null)
map['secondCountingDate'] = secondCountingDate;
if (thirdCountingDate != null) map['thirdCountingDate'] = thirdCountingDate;
if (firstCountingByUser != null)
map['firstCountingByUser'] = firstCountingByUser;
if (secondCountingByUser != null)
map['secondCountingByUser'] = secondCountingByUser;
if (thirdCountingByUser != null)
map['thirdCountingByUser'] = thirdCountingByUser;
return map;
}
@override
String toString() {
return 'InventoryLine{id: $id, version: $version, inventoryId: $inventoryId, productId: $productId, productName: $productName, currentQty: $currentQty, realQty: $realQty, unitId: $unitId, description: $description, observation: $observation, ticketId: $ticketId, rack: $rack, trackingNumberId: $trackingNumberId, countingTypeSelect: $countingTypeSelect, stockLocationId: $stockLocationId, firstCounting: $firstCounting, secondCounting: $secondCounting, thirdCounting: $thirdCounting, firstCountingDate: $firstCountingDate, secondCountingDate: $secondCountingDate, thirdCountingDate: $thirdCountingDate, firstCountingByUser: $firstCountingByUser, secondCountingByUser: $secondCountingByUser, thirdCountingByUser: $thirdCountingByUser}';
}
}

139
lib/models/product.dart Normal file
View File

@@ -0,0 +1,139 @@
import 'package:inventory_app/utils.dart';
class Product {
Unit? unit;
Unit? purchaseUnit;
String? code;
String? name;
int? id;
int? version;
String? productTypeSelect; // must be int, not String
int? familleProduit; // will be sent as object {id: X}
int? sousFamilleProduit; // same
String? internalDescription; // custom logic (not Axelor default field)
String? serialNumber;
String? description;
MetaFile? picture;
String? procurementMethodSelect = 'buy';
Product({
this.unit,
this.purchaseUnit,
this.code,
this.name,
this.id,
this.version,
this.productTypeSelect, // use 0,1,2
this.familleProduit,
this.sousFamilleProduit,
this.internalDescription,
this.serialNumber,
this.description,
this.picture,
this.procurementMethodSelect,
});
Product.fromJson(Map<String, dynamic> json) {
unit = json['unit'] != null ? Unit.fromJson(json['unit']) : null;
purchaseUnit =
json['purchaseUnit'] != null
? Unit.fromJson(json['purchaseUnit'])
: null;
code = json['code'];
name = json['name'];
id = json['id'];
version = json['version'];
productTypeSelect = json['productTypeSelect'];
familleProduit = json['familleProduit']?['id']; // Axelor wraps it in object
sousFamilleProduit = json['sousFamilleProduit']?['id'];
internalDescription = json['internalDescription'];
serialNumber = json['serialNumber'];
description = json['description'];
picture =
json['picture'] != null ? MetaFile.fromJson(json['picture']) : null;
procurementMethodSelect = json['procurementMethodSelect'] ?? 'buy';
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = {};
if (unit != null) {
data['unit'] = unit!.toJson();
}
if (purchaseUnit != null) {
data['purchaseUnit'] = purchaseUnit!.toJson();
}
if (code != null) data['code'] = code;
if (name != null) data['name'] = name;
if (id != null) data['id'] = id;
if (version != null) data['version'] = version;
if (productTypeSelect != null) {
data['productTypeSelect'] = productTypeSelect; // ✅ must be int
}
// ✅ Axelor expects {"id": X}, NOT raw int
if (familleProduit != null) {
data['familleProduit'] = {'id': familleProduit};
}
if (sousFamilleProduit != null) {
data['sousFamilleProduit'] = {'id': sousFamilleProduit};
}
data['internalDescription'] = internalDescription;
data['serialNumber'] = serialNumber;
data['description'] = description;
data['procurementMethodSelect'] = procurementMethodSelect;
return data;
}
@override
String toString() {
return 'Product{unit: $unit, code: $code, name: $name, id: $id, version: $version, productTypeSelect: $productTypeSelect, familleProduit: $familleProduit, sousFamilleProduit: $sousFamilleProduit, internalDescription: $internalDescription, serialNumber: $serialNumber, description: $description}';
}
/// 👇 Get full image URL (you can customize baseUrl to be from `AxelorClient.baseUrl`)
String? get imageUrl {
if (picture?.id != null) {
return '${Utils.url}/ws/rest/com.axelor.meta.db.MetaFile/${picture!.id}/content/download';
}
return null;
}
}
class Unit {
String? name;
int? id;
int? version;
Unit({this.name, this.id, this.version});
Unit.fromJson(Map<String, dynamic> json) {
name = json['name'];
id = json['id'];
version = json['version'];
}
Map<String, dynamic> toJson() {
return {'name': name, 'id': id, 'version': version};
}
}
class MetaFile {
String? fileName;
int? id;
int? version;
MetaFile({this.fileName, this.id, this.version});
MetaFile.fromJson(Map<String, dynamic> json) {
fileName = json['fileName'];
id = json['id'];
version = json['$version'];
}
Map<String, dynamic> toJson() {
return {'fileName': fileName, 'id': id, '\$version': version};
}
}

52
lib/models/profile.dart Normal file
View File

@@ -0,0 +1,52 @@
class Profile {
String? validId;
String? glpiCurrenttime;
int? glpiUseMode;
int? glpiID;
String? glpiisIdsVisible;
String? glpifriendlyname;
String? glpiname;
String? glpirealname;
String? glpifirstname;
int? glpidefaultEntity;
Profile(
{this.validId,
this.glpiCurrenttime,
this.glpiUseMode,
this.glpiID,
this.glpiisIdsVisible,
this.glpifriendlyname,
this.glpiname,
this.glpirealname,
this.glpifirstname,
this.glpidefaultEntity});
Profile.fromJson(Map<String, dynamic> json) {
validId = json['valid_id'];
glpiCurrenttime = json['glpi_currenttime'];
glpiUseMode = json['glpi_use_mode'];
glpiID = json['glpiID'];
glpiisIdsVisible = json['glpiis_ids_visible'];
glpifriendlyname = json['glpifriendlyname'];
glpiname = json['glpiname'];
glpirealname = json['glpirealname'];
glpifirstname = json['glpifirstname'];
glpidefaultEntity = json['glpidefault_entity'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['valid_id'] = validId;
data['glpi_currenttime'] = glpiCurrenttime;
data['glpi_use_mode'] = glpiUseMode;
data['glpiID'] = glpiID;
data['glpiis_ids_visible'] = glpiisIdsVisible;
data['glpifriendlyname'] = glpifriendlyname;
data['glpiname'] = glpiname;
data['glpirealname'] = glpirealname;
data['glpifirstname'] = glpifirstname;
data['glpidefault_entity'] = glpidefaultEntity;
return data;
}
}

View File

@@ -0,0 +1,35 @@
import 'package:inventory_app/models/product.dart';
class TrackingNumber {
int? id;
int? version;
String? trackingNumberSeq;
String? perishableExpirationDate;
Product? product;
TrackingNumber({
this.id,
this.version,
this.trackingNumberSeq,
this.perishableExpirationDate,
this.product,
});
TrackingNumber.fromJson(Map<String, dynamic> json) {
id = json['id'];
version = json['version'];
trackingNumberSeq = json['trackingNumberSeq'];
perishableExpirationDate = json['perishableExpirationDate'];
product = json['product'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['version'] = this.version;
data['trackingNumberSeq'] = this.trackingNumberSeq;
data['perishableExpirationDate'] = this.perishableExpirationDate;
data['product'] = this.product;
return data;
}
}

52
lib/my_app.dart Normal file
View File

@@ -0,0 +1,52 @@
import 'package:curved_navigation_bar/curved_navigation_bar.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:inventory_app/home.dart';
import 'package:inventory_app/controllers/home_controller.dart';
import 'package:inventory_app/views/panoramic.dart';
import 'package:inventory_app/views/profile.dart';
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
int _selectedIndex = 0;
late final HomeController controller;
@override
void initState() {
super.initState();
Get.lazyPut(() => HomeController());
}
final List<Widget> _screens = [
Home(), // Your main inventory/home screen
ProfilePage(), // Profile page
Panoramic(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(index: _selectedIndex, children: _screens),
bottomNavigationBar: CurvedNavigationBar(
backgroundColor: Colors.transparent,
color: Colors.blue,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
items: const [
Icon(Icons.home, color: Colors.white),
Icon(Icons.person, color: Colors.white),
Icon(Icons.image, color: Colors.white),
],
),
);
}
}

View File

@@ -0,0 +1,640 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:get/get.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:get_storage/get_storage.dart';
import 'package:inventory_app/constants.dart';
import 'package:inventory_app/models/famille_produit.dart';
import 'package:inventory_app/models/depot.dart';
import 'package:inventory_app/models/inventory_line.dart';
import 'package:inventory_app/models/product.dart';
import 'package:inventory_app/models/tracking_number.dart';
import 'package:inventory_app/utils.dart';
import 'package:path_provider/path_provider.dart';
class AxelorClient {
late Dio dio;
late PersistCookieJar cookieJar;
static final GetStorage _storage = GetStorage();
AxelorClient._internal();
static Future<AxelorClient> create() async {
final client = AxelorClient._internal();
Directory appDocDir = await getApplicationDocumentsDirectory();
String cookiePath = '${appDocDir.path}/cookies';
client.cookieJar = PersistCookieJar(storage: FileStorage(cookiePath));
client.dio = Dio(BaseOptions(baseUrl: Utils.url));
client.dio.interceptors.add(CookieManager(client.cookieJar));
// Set saved session ID if available
final storedSessionId = _storage.read('sessionId');
if (storedSessionId != null) {
client.dio.options.headers['Cookie'] = 'JSESSIONID=$storedSessionId';
}
return client;
}
Future<bool> login(String username, String password) async {
try {
final response = await dio.post(
'${Utils.url}/login.jsp',
data: {'username': username, 'password': password},
options: Options(
contentType: Headers.formUrlEncodedContentType,
followRedirects: false,
// Make sure redirect is not automatically followed
validateStatus: (status) => status != null && status < 500,
headers: {
'User-Agent': 'Mozilla/5.0 (Android; Flutter App)',
'Accept': '*/*',
},
),
);
// Handle 302 redirect: cookies should be in headers
final cookies = response.headers['set-cookie'];
if (cookies != null) {
// Parse JSESSIONID
String? sessionId;
for (var cookie in cookies) {
if (cookie.contains('JSESSIONID')) {
sessionId = cookie.split(';').first.split('=').last;
break;
}
}
if (sessionId != null && sessionId.isNotEmpty) {
// Set the cookie so the validation request is authenticated
dio.options.headers['Cookie'] = 'JSESSIONID=$sessionId';
// Verify the session is actually authenticated (not just an anonymous session)
final valid = await isSessionValid();
if (!valid) {
dio.options.headers.remove('Cookie');
return false;
}
await _storage.write('sessionId', sessionId);
await _storage.write('username', username);
return true;
}
}
return false;
} catch (e) {}
return false;
}
Future<bool> logout() async {
try {
final response = await dio.get('/logout');
if (response.statusCode == 200) {
await _storage.remove('sessionId');
await _storage.remove('username');
await cookieJar.deleteAll();
return true;
}
} catch (e) {}
return false;
}
Future<List<Depot>?> fetchLocations() async {
try {
final response = await dio.post(
'/ws/rest/com.axelor.apps.stock.db.StockLocation/search',
data: {
"offset": 0,
"limit": 800,
"data": {
"criteria": [
{
"fieldName": "usableOnImmobilisation",
"operator": "=",
"value": true,
},
],
},
},
);
if (response.statusCode == 200 && response.data['data'] != null) {
List<Depot> depots = [];
for (var depot in response.data["data"]) {
depots.add(Depot.fromJson(depot));
}
return depots;
} else {
return [];
}
} catch (e) {}
return null;
}
Future<void> saveInventoryLine({required InventoryLine inventoryLine}) async {
final String url =
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine';
String domain;
if (inventoryLine.trackingNumberId == null) {
domain =
"self.inventory.id = :inventoryId AND self.product.id = :product AND self.trackingNumber IS NULL";
} else {
domain =
"self.inventory.id = :inventoryId AND self.product.id = :product AND self.trackingNumber.id = :trackingNumberId";
}
try {
// 1. Check if InventoryLine already exists
final searchResponse = await dio.post(
'$url/search',
data: {
"data": {
"_domain": domain,
"_domainContext": {
"inventoryId": inventoryLine.inventoryId,
"product": inventoryLine.productId,
"trackingNumberId": inventoryLine.trackingNumberId,
},
"_archived": false,
},
},
);
if (searchResponse.statusCode == 200 &&
searchResponse.data['status'] == 0) {
final existing = searchResponse.data['data'];
if (existing != null && existing.isNotEmpty) {
final existingLine = existing[0];
inventoryLine.id = existingLine['id'];
inventoryLine.version = existingLine['version'];
inventoryLine.firstCounting =
inventoryLine.firstCounting != 0
? inventoryLine.firstCounting
: double.tryParse(
existingLine['firstCounting']?.toString() ?? '',
) ??
0.0;
inventoryLine.secondCounting =
inventoryLine.secondCounting != 0
? inventoryLine.secondCounting
: double.tryParse(
existingLine['secondCounting']?.toString() ?? '',
) ??
0.0;
inventoryLine.thirdCounting =
inventoryLine.thirdCounting != 0
? inventoryLine.thirdCounting
: double.tryParse(
existingLine['thirdCounting']?.toString() ?? '',
) ??
0.0;
inventoryLine.firstCountingDate =
inventoryLine.firstCountingDate ??
existingLine['firstCountingDate'];
inventoryLine.secondCountingDate =
inventoryLine.secondCountingDate ??
existingLine['secondCountingDate'];
inventoryLine.thirdCountingDate =
inventoryLine.thirdCountingDate ??
existingLine['thirdCountingDate'];
inventoryLine.firstCountingByUser =
inventoryLine.firstCountingByUser ??
existingLine['firstCountingByUser'];
inventoryLine.secondCountingByUser =
inventoryLine.secondCountingByUser ??
existingLine['secondCountingByUser'];
inventoryLine.thirdCountingByUser =
inventoryLine.thirdCountingByUser ??
existingLine['thirdCountingByUser'];
} else {}
}
// 2. Create or update the InventoryLine
final saveResponse = await dio.post(
url,
data: jsonEncode({"data": inventoryLine.toJson()}),
);
if (saveResponse.statusCode == 200) {
final result = saveResponse.data;
if (result['status'] == 0) {
} else {}
} else {}
} on DioException catch (e) {
if (e.response != null) {}
} catch (e) {}
}
Future<Product?> fetchProductByCode(String code) async {
try {
final response = await dio.post(
'${Utils.url}/ws/rest/com.axelor.apps.base.db.Product/search',
data: {
"offset": 0,
"limit": 1,
"sortBy": ["code", "name", "unit"],
"data": {
"criteria": [
{"fieldName": "code", "operator": "=", "value": code},
],
},
},
);
if (response.statusCode == 200 && response.data['status'] == 0) {
final list = response.data["data"];
if (list != null && list.isNotEmpty) {
return Product.fromJson(list[0]);
}
}
} catch (e) {}
return null;
}
Future<List<TrackingNumber>?> fetchTrackingNumberByProduct(int id) async {
try {
final response = await dio.post(
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber/search',
data: {
"offset": 0,
"limit": 100,
"fields": ["trackingNumberSeq", "perishableExpirationDate"],
"data": {
"criteria": [
{"fieldName": "product.id", "operator": "=", "value": id},
],
},
},
);
if (response.statusCode == 200 && response.data['data'] != null) {
List<TrackingNumber> trackingNumbers = [];
for (var trackingNumber in response.data["data"]) {
trackingNumbers.add(TrackingNumber.fromJson(trackingNumber));
}
return trackingNumbers;
} else {
return [];
}
} catch (e) {}
return null;
}
Future<List<InventoryLine>?> fetchInventoryLinesByLocation(
int locationId,
) async {
try {
final response = await dio.post(
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine/search',
data: {
"data": {
"_domain":
"self.stockLocation.id = :locationId and self.inventory.id = :inventoryId",
"_domainContext": {
"locationId": locationId,
"inventoryId": kInventoryId,
},
"_archived": false,
},
},
);
if (response.statusCode == 200 && response.data['data'] != null) {
List<InventoryLine> lines = [];
for (var line in response.data["data"]) {
lines.add(InventoryLine.fromJson(line));
}
return lines;
}
return [];
} catch (e) {
return null;
}
}
Future<bool> isSessionValid() async {
try {
final res = await dio.get(
'/ws/rest/com.axelor.apps.stock.db.StockLocation?offset=0&limit=1',
data: {
"offset": 0,
"limit": 1,
"data": {
"criteria": [
{
"fieldName": "usableOnImmobilisation",
"operator": "=",
"value": true,
},
],
},
},
);
return res.statusCode == 200 && res.data['status'] == 0;
} catch (_) {
return false;
}
}
Future<Map<String, dynamic>?> fetchUserProfile() async {
final username = _storage.read('username');
if (username == null) {
return null;
}
try {
final response = await dio.post(
'/ws/rest/com.axelor.auth.db.User/search',
data: {
"offset": 0,
"limit": 1,
"data": {
"criteria": [
{"fieldName": "code", "operator": "=", "value": username},
],
},
},
);
if (response.statusCode == 200 &&
response.data['status'] == 0 &&
response.data['data'] != null &&
response.data['data'].isNotEmpty) {
final user = response.data['data'][0];
return user;
} else {}
} catch (e) {}
return null;
}
Future<Map<String, dynamic>?> fetchStockLocationByName(String name) async {
try {
final response = await dio.post(
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.StockLocation/search',
data: {
"offset": 0,
"limit": 10000,
"data": {
"criteria": [
{"fieldName": "name", "operator": "=", "value": "$name"},
],
},
},
);
if (response.statusCode == 200 &&
response.data['status'] == 0 &&
response.data['data'] != null &&
response.data['data'].isNotEmpty) {
final location = response.data['data'][0];
return location;
} else {}
} catch (e) {}
return null;
}
Future<Uint8List?> fetchImageBytes(int imageId) async {
final url =
'${Utils.url}/ws/rest/com.axelor.meta.db.MetaFile/$imageId/content/download';
try {
final response = await dio.get(
url,
options: Options(responseType: ResponseType.bytes),
);
final contentType = response.headers.value('content-type');
if (contentType == null ||
(!contentType.startsWith('image/') &&
contentType != 'application/octet-stream')) {
return null;
}
return Uint8List.fromList(response.data);
} catch (e) {
return null;
}
}
Future<Product?> createProducts(Product product, Depot? depot) async {
final String url = '${Utils.url}/ws/rest/com.axelor.apps.base.db.Product';
try {
// Create or update the InventoryLine
final saveResponse = await dio.post(
url,
data: jsonEncode({"data": product.toJson()}),
);
if (saveResponse.statusCode == 200) {
final result = saveResponse.data;
if (result['status'] == 0) {
Product newProduct = Product.fromJson(result["data"][0]);
TrackingNumber? trackingNumber = await createTrackingNumber(
trackingNumber: TrackingNumber(
trackingNumberSeq: product.internalDescription,
product: Product(id: newProduct.id),
),
);
// Create initial inventory line
await createInitialInventoryLine(newProduct, trackingNumber, depot);
return newProduct;
} else {}
} else {}
} on DioException catch (e) {
if (e.response != null) {}
} catch (e) {}
return null;
}
Future<List<FamilleProduit>?> getFamilleProduit() async {
final response = await dio.post(
'${Utils.url}/ws/rest/com.axelor.apps.base.db.FamilleProduit/search',
data: {
"data": {
"criteria": [
{
"operator": "and",
"criteria": [
{"fieldName": "niveau", "operator": "=", "value": 0},
{
"fieldName": "usableOnImmobilisation",
"operator": "=",
"value": true,
},
],
},
],
},
},
);
if (response.statusCode == 200 && response.data['data'] != null) {
List<FamilleProduit> familleProduits = [];
for (var familleProduit in response.data["data"]) {
familleProduits.add(FamilleProduit.fromJson(familleProduit));
}
return familleProduits;
} else {
return [];
}
return null;
}
Future<List<FamilleProduit>?> getSousFamilleProduit(parentId) async {
final response = await dio.post(
'${Utils.url}/ws/rest/com.axelor.apps.base.db.FamilleProduit/search',
data: {
"data": {
"_domain": "self.parente.id = :parente",
"_domainContext": {"parente": parentId},
"_archived": false,
},
},
);
if (response.statusCode == 200 && response.data['data'] != null) {
List<FamilleProduit> sousfamilleProduits = [];
for (var sousfamilleProduit in response.data["data"]) {
sousfamilleProduits.add(FamilleProduit.fromJson(sousfamilleProduit));
}
return sousfamilleProduits;
} else {
return [];
}
}
Future<List<InventoryLine>?> getMyInventoryLines() async {
final user = await fetchUserProfile();
final response = await dio.post(
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.InventoryLine/search',
data: {
"data": {
"_domain":
"self.createdBy.id = :createdBy and self.inventory.id = :inventoryId",
"_domainContext": {
"inventoryId": kInventoryId,
"createdBy": user!['id'],
},
"_archived": false,
},
},
);
if (response.statusCode == 200 && response.data['data'] != null) {
List<InventoryLine>? inventoryLines = [];
for (var inventoryLine in response.data["data"]) {
inventoryLines.add(InventoryLine.fromJson(inventoryLine));
}
return inventoryLines;
} else {
return [];
}
return null;
}
Future<TrackingNumber?> createTrackingNumber({
required TrackingNumber trackingNumber,
}) async {
final String url =
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber';
try {
// 1. Check if product already exists
final searchResponse = await dio.post(
'${Utils.url}/ws/rest/com.axelor.apps.stock.db.TrackingNumber/search',
data: {
"data": {
"_domain":
"self.trackingNumberSeq = :trackingNumberSeq and self.product.id = :productId",
"_domainContext": {
"trackingNumberSeq": trackingNumber.trackingNumberSeq,
"productId": trackingNumber.product,
},
"_archived": false,
},
},
);
if (searchResponse.statusCode == 200 &&
searchResponse.data['status'] == 0) {
final existing = searchResponse.data['data'];
if (existing != null && existing.isNotEmpty) {
Get.snackbar('Erreur', "N° de serie existe deja");
return null;
} else {}
}
// 2. Create or update the InventoryLine
final saveResponse = await dio.post(
url,
data: jsonEncode({"data": trackingNumber.toJson()}),
);
if (saveResponse.statusCode == 200) {
final result = saveResponse.data;
if (result['status'] == 0) {
return TrackingNumber.fromJson(result["data"][0]);
} else {}
} else {}
} on DioException catch (e) {
if (e.response != null) {}
} catch (e) {}
return null;
}
Future<void> createInitialInventoryLine(
Product product,
TrackingNumber? trackingNumber,
Depot? depot,
) async {
final user = await fetchUserProfile();
final line = InventoryLine(
inventoryId: kInventoryId,
productId: product.id!,
productName: product.name ?? '',
currentQty: 1,
realQty: 1,
description: "Initial entry from mobile",
unitId: product.unit?.id ?? 4,
countingTypeSelect: 1,
firstCounting: 1,
firstCountingDate: DateTime.now().toIso8601String(),
firstCountingByUser: user,
stockLocationId: depot!.id!,
ticketId: product.internalDescription ?? '',
secondCounting: null,
thirdCounting: null,
trackingNumberId: trackingNumber?.id,
);
await saveInventoryLine(inventoryLine: line);
}
}

View File

@@ -0,0 +1,155 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart' as d;
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:image_picker/image_picker.dart';
import 'package:get/get.dart';
import 'package:inventory_app/models/product.dart';
import 'package:mime/mime.dart';
class ProductImageUpdater {
final d.Dio dio = d.Dio();
final String baseUrl;
final box = GetStorage();
ProductImageUpdater({required this.baseUrl});
final ImagePicker _picker = ImagePicker();
/// Step 1: Pick or take a photo
Future<File?> pickPhoto({bool fromCamera = true}) async {
final XFile? photo = await _picker.pickImage(
source: fromCamera ? ImageSource.camera : ImageSource.gallery,
);
return photo != null ? File(photo.path) : null;
}
Future<int?> uploadMetaFile(File file) async {
final sessionId = box.read('sessionId');
if (sessionId == null) {
Get.snackbar("Erreur", "Session ID introuvable",backgroundColor: Colors.red);
return null;
}
final fileName = file.path.split('/').last;
final fileSize = await file.length();
if (fileSize > 5 * 1024 * 1024) {
Get.snackbar("Erreur", "Fichier trop volumineux. Max: 5MB",backgroundColor: Colors.red);
return null;
}
final mimeType = lookupMimeType(file.path) ?? 'application/octet-stream';
try {
final response = await dio.post(
'$baseUrl/ws/files/upload',
data: file.openRead(), // ✅ Send binary stream
options: d.Options(
headers: {
'Cookie': 'JSESSIONID=$sessionId',
'Content-Type': 'application/octet-stream',
'X-File-Name': fileName,
'X-File-Size': fileSize.toString(),
'X-File-Type': mimeType,
'X-File-Offset': '0',
},
responseType: d.ResponseType.json,
),
);
print('📦 StatusCode: ${response.statusCode}');
print('📦 Response: ${response.data}');
if (response.statusCode == 200 && response != null) {
final int id = response.data['id'];
print('✅ File uploaded, MetaFile ID: $id');
return id;
} else {
print('❌ Upload failed: ${response.data}');
}
} on d.DioException catch (e) {
print('❌ DioException: ${e.message}');
if (e.response != null) {
print('📄 Response data: ${e.response?.data}');
}
} catch (e) {
print('❌ Unexpected error: $e');
}
return null;
}
/// Step 3: Update the product with the image ID
Future<bool> updateProductImage({
required Product product,
required int imageId,
}) async {
final sessionId = box.read('sessionId');
if (sessionId == null) {
Get.snackbar("Erreur", "Session ID introuvable",backgroundColor: Colors.red);
return false;
}
final payload = {
"data": {
"id": product.id,
"version": product.version,
"picture": {"id": imageId},
},
};
print('✅ payload $payload');
try {
final response = await dio.post(
'$baseUrl/ws/rest/com.axelor.apps.base.db.Product/${product.id}',
data: jsonEncode(payload),
options: d.Options(
headers: {
'Cookie': 'JSESSIONID=$sessionId',
'Content-Type': 'application/json',
},
),
);
if (response.statusCode == 200 && response.data['status'] == 0) {
print('✅ Product image updated');
return true;
} else {
print('❌ Update failed: ${response.data}');
}
} catch (e) {
print('❌ d.Dio error: $e');
}
return false;
}
/// Step 4: Combine all actions in one flow
Future<void> updateProductPictureFlow(
BuildContext context,
Product product,
) async {
File? file = await pickPhoto();
if (file == null) {
Get.snackbar("Annulé", "Aucune image sélectionnée",backgroundColor: Colors.red);
return;
}
final imageId = await uploadMetaFile(file);
print('✅ imageId $imageId');
if (imageId == null) {
Get.snackbar("Erreur", "Upload échoué",backgroundColor: Colors.red);
return;
}
final success = await updateProductImage(
product: product,
imageId: imageId,
);
if (success) {
Get.snackbar("Succès", "Image du produit mise à jour",backgroundColor: Colors.green);
}
}
}

6
lib/utils.example.dart Normal file
View File

@@ -0,0 +1,6 @@
// Copy this file to utils.dart and fill in your actual endpoints.
// utils.dart is gitignored so real endpoints never get committed.
class Utils {
static const String url = 'https://your-erp-host.example.com/prod';
static const String ticketUrl = 'https://your-ticket-system.example.com/ticket.form.php';
}

View File

@@ -0,0 +1,580 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:inventory_app/constants.dart';
import 'package:inventory_app/models/inventory_line.dart';
import 'package:inventory_app/service/axelor_client.dart';
import 'package:inventory_app/widgets/glass_widgets.dart';
class BureauInventoryPage extends StatefulWidget {
final int locationId;
final String locationName;
const BureauInventoryPage({
super.key,
required this.locationId,
required this.locationName,
});
@override
State<BureauInventoryPage> createState() => _BureauInventoryPageState();
}
class _BureauInventoryPageState extends State<BureauInventoryPage> {
Future<List<InventoryLine>?>? _future;
List<InventoryLine> _allLines = [];
final TextEditingController _searchController = TextEditingController();
String _query = '';
@override
void initState() {
super.initState();
_searchController.addListener(() {
setState(() => _query = _searchController.text.trim().toLowerCase());
});
_load();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
Future<void> _load() async {
final client = await AxelorClient.create();
setState(() {
_future = client.fetchInventoryLinesByLocation(widget.locationId).then((
lines,
) {
_allLines = lines ?? [];
return lines;
});
});
}
List<InventoryLine> get _filteredLines {
if (_query.isEmpty) return _allLines;
return _allLines.where((line) {
return line.productName.toLowerCase().contains(_query) ||
line.ticketId.toLowerCase().contains(_query) ||
(line.observation?.toLowerCase().contains(_query) ?? false) ||
(line.description?.toLowerCase().contains(_query) ?? false);
}).toList();
}
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
body: Stack(
children: [
const FancyBackground(),
SafeArea(
child: Column(
children: [
BackAppBar(
title: widget.locationName,
icon: Icons.business_rounded,
),
Expanded(
child: FutureBuilder<List<InventoryLine>?>(
future: _future,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError || snapshot.data == null) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.error_outline,
color: Colors.redAccent,
size: 48,
),
const SizedBox(height: 12),
Text(
'Impossible de charger les articles.',
style: TextStyle(color: Colors.redAccent),
),
const SizedBox(height: 12),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('Réessayer'),
onPressed: _load,
),
],
),
);
}
if (_allLines.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.inventory_2_outlined,
size: 64,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Aucun article trouvé dans ce bureau.',
style: TextStyle(
fontSize: 16,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
final filtered = _filteredLines;
return Column(
children: [
_SearchBar(
controller: _searchController,
isDark: isDark,
total: _allLines.length,
shown: filtered.length,
),
Expanded(
child:
filtered.isEmpty
? Center(
child: Text(
'Aucun résultat pour "${_searchController.text}"',
style: TextStyle(
fontSize: 15,
color: Colors.grey.shade500,
),
textAlign: TextAlign.center,
),
)
: RefreshIndicator(
onRefresh: _load,
child: ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(
16,
4,
16,
100,
),
itemCount: filtered.length,
itemBuilder: (context, index) {
return _LineCard(
line: filtered[index],
isDark: isDark,
onTap:
() => _showDetails(
context,
filtered[index],
),
);
},
),
),
),
],
);
},
),
),
],
),
),
],
),
);
}
void _showDetails(BuildContext context, InventoryLine line) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textColor = isDark ? Colors.white : Colors.blue.shade700;
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withValues(alpha: 0.3),
isScrollControlled: true,
builder: (_) {
return Padding(
padding: const EdgeInsets.all(12),
child: GlassCard(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SheetHandle(Colors.blue),
Padding(
padding: const EdgeInsets.only(left: 8.0),
child: Text(
line.productName,
style: kTextFormFieldStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
),
const SizedBox(height: 12),
_detail('N° Ticket', line.ticketId, textColor),
if (line.description != null && line.description!.isNotEmpty)
_detail('État', line.description!, textColor),
if (line.observation != null && line.observation!.isNotEmpty)
_detail('N° Série', line.observation!, textColor),
if (line.firstCounting != null && line.firstCounting! > 0)
_detail(
'Comptage 1',
line.firstCounting!.toStringAsFixed(0),
textColor,
),
if (line.secondCounting != null && line.secondCounting! > 0)
_detail(
'Comptage 2',
line.secondCounting!.toStringAsFixed(0),
textColor,
),
if (line.thirdCounting != null && line.thirdCounting! > 0)
_detail(
'Comptage 3',
line.thirdCounting!.toStringAsFixed(0),
textColor,
),
if (line.firstCountingDate != null)
_detail(
'Date C1',
_formatDate(line.firstCountingDate!),
textColor,
),
if (line.firstCountingByUser != null)
_detail(
'Agent C1',
line.firstCountingByUser!['fullName'] ?? '-',
textColor,
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
icon: const Icon(
Icons.close_rounded,
color: Colors.white,
),
label: const Text(
'Fermer',
style: TextStyle(color: Colors.white),
),
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
),
],
),
),
),
);
},
);
}
Widget _detail(String label, String value, Color textColor) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 110,
child: Text(
label,
style: kTextFormFieldStyle(
fontWeight: FontWeight.w600,
color: textColor,
),
),
),
Expanded(
child: Text(
value,
style: kTextFormFieldStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
),
],
),
);
}
String _formatDate(String iso) {
try {
final dt = DateTime.parse(iso).toLocal();
return '${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year}';
} catch (_) {
return iso;
}
}
}
// ── Search bar ───────────────────────────────────────────────────────────────
class _SearchBar extends StatelessWidget {
final TextEditingController controller;
final bool isDark;
final int total;
final int shown;
const _SearchBar({
required this.controller,
required this.isDark,
required this.total,
required this.shown,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 4, 16, 8),
child: TextField(
controller: controller,
style: TextStyle(color: isDark ? Colors.white : Colors.black87),
decoration: InputDecoration(
hintText: 'Rechercher par nom, ticket, état...',
hintStyle: TextStyle(
color: isDark ? Colors.white38 : Colors.black38,
fontSize: 14,
),
prefixIcon: Icon(
Icons.search_rounded,
color: isDark ? Colors.white54 : Colors.black45,
),
suffixIcon:
controller.text.isNotEmpty
? IconButton(
icon: Icon(
Icons.close_rounded,
color: isDark ? Colors.white54 : Colors.black45,
),
onPressed: () => controller.clear(),
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12),
child: Text(
'$total articles',
style: TextStyle(
fontSize: 12,
color: isDark ? Colors.white38 : Colors.black38,
),
),
),
filled: true,
fillColor:
isDark
? Colors.white.withValues(alpha: 0.08)
: Colors.white.withValues(alpha: 0.75),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(vertical: 12),
),
),
);
}
}
// ── Card for each inventory line ─────────────────────────────────────────────
class _LineCard extends StatelessWidget {
final InventoryLine line;
final bool isDark;
final VoidCallback onTap;
const _LineCard({
required this.line,
required this.isDark,
required this.onTap,
});
@override
Widget build(BuildContext context) {
final textColor = isDark ? Colors.white : Colors.blue.shade700;
final subColor = isDark ? Colors.white70 : Colors.blue.shade400;
final qty =
(line.thirdCounting ?? 0) > 0
? line.thirdCounting!
: (line.secondCounting ?? 0) > 0
? line.secondCounting!
: (line.firstCounting ?? 0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: GlassCard(
child: InkWell(
borderRadius: BorderRadius.circular(20),
onTap: () {
HapticFeedback.selectionClick();
onTap();
},
child: Padding(
padding: const EdgeInsets.all(5),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.12),
shape: BoxShape.circle,
),
child: const Icon(
Icons.inventory_2_rounded,
color: Colors.blue,
size: 22,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
line.productName,
style: kTextFormFieldStyle(
fontWeight: FontWeight.bold,
color: textColor,
),
),
const SizedBox(height: 4),
if (line.ticketId.isNotEmpty)
Text(
'Ticket: ${line.ticketId}',
style: kTextFormFieldStyle(
fontSize: 12.0,
color: subColor,
),
),
if (line.description != null &&
line.description!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
'État: ${line.description}',
style: kTextFormFieldStyle(
fontSize: 12.0,
color: subColor,
),
),
),
if (line.observation != null &&
line.observation!.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
'N° Série: ${line.observation}',
style: kTextFormFieldStyle(
fontSize: 12.0,
color: subColor,
),
),
),
const SizedBox(height: 6),
Wrap(
spacing: 6,
children: [
if ((line.firstCounting ?? 0) > 0)
_CountChip(
label:
'C1: ${line.firstCounting!.toStringAsFixed(0)}',
),
if ((line.secondCounting ?? 0) > 0)
_CountChip(
label:
'C2: ${line.secondCounting!.toStringAsFixed(0)}',
color: Colors.orange,
),
if ((line.thirdCounting ?? 0) > 0)
_CountChip(
label:
'C3: ${line.thirdCounting!.toStringAsFixed(0)}',
color: Colors.green,
),
],
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.blue.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(12),
),
child: Text(
qty.toStringAsFixed(0),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
),
],
),
),
),
),
);
}
}
class _CountChip extends StatelessWidget {
final String label;
final Color color;
const _CountChip({required this.label, this.color = Colors.blue});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: color,
),
),
);
}
}

439
lib/views/login_view.dart Normal file
View File

@@ -0,0 +1,439 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:inventory_app/models/depot.dart';
import 'package:inventory_app/my_app.dart';
import 'package:inventory_app/service/axelor_client.dart';
import 'package:lottie/lottie.dart';
import 'package:quickalert/quickalert.dart';
import 'package:inventory_app/home.dart';
import '../constants.dart';
import '../controllers/simple_ui_controller.dart';
import 'package:inventory_app/controllers/theme_controller.dart';
class LoginView extends StatefulWidget {
const LoginView({super.key});
@override
State<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
TextEditingController nameController = TextEditingController(text: "");
TextEditingController passwordController = TextEditingController(text: "");
final _formKey = GlobalKey<FormState>();
bool _loading = false;
late AxelorClient client;
bool _clientInitialized = false;
SimpleUIController simpleUIController = Get.put(SimpleUIController());
@override
void initState() {
super.initState();
_initClient();
}
Future<void> _initClient() async {
client = await AxelorClient.create();
setState(() => _clientInitialized = true);
}
@override
Widget build(BuildContext context) {
final themeController = Get.find<ThemeController>();
final isDark = themeController.isDarkMode.value;
final size = MediaQuery.of(context).size;
return Scaffold(
resizeToAvoidBottomInset: true,
body: Stack(
children: [
const _FancyBackground(), // 🧊 Adaptive gradient background
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: _GlassCard(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// 🌊 Animated wave header
Lottie.asset(
'assets/wave.json',
height: size.height * 0.25,
repeat: true,
),
const SizedBox(height: 20),
Text(
"Se connecter",
style: kLoginTitleStyle(size * 0.80).copyWith(
color:
isDark
? Colors.white.withOpacity(0.95)
: Colors.blue.shade700,
),
),
const SizedBox(height: 8),
Text(
"Bienvenue",
style: kLoginSubtitleStyle(size).copyWith(
color:
isDark
? Colors.white
: Colors.black.withOpacity(0.7),
),
),
const SizedBox(height: 28),
// 👤 Username
TextFormField(
controller: nameController,
style: kTextFormFieldStyle(
color:
isDark
? Colors.white.withOpacity(0.9)
: Colors.black87,
),
decoration: _inputDecoration(
hint: 'Nom d\'utilisateur',
icon: Icons.person,
isDark: isDark,
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Entrer un nom d\'utilisateur';
} else if (value.length < 4) {
return 'Au moins 4 caractères';
} else if (value.length > 20) {
return 'Maximum 20 caractères';
}
return null;
},
),
const SizedBox(height: 16),
// 🔒 Password
Obx(
() => TextFormField(
controller: passwordController,
obscureText: simpleUIController.isObscure.value,
style: kTextFormFieldStyle(
color:
isDark
? Colors.white.withOpacity(0.9)
: Colors.black87,
),
decoration: _inputDecoration(
hint: 'Mot de passe',
icon: Icons.lock_open_rounded,
isDark: isDark,
suffix: IconButton(
icon: Icon(
simpleUIController.isObscure.value
? Icons.visibility
: Icons.visibility_off,
color: isDark ? Colors.white70 : Colors.black54,
),
onPressed: simpleUIController.isObscureActive,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Entrer un mot de passe';
} else if (value.length < 6) {
return 'Au moins 6 caractères';
}
return null;
},
),
),
const SizedBox(height: 14),
Text(
"Contactez votre administrateur si vous avez oublié votre mot de passe",
style: kLoginTermsAndPrivacyStyle(size).copyWith(
color:
isDark
? Colors.white54
: Colors.black.withOpacity(0.5),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// 🚪 Login button
SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
isDark ? Colors.blue.shade400 : Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
),
shadowColor: Colors.blue.withOpacity(0.4),
elevation: 8,
),
onPressed:
_loading
? null
: () async {
if (_formKey.currentState!.validate()) {
setState(() => _loading = true);
if (!_clientInitialized) {
QuickAlert.show(
context: context,
type: QuickAlertType.error,
title: 'Erreur',
text:
'Client non initialisé. Veuillez réessayer.',
);
setState(() => _loading = false);
return;
}
final loggedIn = await client.login(
nameController.text.trim(),
passwordController.text.trim(),
);
if (loggedIn) {
// final locations =
// await client.fetchLocations();
Get.off(
() => const MyApp(),
// arguments: locations,
);
} else {
QuickAlert.show(
context: context,
type: QuickAlertType.error,
title: 'Erreur',
text:
'Nom d\'utilisateur ou mot de passe incorrect',
);
}
setState(() => _loading = false);
}
},
child:
_loading
? const Center(
child: CircularProgressIndicator(
color: Colors.white,
),
)
: const Text(
'Se connecter',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.w600,
),
),
),
),
const SizedBox(height: 16),
// 🌗 Toggle theme
Obx(() {
final isDark = themeController.isDarkMode.value;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
isDark
? Icons.dark_mode_rounded
: Icons.light_mode_rounded,
color:
isDark
? Colors.white70
: Colors.blue.shade700,
),
Switch(
value: isDark,
onChanged: (v) {
themeController.toggleTheme();
setState(() {});
},
activeColor: Colors.blue,
),
Text(
isDark ? 'Mode sombre' : 'Mode clair',
style: kTextFormFieldStyle(
color: isDark ? Colors.white70 : Colors.black87,
),
),
],
);
}),
],
),
),
),
),
),
],
),
);
}
InputDecoration _inputDecoration({
required String hint,
required IconData icon,
bool isDark = false,
Widget? suffix,
}) {
return InputDecoration(
prefixIcon: Icon(icon, color: isDark ? Colors.white70 : Colors.black54),
suffixIcon: suffix,
hintText: hint,
hintStyle: TextStyle(color: isDark ? Colors.white54 : Colors.black45),
filled: true,
fillColor:
isDark
? Colors.white.withOpacity(0.08)
: Colors.white.withOpacity(0.7),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
);
}
}
/// Glass card reused
class _GlassCard extends StatelessWidget {
final Widget child;
const _GlassCard({required this.child});
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors:
isDark
? [
Colors.white.withOpacity(0.06),
Colors.white.withOpacity(0.03),
]
: [
Colors.white.withOpacity(0.4),
Colors.white.withOpacity(0.2),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
border: Border.all(
color:
isDark
? Colors.white.withOpacity(0.08)
: Colors.white.withOpacity(0.5),
),
boxShadow: [
BoxShadow(
color:
isDark
? Colors.black.withOpacity(0.4)
: Colors.blueGrey.withOpacity(0.15),
blurRadius: 18,
offset: const Offset(0, 10),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: Padding(padding: const EdgeInsets.all(20), child: child),
),
),
);
}
}
/// Gradient background reused
class _FancyBackground extends StatelessWidget {
const _FancyBackground();
@override
Widget build(BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors:
isDark
? [
const Color(0xFF0A0A0A),
const Color(0xFF121212),
const Color(0xFF1E1E1E),
]
: [
const Color(0xFFEEF2FF),
const Color(0xFFE0F2FE),
const Color(0xFFE6FFFA),
],
),
),
child: Stack(
children: [
Positioned(
top: -60,
right: -30,
child: _Blob(
color:
isDark
? Colors.blue.withOpacity(0.1)
: Colors.blue.withOpacity(0.18),
size: 180,
),
),
Positioned(
bottom: -40,
left: -30,
child: _Blob(
color:
isDark
? Colors.cyanAccent.withOpacity(0.08)
: Colors.cyan.withOpacity(0.16),
size: 160,
),
),
],
),
);
}
}
class _Blob extends StatelessWidget {
final Color color;
final double size;
const _Blob({required this.color, required this.size});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
boxShadow: [BoxShadow(color: color, blurRadius: 60, spreadRadius: 30)],
),
);
}
}

264
lib/views/my_scans.dart Normal file
View File

@@ -0,0 +1,264 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:inventory_app/constants.dart';
import 'package:inventory_app/models/inventory_line.dart';
import 'package:inventory_app/service/axelor_client.dart';
import 'package:inventory_app/widgets/glass_widgets.dart';
class MyScans extends StatefulWidget {
const MyScans({Key? key}) : super(key: key);
@override
State<MyScans> createState() => _MyScansState();
}
class _MyScansState extends State<MyScans> {
Future<List<InventoryLine>?>? _futureInventoryLines;
@override
void initState() {
super.initState();
_loadInventoryLines();
}
Future<void> _loadInventoryLines() async {
final client = await AxelorClient.create();
setState(() {
_futureInventoryLines = client.getMyInventoryLines();
});
}
@override
Widget build(BuildContext context) {
final MaterialColor primary = Colors.blue;
final isDark = Theme.of(context).brightness == Brightness.dark;
return Scaffold(
body: Stack(
children: [
const FancyBackground(),
SafeArea(
child: Column(
children: [
BackAppBar(title: '📦 Mes Scans', icon: Icons.inventory_2_rounded),
Expanded(
child: FutureBuilder<List<InventoryLine>?>(
future: _futureInventoryLines,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text(
'❌ Erreur: ${snapshot.error}',
style: const TextStyle(color: Colors.redAccent),
),
);
}
final lines = snapshot.data ?? [];
if (lines.isEmpty) {
return const Center(
child: Text(
'Aucun scan trouvé.',
style: TextStyle(
fontSize: 16,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
);
}
return RefreshIndicator(
onRefresh: _loadInventoryLines,
child: ListView.builder(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.fromLTRB(16, 8, 16, 100),
itemCount: lines.length,
itemBuilder: (context, index) {
final line = lines[index];
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: GlassCard(
padding: const EdgeInsets.all(14),
child: ListTile(
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.inventory_2_rounded,
color: Colors.blue,
),
),
title: Text(
"${line.product!.code!}-${line.productName}",
style: kTextFormFieldStyle(
fontWeight: FontWeight.bold,
color:
isDark
? Colors.white
: primary.shade700,
),
),
subtitle: Padding(
padding: const EdgeInsets.only(top: 6),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
"Stock ID: ${line.id}",
style: kTextFormFieldStyle(
fontSize: 13.0,
color:
isDark
? Colors.white
: primary.shade700,
),
),
Text(
"Comptage: ${line.countingTypeSelect}",
style: kTextFormFieldStyle(
fontSize: 13.0,
color:
isDark
? Colors.white
: primary.shade700,
),
),
Text(
"Quantité Réelle: ${line.realQty}",
style: kTextFormFieldStyle(
fontSize: 13.0,
color:
isDark
? Colors.white
: primary.shade700,
),
),
if (line.observation != null &&
line.observation!.isNotEmpty)
Text(
"Observation: ${line.observation}",
style: kTextFormFieldStyle(
fontSize: 13.0,
color:
isDark
? Colors.white
: primary.shade700,
),
),
],
),
),
onTap: () {
HapticFeedback.selectionClick();
_showLineDetails(context, line);
},
),
),
);
},
),
);
},
),
),
],
),
),
],
),
);
}
void _showLineDetails(BuildContext context, InventoryLine line) {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
barrierColor: Colors.black.withValues(alpha: 0.3),
builder: (_) {
return GlassBottomSheet(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SheetHandle(Colors.blue),
Text(
line.productName,
style: kTextFormFieldStyle(
fontSize: 18.0,
fontWeight: FontWeight.bold,
color: Colors.blue.shade700,
),
),
SizedBox(height: 8),
_lineDetail("ID", line.id?.toString() ?? "-", context),
_lineDetail("Inventaire", "${line.inventoryId}", context),
_lineDetail(
"Type de comptage",
"${line.countingTypeSelect}",
context,
),
if (line.firstCountingDate != null)
_lineDetail("1er comptage", line.firstCountingDate!, context),
if (line.secondCountingDate != null)
_lineDetail("2ème comptage", line.secondCountingDate!, context),
if (line.thirdCountingDate != null)
_lineDetail("3ème comptage", line.thirdCountingDate!, context),
SizedBox(height: 14),
Align(
alignment: Alignment.centerRight,
child: ElevatedButton.icon(
icon: Icon(Icons.close_rounded, color: Colors.white),
label: Text("Fermer"),
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
],
),
);
},
);
}
Widget _lineDetail(String label, String value, BuildContext context) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final MaterialColor primary = Colors.blue;
return Padding(
padding: EdgeInsets.symmetric(vertical: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: kTextFormFieldStyle(
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : primary.shade700,
),
),
Text(
value,
style: kTextFormFieldStyle(
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : primary.shade700,
),
),
],
),
);
}
}

Some files were not shown because too many files have changed in this diff Show More