This commit is contained in:
2026-10-01 16:03:32 +05:30
commit 3db47c4648
102 changed files with 5067 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+36
View File
@@ -0,0 +1,36 @@
# 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: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: android
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: ios
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: web
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# 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'
+16
View File
@@ -0,0 +1,16 @@
# dealer_network
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
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
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
+44
View File
@@ -0,0 +1,44 @@
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.dealernetwork.dealer_network"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
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.dealernetwork.dealer_network"
// 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 = "../.."
}
@@ -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>
+46
View File
@@ -0,0 +1,46 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="Dealer Network"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="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: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>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.dealernetwork.dealer_network
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -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>
@@ -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>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

@@ -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>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FF000000</color>
</resources>
@@ -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>
@@ -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>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+3
View File
@@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
+5
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.12-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")
Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

+34
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
+26
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>13.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+616
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 = 13.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.dealernetwork.dealerNetwork;
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.dealernetwork.dealerNetwork.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.dealernetwork.dealerNetwork.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.dealernetwork.dealerNetwork.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 = 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;
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 = 13.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 = 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 = 13.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.dealernetwork.dealerNetwork;
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.dealernetwork.dealerNetwork;
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 */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -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>
@@ -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>
@@ -0,0 +1,101 @@
<?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"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
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"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
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>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -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>
@@ -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>
+13
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)
}
}
@@ -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: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -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

@@ -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.
@@ -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>
+26
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
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>Dealer Network</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>dealer_network</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>
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+12
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.
}
}
+6
View File
@@ -0,0 +1,6 @@
/// Hosted API. Override with --dart-define=API_BASE_URL=...
String resolveApiBaseUrl() {
const fromEnv = String.fromEnvironment('API_BASE_URL');
if (fromEnv.isNotEmpty) return fromEnv;
return 'https://snrodealer.playpoolstudios.com/api';
}
+113
View File
@@ -0,0 +1,113 @@
const stageOrder = [
'received',
'orderedByCustomer',
'documentsProcessed',
'paymentApproved',
'rmvRegistration',
'plateReceived',
];
const modelOptions = ['gn125', 'boxer125', 'click150i'];
const sriLankaDistricts = [
'Ampara',
'Anuradhapura',
'Badulla',
'Batticaloa',
'Colombo',
'Galle',
'Gampaha',
'Hambantota',
'Jaffna',
'Kalutara',
'Kandy',
'Kegalle',
'Kilinochchi',
'Kurunegala',
'Mannar',
'Matale',
'Matara',
'Moneragala',
'Mullaitivu',
'Nuwara Eliya',
'Polonnaruwa',
'Puttalam',
'Ratnapura',
'Trincomalee',
'Vavuniya',
];
String stageLabel(String stage) {
switch (stage) {
case 'received':
return 'Received';
case 'orderedByCustomer':
return 'Ordered by customer';
case 'documentsProcessed':
return 'Documents processed';
case 'paymentApproved':
return 'Payment approved';
case 'rmvRegistration':
return 'RMV registration';
case 'plateReceived':
return 'Plate received';
default:
return stage;
}
}
String? nextStage(String stage) {
final index = stageOrder.indexOf(stage);
if (index < 0 || index >= stageOrder.length - 1) return null;
return stageOrder[index + 1];
}
String modelLabel(String model) {
switch (model) {
case 'gn125':
return 'GN125';
case 'boxer125':
return 'Boxer 125';
case 'click150i':
return 'Click 150i';
default:
return model;
}
}
String roleLabel(String role) {
switch (role) {
case 'superAdmin':
return 'Super admin';
case 'salesRep':
return 'Sales rep';
case 'dealer':
return 'Dealer';
default:
return role;
}
}
String leaseProviderLabel(String provider) {
switch (provider) {
case 'gci':
return 'GCI';
case 'thirdPartyFinance':
return 'Finance';
case 'both':
return 'GCI and finance';
default:
return provider;
}
}
String planLabel(String planType) {
switch (planType) {
case 'monthlySpecial':
return 'Monthly special';
case 'general':
return 'General';
default:
return planType;
}
}
+41
View File
@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'screens/auth_flow.dart';
import 'screens/home_shell.dart';
import 'screens/splash_screen.dart';
import 'services/auth_controller.dart';
import 'theme/app_theme.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const DealerNetworkApp());
}
class DealerNetworkApp extends StatelessWidget {
const DealerNetworkApp({super.key});
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AuthController()..bootstrap(),
child: MaterialApp(
title: 'Dealer Network',
theme: buildAppTheme(),
home: const _Root(),
),
);
}
}
class _Root extends StatelessWidget {
const _Root();
@override
Widget build(BuildContext context) {
final auth = context.watch<AuthController>();
if (!auth.ready) return const SplashScreen();
if (auth.user == null) return const AuthFlow();
return const HomeShell();
}
}
+252
View File
@@ -0,0 +1,252 @@
class Dealer {
Dealer({
required this.id,
required this.name,
required this.code,
this.district,
this.address,
this.phone,
});
final String id;
final String name;
final String code;
final String? district;
final String? address;
final String? phone;
factory Dealer.fromJson(Map<String, dynamic> json) {
return Dealer(
id: json['id'] as String,
name: json['name'] as String,
code: json['code'] as String,
district: json['district'] as String?,
address: json['address'] as String?,
phone: json['phone'] as String?,
);
}
}
class AppUser {
AppUser({
required this.id,
required this.name,
required this.phone,
required this.role,
this.dealerId,
this.assignedDealerIds = const [],
this.dealers = const [],
});
final String id;
final String name;
final String phone;
final String role;
final String? dealerId;
final List<String> assignedDealerIds;
final List<Dealer> dealers;
factory AppUser.fromJson(Map<String, dynamic> json) {
return AppUser(
id: json['id'] as String,
name: json['name'] as String,
phone: json['phone'] as String,
role: json['role'] as String,
dealerId: json['dealerId'] as String?,
assignedDealerIds: ((json['assignedDealerIds'] as List?) ?? const [])
.map((id) => id as String)
.toList(),
dealers: ((json['dealers'] as List?) ?? const [])
.map((item) => Dealer.fromJson(item as Map<String, dynamic>))
.toList(),
);
}
}
class LoginRequestItem {
LoginRequestItem({
required this.id,
required this.name,
required this.phone,
this.dealerName,
required this.createdAt,
});
final String id;
final String name;
final String phone;
final String? dealerName;
final DateTime createdAt;
factory LoginRequestItem.fromJson(Map<String, dynamic> json) {
final user = json['user'] as Map<String, dynamic>;
return LoginRequestItem(
id: json['id'] as String,
name: user['name'] as String,
phone: user['phone'] as String,
dealerName: json['dealerName'] as String?,
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
}
class DealerLoginStatus {
DealerLoginStatus({required this.status, this.token, this.user});
final String status;
final String? token;
final AppUser? user;
factory DealerLoginStatus.fromJson(Map<String, dynamic> json) {
return DealerLoginStatus(
status: json['status'] as String,
token: json['token'] as String?,
user: json['user'] is Map<String, dynamic>
? AppUser.fromJson(json['user'] as Map<String, dynamic>)
: null,
);
}
}
class StockItem {
StockItem({
required this.id,
required this.chassisNumber,
required this.engineNumber,
required this.model,
required this.color,
required this.currentDealerId,
this.dealerName,
required this.dateReceived,
required this.currentStage,
required this.stageLogDates,
this.paymentType,
this.leaseProviderType,
this.financeCompanyName,
this.plateNumber,
required this.ageInDays,
required this.badge,
});
final String id;
final String chassisNumber;
final String engineNumber;
final String model;
final String color;
final String currentDealerId;
final String? dealerName;
final DateTime dateReceived;
final String currentStage;
final Map<String, DateTime> stageLogDates;
final String? paymentType;
final String? leaseProviderType;
final String? financeCompanyName;
final String? plateNumber;
final int ageInDays;
final String badge;
factory StockItem.fromJson(Map<String, dynamic> json) {
final rawLogs = json['stageLogDates'];
final logs = <String, DateTime>{};
if (rawLogs is Map) {
rawLogs.forEach((key, value) {
if (value != null) {
logs[key.toString()] = DateTime.parse(value.toString());
}
});
}
return StockItem(
id: json['id'] as String,
chassisNumber: json['chassisNumber'] as String,
engineNumber: json['engineNumber'] as String,
model: json['model'] as String,
color: json['color'] as String,
currentDealerId: json['currentDealerId'] as String,
dealerName: json['dealerName'] as String?,
dateReceived: DateTime.parse(json['dateReceived'] as String),
currentStage: json['currentStage'] as String,
stageLogDates: logs,
paymentType: json['paymentType'] as String?,
leaseProviderType: json['leaseProviderType'] as String?,
financeCompanyName: json['financeCompanyName'] as String?,
plateNumber: json['plateNumber'] as String?,
ageInDays: (json['ageInDays'] as num).toInt(),
badge: json['badge'] as String,
);
}
}
class EventPlan {
EventPlan({
required this.id,
required this.dealerId,
this.dealerName,
required this.planType,
this.month,
this.year,
required this.title,
required this.description,
required this.expense,
required this.imageUrls,
required this.createdAt,
});
final String id;
final String dealerId;
final String? dealerName;
final String planType;
final int? month;
final int? year;
final String title;
final String description;
final double expense;
final List<String> imageUrls;
final DateTime createdAt;
factory EventPlan.fromJson(Map<String, dynamic> json) {
return EventPlan(
id: json['id'] as String,
dealerId: json['dealerId'] as String,
dealerName: json['dealerName'] as String?,
planType: json['planType'] as String,
month: json['month'] as int?,
year: json['year'] as int?,
title: json['title'] as String,
description: json['description'] as String,
expense: (json['expense'] as num).toDouble(),
imageUrls: ((json['imageUrls'] as List?) ?? const [])
.map((url) => url as String)
.toList(),
createdAt: DateTime.parse(json['createdAt'] as String),
);
}
}
class ServicePoint {
ServicePoint({
required this.id,
required this.name,
required this.district,
this.address,
this.phone,
this.notes,
});
final String id;
final String name;
final String district;
final String? address;
final String? phone;
final String? notes;
factory ServicePoint.fromJson(Map<String, dynamic> json) {
return ServicePoint(
id: json['id'] as String,
name: json['name'] as String,
district: json['district'] as String,
address: json['address'] as String?,
phone: json['phone'] as String?,
notes: json['notes'] as String?,
);
}
}
+96
View File
@@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
class AddDealerScreen extends StatefulWidget {
const AddDealerScreen({super.key});
@override
State<AddDealerScreen> createState() => _AddDealerScreenState();
}
class _AddDealerScreenState extends State<AddDealerScreen> {
final _name = TextEditingController();
final _code = TextEditingController();
final _address = TextEditingController();
final _phone = TextEditingController();
String? _district;
bool _saving = false;
@override
void dispose() {
_name.dispose();
_code.dispose();
_address.dispose();
_phone.dispose();
super.dispose();
}
Future<void> _save() async {
setState(() => _saving = true);
try {
await context.read<AuthController>().api.createDealer({
'name': _name.text.trim(),
'code': _code.text.trim(),
'district': _district ?? '',
'address': _address.text.trim(),
'phone': _phone.text.trim(),
});
if (!mounted) return;
showNotice(context, 'Dealer and login created');
Navigator.pop(context, true);
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Add dealer')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
const SizedBox(height: 12),
TextField(controller: _code, decoration: const InputDecoration(labelText: 'Code')),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _district,
isExpanded: true,
decoration: const InputDecoration(labelText: 'District'),
items: [
for (final district in sriLankaDistricts)
DropdownMenuItem(value: district, child: Text(district)),
],
onChanged: (value) => setState(() => _district = value),
),
const SizedBox(height: 12),
TextField(controller: _address, decoration: const InputDecoration(labelText: 'Address')),
const SizedBox(height: 12),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(
labelText: 'Contact phone',
hintText: '0715127342',
helperText: 'This number becomes the dealer login',
),
),
const SizedBox(height: 16),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? 'Saving' : 'Create dealer'),
),
],
),
);
}
}
+387
View File
@@ -0,0 +1,387 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
class AdminScreen extends StatelessWidget {
const AdminScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Column(
children: [
const TabBar(
tabs: [
Tab(text: 'Dealers'),
Tab(text: 'Sales reps'),
],
),
const Expanded(
child: TabBarView(
children: [
_DealersTab(),
_RepsTab(),
],
),
),
],
),
);
}
}
class _DealersTab extends StatefulWidget {
const _DealersTab();
@override
State<_DealersTab> createState() => _DealersTabState();
}
class _DealersTabState extends State<_DealersTab> {
final _name = TextEditingController();
final _code = TextEditingController();
final _address = TextEditingController();
final _phone = TextEditingController();
String? _district;
int _districtField = 0;
List<Dealer> _dealers = [];
bool _loading = true;
bool _saving = false;
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_name.dispose();
_code.dispose();
_address.dispose();
_phone.dispose();
super.dispose();
}
Future<void> _load() async {
try {
final dealers = await context.read<AuthController>().api.adminDealers();
if (!mounted) return;
setState(() {
_dealers = dealers;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
Future<void> _delete(Dealer dealer) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Delete dealer'),
content: Text(
'Delete ${dealer.name}? Its login, stock, and plans will be removed.',
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Delete')),
],
),
);
if (confirmed != true || !mounted) return;
try {
await context.read<AuthController>().api.deleteDealer(dealer.id);
await _load();
} catch (error) {
if (mounted) showError(context, error);
}
}
Future<void> _save() async {
setState(() => _saving = true);
try {
await context.read<AuthController>().api.createDealer({
'name': _name.text.trim(),
'code': _code.text.trim(),
'district': _district ?? '',
'address': _address.text.trim(),
'phone': _phone.text.trim(),
});
_name.clear();
_code.clear();
_address.clear();
_phone.clear();
_district = null;
_districtField++;
await _load();
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text('New dealer', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
const SizedBox(height: 12),
TextField(controller: _code, decoration: const InputDecoration(labelText: 'Code')),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
key: ValueKey(_districtField),
initialValue: _district,
isExpanded: true,
decoration: const InputDecoration(labelText: 'District'),
items: [
for (final district in sriLankaDistricts)
DropdownMenuItem(value: district, child: Text(district)),
],
onChanged: (value) => setState(() => _district = value),
),
const SizedBox(height: 12),
TextField(controller: _address, decoration: const InputDecoration(labelText: 'Address')),
const SizedBox(height: 12),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(
labelText: 'Contact phone',
hintText: '0715127342',
helperText: 'This number becomes the dealer login',
),
),
const SizedBox(height: 12),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? 'Saving' : 'Create dealer'),
),
const SizedBox(height: 20),
for (final dealer in _dealers)
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(dealer.name),
subtitle: Text('${dealer.code}${dealer.district == null ? '' : ' · ${dealer.district}'}'),
trailing: IconButton(
tooltip: 'Delete',
onPressed: () => _delete(dealer),
icon: const Icon(Icons.delete_outline),
),
),
],
);
}
}
class _RepsTab extends StatefulWidget {
const _RepsTab();
@override
State<_RepsTab> createState() => _RepsTabState();
}
class _RepsTabState extends State<_RepsTab> {
final _name = TextEditingController();
final _phone = TextEditingController();
final _password = TextEditingController();
List<Dealer> _dealers = [];
List<AppUser> _reps = [];
final Set<String> _selected = {};
bool _loading = true;
bool _saving = false;
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_name.dispose();
_phone.dispose();
_password.dispose();
super.dispose();
}
Future<void> _load() async {
final api = context.read<AuthController>().api;
try {
final results = await Future.wait([api.adminDealers(), api.salesReps()]);
if (!mounted) return;
setState(() {
_dealers = results[0] as List<Dealer>;
_reps = results[1] as List<AppUser>;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
Future<void> _save() async {
setState(() => _saving = true);
try {
await context.read<AuthController>().api.createSalesRep({
'name': _name.text.trim(),
'phone': _phone.text.trim(),
'password': _password.text,
'dealerIds': _selected.toList(),
});
_name.clear();
_phone.clear();
_password.clear();
_selected.clear();
await _load();
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
Future<void> _changeAssignment(AppUser rep, {required bool assign}) async {
final choices = _dealers.where((dealer) {
final has = rep.assignedDealerIds.contains(dealer.id);
return assign ? !has : has;
}).toList();
if (choices.isEmpty) {
showError(context, assign ? 'Every dealer is already assigned' : 'No dealers to remove');
return;
}
String? dealerId = choices.first.id;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: Text(assign ? 'Assign dealer' : 'Remove dealer'),
content: DropdownButtonFormField<String>(
initialValue: dealerId,
items: [
for (final dealer in choices)
DropdownMenuItem(value: dealer.id, child: Text(dealer.name)),
],
onChanged: (value) => dealerId = value,
),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Save')),
],
),
);
if (confirmed != true || dealerId == null || !mounted) return;
try {
await context.read<AuthController>().api.assignDealer(
salesRepId: rep.id,
dealerId: dealerId!,
assigned: assign,
);
await _load();
} catch (error) {
if (mounted) showError(context, error);
}
}
String _dealerNames(AppUser rep) {
if (rep.assignedDealerIds.isEmpty) return 'No dealers assigned';
return rep.assignedDealerIds
.map((id) {
for (final dealer in _dealers) {
if (dealer.id == id) return dealer.name;
}
return id;
})
.join(', ');
}
@override
Widget build(BuildContext context) {
if (_loading) return const Center(child: CircularProgressIndicator());
return ListView(
padding: const EdgeInsets.all(16),
children: [
Text('New sales rep', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
const SizedBox(height: 12),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(labelText: 'Phone', hintText: '0715127342'),
),
const SizedBox(height: 12),
TextField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
),
const SizedBox(height: 8),
for (final dealer in _dealers)
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: Text(dealer.name),
value: _selected.contains(dealer.id),
onChanged: (checked) {
setState(() {
if (checked == true) {
_selected.add(dealer.id);
} else {
_selected.remove(dealer.id);
}
});
},
),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? 'Saving' : 'Create sales rep'),
),
const SizedBox(height: 20),
for (final rep in _reps)
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(rep.name, style: Theme.of(context).textTheme.titleMedium),
Text(rep.phone),
const SizedBox(height: 4),
Text(_dealerNames(rep)),
Row(
children: [
TextButton(
onPressed: () => _changeAssignment(rep, assign: true),
child: const Text('Assign'),
),
TextButton(
onPressed: () => _changeAssignment(rep, assign: false),
child: const Text('Remove'),
),
],
),
],
),
),
),
],
);
}
}
+177
View File
@@ -0,0 +1,177 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import '../services/auth_controller.dart';
class AuthFlow extends StatefulWidget {
const AuthFlow({super.key});
@override
State<AuthFlow> createState() => _AuthFlowState();
}
class _AuthFlowState extends State<AuthFlow> {
final _phone = TextEditingController();
final _password = TextEditingController();
bool _staff = false;
bool _waiting = false;
Timer? _poll;
@override
void dispose() {
_poll?.cancel();
_phone.dispose();
_password.dispose();
super.dispose();
}
Future<void> _staffLogin() async {
final auth = context.read<AuthController>();
try {
await auth.staffLogin(_phone.text.trim(), _password.text);
} catch (_) {}
}
Future<void> _dealerLogin() async {
final auth = context.read<AuthController>();
try {
final started = await auth.startDealerLogin(_phone.text.trim());
if (!mounted) return;
setState(() => _waiting = true);
_poll = Timer.periodic(
const Duration(seconds: 2),
(_) => _check(started.requestId, started.secret),
);
await _check(started.requestId, started.secret);
} catch (_) {}
}
Future<void> _check(String requestId, String secret) async {
final auth = context.read<AuthController>();
try {
final status = await auth.pollDealerLogin(requestId, secret);
if (!mounted) return;
if (status == 'pending') return;
_poll?.cancel();
if (status != 'approved') setState(() => _waiting = false);
} catch (_) {
if (!mounted) return;
_poll?.cancel();
setState(() => _waiting = false);
}
}
void _cancelWait() {
_poll?.cancel();
context.read<AuthController>().error = null;
setState(() => _waiting = false);
}
@override
Widget build(BuildContext context) {
final auth = context.watch<AuthController>();
final theme = Theme.of(context);
return Scaffold(
body: SafeArea(
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 440),
child: ListView(
padding: const EdgeInsets.fromLTRB(24, 48, 24, 24),
children: [
Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.asset(
'assets/images/senaro_logo.png',
width: 300,
semanticLabel: 'Senaro',
),
),
),
const SizedBox(height: 20),
Text(
'Dealer Network',
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium,
),
const SizedBox(height: 8),
Text(
_waiting
? 'Waiting for a sales rep or super admin to approve ${_phone.text.trim()}.'
: _staff
? 'Staff sign in with the phone number and password on the account.'
: 'Dealers sign in with the phone number already registered for the dealership.',
style: theme.textTheme.bodyLarge,
),
const SizedBox(height: 28),
if (_waiting)
const Center(child: CircularProgressIndicator())
else ...[
SegmentedButton<bool>(
segments: const [
ButtonSegment(value: false, label: Text('Dealer')),
ButtonSegment(value: true, label: Text('Staff')),
],
selected: {_staff},
onSelectionChanged: auth.busy
? null
: (value) => setState(() => _staff = value.first),
),
const SizedBox(height: 16),
TextField(
controller: _phone,
keyboardType: TextInputType.phone,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(
labelText: 'Phone',
hintText: '071xxxxxxx',
),
onSubmitted: (_) => _staff ? _staffLogin() : _dealerLogin(),
),
if (_staff) ...[
const SizedBox(height: 12),
TextField(
controller: _password,
obscureText: true,
decoration: const InputDecoration(labelText: 'Password'),
onSubmitted: (_) => _staffLogin(),
),
],
],
if (auth.error != null) ...[
const SizedBox(height: 12),
Text(
auth.error!,
style: TextStyle(color: theme.colorScheme.error),
),
],
const SizedBox(height: 20),
if (_waiting)
TextButton(
onPressed: _cancelWait,
child: const Text('Cancel'),
)
else
FilledButton(
onPressed: auth.busy
? null
: (_staff ? _staffLogin : _dealerLogin),
child: Text(
auth.busy
? 'Please wait'
: (_staff ? 'Sign in' : 'Request login'),
),
),
],
),
),
),
),
);
}
}
+112
View File
@@ -0,0 +1,112 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../theme/app_theme.dart';
import '../ui/feedback.dart';
import 'stock_detail_screen.dart';
import 'stock_form_screen.dart';
class DealerStockScreen extends StatefulWidget {
const DealerStockScreen({super.key, required this.dealer, required this.dealers});
final Dealer dealer;
final List<Dealer> dealers;
@override
State<DealerStockScreen> createState() => _DealerStockScreenState();
}
class _DealerStockScreenState extends State<DealerStockScreen> {
List<StockItem>? _stocks;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final stocks = await context.read<AuthController>().api.stocks(dealerId: widget.dealer.id);
if (!mounted) return;
setState(() {
_stocks = stocks;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
@override
Widget build(BuildContext context) {
final role = context.watch<AuthController>().user?.role;
final stocks = _stocks ?? [];
final canAdd = role == 'dealer' || role == 'salesRep' || role == 'superAdmin';
return Scaffold(
appBar: AppBar(title: Text(widget.dealer.name)),
floatingActionButton: canAdd
? FloatingActionButton.extended(
onPressed: () async {
final created = await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => StockFormScreen(dealers: [widget.dealer]),
),
);
if (created == true) _load();
},
icon: const Icon(Icons.add),
label: const Text('Add stock'),
)
: null,
body: _loading && _stocks == null
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _load,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 96),
children: [
if (stocks.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: Text('No bikes for this dealer.')),
),
for (final stock in stocks)
Card(
child: ListTile(
leading: CircleAvatar(
backgroundColor: badgeColor(stock.badge),
child: Text(
'${stock.ageInDays}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
),
title: Text('${modelLabel(stock.model)} · ${stock.color}'),
subtitle: Text(
'${stock.chassisNumber}${stock.plateNumber == null ? '' : ' · ${stock.plateNumber}'}\n${stageLabel(stock.currentStage)}',
),
isThreeLine: true,
trailing: Text('${stock.ageInDays}d'),
onTap: () async {
await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => StockDetailScreen(stock: stock, dealers: widget.dealers),
),
);
if (mounted) _load();
},
),
),
],
),
),
);
}
}
+189
View File
@@ -0,0 +1,189 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
class EventFormScreen extends StatefulWidget {
const EventFormScreen({super.key});
@override
State<EventFormScreen> createState() => _EventFormScreenState();
}
class _EventFormScreenState extends State<EventFormScreen> {
final _title = TextEditingController();
final _description = TextEditingController();
final _expense = TextEditingController();
final _urls = TextEditingController();
String _planType = 'general';
int _month = DateTime.now().month;
int _year = DateTime.now().year;
String? _dealerId;
List<Dealer> _dealers = [];
bool _loading = true;
bool _saving = false;
@override
void initState() {
super.initState();
_prepare();
}
Future<void> _prepare() async {
final user = context.read<AuthController>().user;
if (user?.role == 'dealer') {
setState(() {
_dealerId = user?.dealerId;
_loading = false;
});
return;
}
try {
final dealers = await context.read<AuthController>().api.dealers();
if (!mounted) return;
setState(() {
_dealers = dealers;
if (dealers.length == 1) _dealerId = dealers.first.id;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
@override
void dispose() {
_title.dispose();
_description.dispose();
_expense.dispose();
_urls.dispose();
super.dispose();
}
Future<void> _save() async {
final expense = double.tryParse(_expense.text.trim());
if (_dealerId == null || expense == null) {
showError(context, 'Dealer and a numeric expense are required');
return;
}
final urls = _urls.text
.split('\n')
.map((line) => line.trim())
.where((line) => line.isNotEmpty)
.toList();
setState(() => _saving = true);
try {
await context.read<AuthController>().api.createEvent({
'dealerId': _dealerId,
'planType': _planType,
'title': _title.text.trim(),
'description': _description.text.trim(),
'expense': expense,
'imageUrls': urls,
if (_planType == 'monthlySpecial') 'month': _month,
if (_planType == 'monthlySpecial') 'year': _year,
});
if (mounted) Navigator.pop(context, true);
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Scaffold(body: Center(child: CircularProgressIndicator()));
}
final isDealer = context.watch<AuthController>().user?.role == 'dealer';
return Scaffold(
appBar: AppBar(title: const Text('New plan')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
SegmentedButton<String>(
segments: const [
ButtonSegment(value: 'general', label: Text('General')),
ButtonSegment(value: 'monthlySpecial', label: Text('Monthly')),
],
selected: {_planType},
onSelectionChanged: (value) => setState(() => _planType = value.first),
),
const SizedBox(height: 12),
TextField(controller: _title, decoration: const InputDecoration(labelText: 'Title')),
const SizedBox(height: 12),
TextField(
controller: _description,
minLines: 3,
maxLines: 5,
decoration: const InputDecoration(labelText: 'Description'),
),
const SizedBox(height: 12),
TextField(
controller: _expense,
keyboardType: TextInputType.number,
decoration: const InputDecoration(labelText: 'Expense'),
),
const SizedBox(height: 12),
TextField(
controller: _urls,
minLines: 2,
maxLines: 4,
decoration: const InputDecoration(
labelText: 'Image URLs',
hintText: 'One URL per line',
),
),
if (!isDealer) ...[
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _dealerId,
decoration: const InputDecoration(labelText: 'Dealer'),
items: [
for (final dealer in _dealers)
DropdownMenuItem(value: dealer.id, child: Text('${dealer.name} (${dealer.code})')),
],
onChanged: (value) => setState(() => _dealerId = value),
),
],
if (_planType == 'monthlySpecial') ...[
const SizedBox(height: 12),
DropdownButtonFormField<int>(
initialValue: _month,
decoration: const InputDecoration(labelText: 'Month'),
items: [
for (var month = 1; month <= 12; month++)
DropdownMenuItem(value: month, child: Text('$month')),
],
onChanged: (value) {
if (value != null) setState(() => _month = value);
},
),
const SizedBox(height: 12),
DropdownButtonFormField<int>(
initialValue: _year,
decoration: const InputDecoration(labelText: 'Year'),
items: [
for (var year = DateTime.now().year - 1; year <= DateTime.now().year + 2; year++)
DropdownMenuItem(value: year, child: Text('$year')),
],
onChanged: (value) {
if (value != null) setState(() => _year = value);
},
),
],
const SizedBox(height: 16),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? 'Saving' : 'Save plan'),
),
],
),
);
}
}
+119
View File
@@ -0,0 +1,119 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
import 'event_form_screen.dart';
class EventsScreen extends StatefulWidget {
const EventsScreen({super.key});
@override
State<EventsScreen> createState() => _EventsScreenState();
}
class _EventsScreenState extends State<EventsScreen> {
List<EventPlan>? _events;
String? _planType;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final events = await context.read<AuthController>().api.events(planType: _planType);
if (!mounted) return;
setState(() {
_events = events;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
@override
Widget build(BuildContext context) {
final events = _events ?? [];
if (_loading && _events == null) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final created = await Navigator.of(context).push<bool>(
MaterialPageRoute(builder: (_) => const EventFormScreen()),
);
if (created == true) _load();
},
icon: const Icon(Icons.add),
label: const Text('New plan'),
),
body: RefreshIndicator(
onRefresh: _load,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 96),
children: [
Wrap(
spacing: 8,
children: [
ChoiceChip(
label: const Text('All'),
selected: _planType == null,
onSelected: (_) {
setState(() => _planType = null);
_load();
},
),
ChoiceChip(
label: const Text('General'),
selected: _planType == 'general',
onSelected: (_) {
setState(() => _planType = 'general');
_load();
},
),
ChoiceChip(
label: const Text('Monthly'),
selected: _planType == 'monthlySpecial',
onSelected: (_) {
setState(() => _planType = 'monthlySpecial');
_load();
},
),
],
),
const SizedBox(height: 12),
if (events.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: Text('No plans yet.')),
),
for (final event in events)
Card(
child: ListTile(
title: Text(event.title),
subtitle: Text(
'${planLabel(event.planType)}'
'${event.month != null ? ' · ${event.month}/${event.year}' : ''}'
'\n${event.dealerName ?? ''}\n${event.description}',
),
isThreeLine: true,
trailing: Text(event.expense.toStringAsFixed(0)),
),
),
],
),
),
);
}
}
+156
View File
@@ -0,0 +1,156 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../services/auth_controller.dart';
import 'add_dealer_screen.dart';
import 'admin_screen.dart';
import 'events_screen.dart';
import 'login_requests_screen.dart';
import 'service_points_screen.dart';
import 'stock_screen.dart';
class HomeShell extends StatefulWidget {
const HomeShell({super.key});
@override
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> {
int _index = 0;
int _pendingLogins = 0;
int _stockVersion = 0;
Timer? _pendingTimer;
@override
void initState() {
super.initState();
_refreshPending();
_pendingTimer = Timer.periodic(const Duration(seconds: 8), (_) => _refreshPending());
}
@override
void dispose() {
_pendingTimer?.cancel();
super.dispose();
}
Future<void> _refreshPending() async {
final role = context.read<AuthController>().user?.role;
if (role != 'superAdmin' && role != 'salesRep') return;
try {
final requests = await context.read<AuthController>().api.loginRequests();
if (!mounted) return;
setState(() => _pendingLogins = requests.length);
} catch (_) {}
}
Future<void> _logout() async {
final auth = context.read<AuthController>();
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Log out'),
content: Text('Remove the saved login for ${auth.user?.name ?? 'this account'}?'),
actions: [
TextButton(onPressed: () => Navigator.pop(context, false), child: const Text('Cancel')),
FilledButton(onPressed: () => Navigator.pop(context, true), child: const Text('Log out')),
],
),
);
if (confirmed == true && mounted) {
await context.read<AuthController>().logout();
}
}
@override
Widget build(BuildContext context) {
final auth = context.watch<AuthController>();
final role = auth.user?.role ?? 'dealer';
final pages = <Widget>[
StockScreen(key: ValueKey(_stockVersion)),
const EventsScreen(),
const ServicePointsScreen(),
if (role == 'superAdmin') const AdminScreen(),
];
final destinations = <NavigationDestination>[
const NavigationDestination(icon: Icon(Icons.inventory_2_outlined), label: 'Stock'),
const NavigationDestination(icon: Icon(Icons.event_note_outlined), label: 'Plans'),
const NavigationDestination(icon: Icon(Icons.handyman_outlined), label: 'Service'),
if (role == 'superAdmin')
const NavigationDestination(icon: Icon(Icons.admin_panel_settings_outlined), label: 'Admin'),
];
final safeIndex = _index.clamp(0, pages.length - 1);
final titles = ['Stock', 'Plans', 'Service', 'Admin'];
return Scaffold(
appBar: AppBar(
title: Text(titles[safeIndex]),
actions: [
if (role == 'superAdmin' || role == 'salesRep')
IconButton(
tooltip: 'Login requests',
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const LoginRequestsScreen()),
);
_refreshPending();
},
icon: Badge(
isLabelVisible: _pendingLogins > 0,
label: Text('$_pendingLogins'),
child: const Icon(Icons.how_to_reg_outlined),
),
),
if (role == 'salesRep')
IconButton(
tooltip: 'Add dealer',
onPressed: () async {
final created = await Navigator.of(context).push<bool>(
MaterialPageRoute(builder: (_) => const AddDealerScreen()),
);
if (created != true || !context.mounted) return;
try {
await context.read<AuthController>().refreshProfile();
} catch (_) {}
if (context.mounted) setState(() => _stockVersion++);
},
icon: const Icon(Icons.add_business_outlined),
),
IconButton(
tooltip: 'Account',
onPressed: _logout,
icon: const Icon(Icons.logout),
),
],
),
body: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 960),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Text(
'${auth.user?.name ?? ''} · ${roleLabel(role)}',
style: Theme.of(context).textTheme.labelLarge,
),
),
Expanded(child: pages[safeIndex]),
],
),
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: safeIndex,
destinations: destinations,
onDestinationSelected: (index) => setState(() => _index = index),
),
);
}
}
+102
View File
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
class LoginRequestsScreen extends StatefulWidget {
const LoginRequestsScreen({super.key});
@override
State<LoginRequestsScreen> createState() => _LoginRequestsScreenState();
}
class _LoginRequestsScreenState extends State<LoginRequestsScreen> {
List<LoginRequestItem>? _requests;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
try {
final requests = await context.read<AuthController>().api.loginRequests();
if (!mounted) return;
setState(() {
_requests = requests;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
Future<void> _decide(LoginRequestItem request, {required bool approve}) async {
final api = context.read<AuthController>().api;
try {
if (approve) {
await api.approveLogin(request.id);
} else {
await api.rejectLogin(request.id);
}
await _load();
} catch (error) {
if (mounted) showError(context, error);
}
}
@override
Widget build(BuildContext context) {
final requests = _requests ?? [];
return Scaffold(
appBar: AppBar(title: const Text('Login requests')),
body: _loading && _requests == null
? const Center(child: CircularProgressIndicator())
: RefreshIndicator(
onRefresh: _load,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
if (requests.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: Text('No dealers are waiting to sign in.')),
),
for (final request in requests)
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(request.name, style: Theme.of(context).textTheme.titleMedium),
Text(request.phone),
if (request.dealerName != null) Text(request.dealerName!),
Row(
children: [
TextButton(
onPressed: () => _decide(request, approve: false),
child: const Text('Reject'),
),
FilledButton(
onPressed: () => _decide(request, approve: true),
child: const Text('Approve'),
),
],
),
],
),
),
),
],
),
),
);
}
}
@@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
class ServicePointFormScreen extends StatefulWidget {
const ServicePointFormScreen({super.key});
@override
State<ServicePointFormScreen> createState() => _ServicePointFormScreenState();
}
class _ServicePointFormScreenState extends State<ServicePointFormScreen> {
final _name = TextEditingController();
final _address = TextEditingController();
String? _district;
final _phone = TextEditingController();
final _notes = TextEditingController();
bool _saving = false;
@override
void dispose() {
_name.dispose();
_address.dispose();
_phone.dispose();
_notes.dispose();
super.dispose();
}
Future<void> _save() async {
if (_district == null) {
showError(context, 'Choose a district');
return;
}
setState(() => _saving = true);
try {
await context.read<AuthController>().api.createServicePoint({
'name': _name.text.trim(),
'district': _district,
'address': _address.text.trim(),
'phone': _phone.text.trim(),
'notes': _notes.text.trim(),
});
if (mounted) Navigator.pop(context, true);
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Add service point')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(controller: _name, decoration: const InputDecoration(labelText: 'Name')),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _district,
isExpanded: true,
decoration: const InputDecoration(labelText: 'District'),
items: [
for (final district in sriLankaDistricts)
DropdownMenuItem(value: district, child: Text(district)),
],
onChanged: (value) => setState(() => _district = value),
),
const SizedBox(height: 12),
TextField(controller: _address, decoration: const InputDecoration(labelText: 'Address')),
const SizedBox(height: 12),
TextField(controller: _phone, decoration: const InputDecoration(labelText: 'Phone')),
const SizedBox(height: 12),
TextField(
controller: _notes,
minLines: 2,
maxLines: 4,
decoration: const InputDecoration(labelText: 'Notes'),
),
const SizedBox(height: 16),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? 'Saving' : 'Save service point'),
),
],
),
);
}
}
+132
View File
@@ -0,0 +1,132 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
import 'service_point_form_screen.dart';
class ServicePointsScreen extends StatefulWidget {
const ServicePointsScreen({super.key});
@override
State<ServicePointsScreen> createState() => _ServicePointsScreenState();
}
class _ServicePointsScreenState extends State<ServicePointsScreen> {
final _search = TextEditingController();
String? _district;
List<ServicePoint>? _points;
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
@override
void dispose() {
_search.dispose();
super.dispose();
}
Future<void> _load() async {
try {
final points = await context.read<AuthController>().api.servicePoints(
district: _district,
search: _search.text.trim(),
);
if (!mounted) return;
setState(() {
_points = points;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
@override
Widget build(BuildContext context) {
final role = context.watch<AuthController>().user?.role;
final points = _points ?? [];
if (_loading && _points == null) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
floatingActionButton: role == 'dealer'
? null
: FloatingActionButton.extended(
onPressed: () async {
final created = await Navigator.of(context).push<bool>(
MaterialPageRoute(builder: (_) => const ServicePointFormScreen()),
);
if (created == true) _load();
},
icon: const Icon(Icons.add),
label: const Text('Add point'),
),
body: RefreshIndicator(
onRefresh: _load,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 96),
children: [
DropdownButtonFormField<String>(
initialValue: _district,
isExpanded: true,
decoration: const InputDecoration(labelText: 'District'),
items: [
const DropdownMenuItem(value: null, child: Text('All districts')),
for (final district in sriLankaDistricts)
DropdownMenuItem(value: district, child: Text(district)),
],
onChanged: (value) {
setState(() => _district = value);
_load();
},
),
const SizedBox(height: 12),
TextField(
controller: _search,
decoration: const InputDecoration(
labelText: 'Search',
hintText: 'Name, repair work, phone',
),
onSubmitted: (_) => _load(),
),
const SizedBox(height: 12),
Align(
alignment: Alignment.centerLeft,
child: FilledButton(onPressed: _load, child: const Text('Search')),
),
const SizedBox(height: 12),
if (points.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: Text('No service points match.')),
),
for (final point in points)
Card(
child: ListTile(
title: Text(point.name),
subtitle: Text(
[
point.district,
if (point.address != null && point.address!.isNotEmpty) point.address,
if (point.phone != null && point.phone!.isNotEmpty) point.phone,
if (point.notes != null && point.notes!.isNotEmpty) point.notes,
].join('\n'),
),
),
),
],
),
),
);
}
}
+27
View File
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
class SplashScreen extends StatelessWidget {
const SplashScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
'assets/images/senaro_logo.png',
width: 220,
semanticLabel: 'Senaro',
),
SizedBox(height: 16),
Text('Dealer Network'),
SizedBox(height: 24),
CircularProgressIndicator(),
],
),
),
);
}
}
+224
View File
@@ -0,0 +1,224 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../theme/app_theme.dart';
import '../ui/feedback.dart';
class StockDetailScreen extends StatefulWidget {
const StockDetailScreen({super.key, required this.stock, required this.dealers});
final StockItem stock;
final List<Dealer> dealers;
@override
State<StockDetailScreen> createState() => _StockDetailScreenState();
}
class _StockDetailScreenState extends State<StockDetailScreen> {
late StockItem _stock = widget.stock;
String _paymentType = 'cash';
bool _useGci = true;
bool _useFinance = false;
String? _transferDealerId;
final _financeName = TextEditingController();
final _plateNumber = TextEditingController();
bool _saving = false;
@override
void dispose() {
_financeName.dispose();
_plateNumber.dispose();
super.dispose();
}
Future<void> _advance() async {
final stage = nextStage(_stock.currentStage);
if (stage == null) return;
final body = <String, dynamic>{'stage': stage};
if (stage == 'paymentApproved') {
body['paymentType'] = _paymentType;
if (_paymentType == 'lease') {
if (!_useGci && !_useFinance) {
showError(context, 'Choose GCI, finance, or both');
return;
}
body['leaseProviderType'] = _useGci && _useFinance
? 'both'
: _useGci
? 'gci'
: 'thirdPartyFinance';
if (_useFinance) body['financeCompanyName'] = _financeName.text.trim();
}
}
if (stage == 'plateReceived') {
final plateNumber = _plateNumber.text.trim();
if (plateNumber.isEmpty) {
showError(context, 'Enter the plate number');
return;
}
body['plateNumber'] = plateNumber;
}
setState(() => _saving = true);
try {
final updated = await context.read<AuthController>().api.updateStage(_stock.chassisNumber, body);
if (!mounted) return;
setState(() => _stock = updated);
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
Future<void> _transfer() async {
final dealerId = _transferDealerId;
if (dealerId == null) return;
setState(() => _saving = true);
try {
final updated = await context.read<AuthController>().api.transferStock(_stock.chassisNumber, dealerId);
if (!mounted) return;
setState(() {
_stock = updated;
_transferDealerId = null;
});
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
final role = context.watch<AuthController>().user?.role;
final upcoming = nextStage(_stock.currentStage);
final targets = widget.dealers.where((dealer) => dealer.id != _stock.currentDealerId).toList();
return Scaffold(
appBar: AppBar(title: Text(_stock.chassisNumber)),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Row(
children: [
CircleAvatar(
backgroundColor: badgeColor(_stock.badge),
child: Text('${_stock.ageInDays}', style: const TextStyle(color: Colors.white, fontSize: 12)),
),
const SizedBox(width: 12),
Expanded(
child: Text(
'${_stock.ageInDays} days in stock · ${stageLabel(_stock.currentStage)}',
style: Theme.of(context).textTheme.titleMedium,
),
),
],
),
const SizedBox(height: 16),
_line('Model', modelLabel(_stock.model)),
_line('Color', _stock.color),
_line('Engine', _stock.engineNumber),
if (_stock.plateNumber != null) _line('Plate', _stock.plateNumber!),
_line('Dealer', _stock.dealerName ?? _stock.currentDealerId),
_line('Received', _stock.dateReceived.toLocal().toString().split(' ').first),
if (_stock.paymentType != null) _line('Payment', _stock.paymentType!),
if (_stock.leaseProviderType != null)
_line('Lease', leaseProviderLabel(_stock.leaseProviderType!)),
if (_stock.financeCompanyName != null) _line('Finance company', _stock.financeCompanyName!),
const SizedBox(height: 16),
Text('Stage log', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 8),
for (final stage in stageOrder)
if (_stock.stageLogDates.containsKey(stage))
ListTile(
contentPadding: EdgeInsets.zero,
title: Text(stageLabel(stage)),
trailing: Text(_stock.stageLogDates[stage]!.toLocal().toString().split(' ').first),
),
if (upcoming != null) ...[
const Divider(),
Text('Advance to ${stageLabel(upcoming)}', style: Theme.of(context).textTheme.titleMedium),
if (upcoming == 'paymentApproved') ...[
const SizedBox(height: 12),
SegmentedButton<String>(
segments: const [
ButtonSegment(value: 'cash', label: Text('Cash')),
ButtonSegment(value: 'lease', label: Text('Lease')),
],
selected: {_paymentType},
onSelectionChanged: (value) => setState(() => _paymentType = value.first),
),
if (_paymentType == 'lease') ...[
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: const Text('GCI'),
value: _useGci,
onChanged: (value) => setState(() => _useGci = value ?? false),
),
CheckboxListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Finance'),
value: _useFinance,
onChanged: (value) => setState(() => _useFinance = value ?? false),
),
if (_useFinance)
TextField(
controller: _financeName,
decoration: const InputDecoration(labelText: 'Finance company'),
),
],
],
if (upcoming == 'plateReceived') ...[
const SizedBox(height: 12),
TextField(
controller: _plateNumber,
textCapitalization: TextCapitalization.characters,
decoration: const InputDecoration(labelText: 'Plate number'),
),
],
const SizedBox(height: 12),
FilledButton(
onPressed: _saving ? null : _advance,
child: Text(_saving ? 'Saving' : 'Update stage'),
),
],
if (role != 'dealer' && targets.isNotEmpty) ...[
const Divider(),
Text('Transfer', style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _transferDealerId,
decoration: const InputDecoration(labelText: 'Destination dealer'),
items: [
for (final dealer in targets)
DropdownMenuItem(value: dealer.id, child: Text('${dealer.name} (${dealer.code})')),
],
onChanged: (value) => setState(() => _transferDealerId = value),
),
const SizedBox(height: 12),
OutlinedButton(
onPressed: _saving || _transferDealerId == null ? null : _transfer,
child: const Text('Transfer unit'),
),
],
],
),
);
}
Widget _line(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(width: 140, child: Text(label)),
Expanded(child: Text(value)),
],
),
);
}
}
+144
View File
@@ -0,0 +1,144 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../labels.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../ui/feedback.dart';
class StockFormScreen extends StatefulWidget {
const StockFormScreen({super.key, required this.dealers});
final List<Dealer> dealers;
@override
State<StockFormScreen> createState() => _StockFormScreenState();
}
class _StockFormScreenState extends State<StockFormScreen> {
final _chassis = TextEditingController();
final _engine = TextEditingController();
final _color = TextEditingController();
String _model = modelOptions.first;
String? _dealerId;
DateTime _received = DateTime.now();
bool _saving = false;
@override
void initState() {
super.initState();
final user = context.read<AuthController>().user;
if (user?.role == 'dealer') {
_dealerId = user?.dealerId ?? (widget.dealers.isNotEmpty ? widget.dealers.first.id : null);
} else if (widget.dealers.length == 1) {
_dealerId = widget.dealers.first.id;
}
}
@override
void dispose() {
_chassis.dispose();
_engine.dispose();
_color.dispose();
super.dispose();
}
Future<void> _save() async {
if (_dealerId == null) {
showError(context, 'Choose a dealer');
return;
}
setState(() => _saving = true);
try {
await context.read<AuthController>().api.createStock({
'chassisNumber': _chassis.text.trim(),
'engineNumber': _engine.text.trim(),
'model': _model,
'color': _color.text.trim(),
'currentDealerId': _dealerId,
'dateReceived': _received.toUtc().toIso8601String(),
});
if (mounted) Navigator.pop(context, true);
} catch (error) {
if (mounted) showError(context, error);
} finally {
if (mounted) setState(() => _saving = false);
}
}
@override
Widget build(BuildContext context) {
final lockedToOwnDealer = context.watch<AuthController>().user?.role == 'dealer';
Dealer? ownDealer;
for (final dealer in widget.dealers) {
if (dealer.id == _dealerId) {
ownDealer = dealer;
break;
}
}
return Scaffold(
appBar: AppBar(title: const Text('Add stock')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
TextField(controller: _chassis, decoration: const InputDecoration(labelText: 'Chassis number')),
const SizedBox(height: 12),
TextField(controller: _engine, decoration: const InputDecoration(labelText: 'Engine number')),
const SizedBox(height: 12),
DropdownButtonFormField<String>(
initialValue: _model,
decoration: const InputDecoration(labelText: 'Model'),
items: [
for (final model in modelOptions)
DropdownMenuItem(value: model, child: Text(modelLabel(model))),
],
onChanged: (value) {
if (value != null) setState(() => _model = value);
},
),
const SizedBox(height: 12),
TextField(controller: _color, decoration: const InputDecoration(labelText: 'Color')),
const SizedBox(height: 12),
if (lockedToOwnDealer)
InputDecorator(
decoration: const InputDecoration(labelText: 'Dealer'),
child: Text(
ownDealer == null ? 'Your dealership' : '${ownDealer.name} (${ownDealer.code})',
),
)
else
DropdownButtonFormField<String>(
initialValue: _dealerId,
decoration: const InputDecoration(labelText: 'Dealer'),
items: [
for (final dealer in widget.dealers)
DropdownMenuItem(value: dealer.id, child: Text('${dealer.name} (${dealer.code})')),
],
onChanged: (value) => setState(() => _dealerId = value),
),
const SizedBox(height: 12),
ListTile(
contentPadding: EdgeInsets.zero,
title: const Text('Received date'),
subtitle: Text(_received.toLocal().toString().split(' ').first),
trailing: const Icon(Icons.calendar_today),
onTap: () async {
final picked = await showDatePicker(
context: context,
initialDate: _received,
firstDate: DateTime(2000),
lastDate: DateTime.now().add(const Duration(days: 1)),
);
if (picked != null) setState(() => _received = picked);
},
),
const SizedBox(height: 16),
FilledButton(
onPressed: _saving ? null : _save,
child: Text(_saving ? 'Saving' : 'Save stock'),
),
],
),
);
}
}
+168
View File
@@ -0,0 +1,168 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../models/models.dart';
import '../services/auth_controller.dart';
import '../theme/app_theme.dart';
import '../ui/feedback.dart';
import 'dealer_stock_screen.dart';
import 'stock_form_screen.dart';
class StockScreen extends StatefulWidget {
const StockScreen({super.key});
@override
State<StockScreen> createState() => _StockScreenState();
}
class _StockScreenState extends State<StockScreen> {
List<Dealer> _dealers = [];
List<StockItem> _stocks = [];
bool _loading = true;
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final api = context.read<AuthController>().api;
try {
final results = await Future.wait([
api.dealers(),
api.stocks(),
]);
if (!mounted) return;
setState(() {
_dealers = results[0] as List<Dealer>;
_stocks = results[1] as List<StockItem>;
_loading = false;
});
} catch (error) {
if (!mounted) return;
setState(() => _loading = false);
showError(context, error);
}
}
int _count(String dealerId, String badge) {
return _stocks.where((item) => item.currentDealerId == dealerId && item.badge == badge).length;
}
@override
Widget build(BuildContext context) {
final role = context.watch<AuthController>().user?.role;
final canAdd = role == 'dealer' || role == 'salesRep' || role == 'superAdmin';
if (_loading && _dealers.isEmpty && _stocks.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
floatingActionButton: canAdd
? FloatingActionButton.extended(
onPressed: () async {
final created = await Navigator.of(context).push<bool>(
MaterialPageRoute(builder: (_) => StockFormScreen(dealers: _dealers)),
);
if (created == true) _load();
},
icon: const Icon(Icons.add),
label: const Text('Add stock'),
)
: null,
body: RefreshIndicator(
onRefresh: _load,
child: ListView(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 96),
children: [
if (_dealers.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(child: Text('No dealers to show.')),
),
for (final dealer in _dealers)
Card(
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
await Navigator.of(context).push<bool>(
MaterialPageRoute(
builder: (_) => DealerStockScreen(dealer: dealer, dealers: _dealers),
),
);
if (mounted) _load();
},
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(dealer.name, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 2),
Text(
dealer.district == null || dealer.district!.isEmpty
? dealer.code
: '${dealer.code} · ${dealer.district}',
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _AgeCount(
label: 'Under 30',
count: _count(dealer.id, 'green'),
badge: 'green',
),
),
Expanded(
child: _AgeCount(
label: '30–45',
count: _count(dealer.id, 'yellow'),
badge: 'yellow',
),
),
Expanded(
child: _AgeCount(
label: 'Over 45',
count: _count(dealer.id, 'red'),
badge: 'red',
),
),
],
),
],
),
),
),
),
],
),
),
);
}
}
class _AgeCount extends StatelessWidget {
const _AgeCount({required this.label, required this.count, required this.badge});
final String label;
final int count;
final String badge;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'$count',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(color: badgeColor(badge)),
),
const SizedBox(height: 2),
Text(label, style: Theme.of(context).textTheme.bodySmall, textAlign: TextAlign.center),
],
);
}
}
+228
View File
@@ -0,0 +1,228 @@
import 'package:dio/dio.dart';
import '../config/api_config.dart';
import '../models/models.dart';
class ApiException implements Exception {
ApiException(this.message, {this.statusCode});
final String message;
final int? statusCode;
@override
String toString() => message;
}
class ApiClient {
ApiClient() {
dio = Dio(
BaseOptions(
baseUrl: resolveApiBaseUrl(),
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 15),
headers: const {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
if (token != null && token!.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
},
),
);
}
late final Dio dio;
String? token;
Future<({String token, AppUser user})> staffLogin(String phone, String password) {
return _send(
dio.post('/auth/staff-login', data: {'phone': phone, 'password': password}),
(data) {
final map = data as Map<String, dynamic>;
return (
token: map['token'] as String,
user: AppUser.fromJson(map['user'] as Map<String, dynamic>),
);
},
);
}
Future<({String requestId, String secret})> dealerLogin(String phone) {
return _send(dio.post('/auth/dealer-login', data: {'phone': phone}), (data) {
final map = data as Map<String, dynamic>;
return (requestId: map['requestId'] as String, secret: map['secret'] as String);
});
}
Future<DealerLoginStatus> dealerLoginStatus(String requestId, String secret) {
return _send(
dio.get('/auth/dealer-login/$requestId', queryParameters: {'secret': secret}),
(data) => DealerLoginStatus.fromJson(data as Map<String, dynamic>),
);
}
Future<List<LoginRequestItem>> loginRequests() {
return _send(dio.get('/auth/login-requests'), (data) => _list(data, LoginRequestItem.fromJson));
}
Future<void> approveLogin(String id) {
return _send(dio.post('/auth/login-requests/$id/approve'), (_) {});
}
Future<void> rejectLogin(String id) {
return _send(dio.post('/auth/login-requests/$id/reject'), (_) {});
}
Future<AppUser> me() {
return _send(
dio.get('/auth/me'),
(data) => AppUser.fromJson(data as Map<String, dynamic>),
);
}
Future<List<Dealer>> dealers() {
return _send(dio.get('/dealers'), (data) => _list(data, Dealer.fromJson));
}
Future<List<StockItem>> stocks({String? dealerId}) {
return _send(
dio.get(
'/stocks',
queryParameters: {
if (dealerId != null && dealerId.isNotEmpty) 'dealerId': dealerId,
},
),
(data) => _list(data, StockItem.fromJson),
);
}
Future<StockItem> createStock(Map<String, dynamic> body) {
return _send(
dio.post('/stocks', data: body),
(data) => StockItem.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
);
}
Future<StockItem> updateStage(String chassisNumber, Map<String, dynamic> body) {
return _send(
dio.patch('/stocks/$chassisNumber/stage', data: body),
(data) => StockItem.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
);
}
Future<StockItem> transferStock(String chassisNumber, String dealerId) {
return _send(
dio.post('/stocks/$chassisNumber/transfer', data: {'dealerId': dealerId}),
(data) => StockItem.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
);
}
Future<List<EventPlan>> events({String? dealerId, String? planType}) {
return _send(
dio.get(
'/events',
queryParameters: {
if (dealerId != null && dealerId.isNotEmpty) 'dealerId': dealerId,
if (planType != null && planType.isNotEmpty) 'planType': planType,
},
),
(data) => _list(data, EventPlan.fromJson),
);
}
Future<EventPlan> createEvent(Map<String, dynamic> body) {
return _send(
dio.post('/events', data: body),
(data) => EventPlan.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
);
}
Future<List<ServicePoint>> servicePoints({String? district, String? search}) {
return _send(
dio.get(
'/service-points',
queryParameters: {
if (district != null && district.isNotEmpty) 'district': district,
if (search != null && search.isNotEmpty) 'search': search,
},
),
(data) => _list(data, ServicePoint.fromJson),
);
}
Future<ServicePoint> createServicePoint(Map<String, dynamic> body) {
return _send(
dio.post('/service-points', data: body),
(data) => ServicePoint.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
);
}
Future<Dealer> createDealer(Map<String, dynamic> body) {
return _send(
dio.post('/admin/dealers', data: body),
(data) => Dealer.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
);
}
Future<void> deleteDealer(String id) {
return _send(dio.delete('/admin/dealers/$id'), (_) {});
}
Future<List<Dealer>> adminDealers() {
return _send(dio.get('/admin/dealers'), (data) => _list(data, Dealer.fromJson));
}
Future<List<AppUser>> salesReps() {
return _send(dio.get('/admin/sales-reps'), (data) => _list(data, AppUser.fromJson));
}
Future<AppUser> createSalesRep(Map<String, dynamic> body) {
return _send(
dio.post('/admin/sales-reps', data: body),
(data) => AppUser.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
);
}
Future<AppUser> assignDealer({
required String salesRepId,
required String dealerId,
required bool assigned,
}) {
return _send(
dio.patch(
'/admin/assign-dealer',
data: {
'salesRepId': salesRepId,
'dealerId': dealerId,
'assigned': assigned,
},
),
(data) => AppUser.fromJson((data as Map<String, dynamic>)['data'] as Map<String, dynamic>),
);
}
Future<T> _send<T>(Future<Response<dynamic>> request, T Function(dynamic data) parse) async {
try {
final response = await request;
return parse(response.data);
} on DioException catch (error) {
final data = error.response?.data;
final message = data is Map && data['message'] is String
? data['message'] as String
: 'Network request failed';
throw ApiException(message, statusCode: error.response?.statusCode);
}
}
List<T> _list<T>(dynamic data, T Function(Map<String, dynamic>) parse) {
final raw = (data as Map<String, dynamic>)['data'] as List<dynamic>;
return raw.map((item) => parse(item as Map<String, dynamic>)).toList();
}
}
+103
View File
@@ -0,0 +1,103 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../models/models.dart';
import 'api_client.dart';
class AuthController extends ChangeNotifier {
AuthController({FlutterSecureStorage? storage, ApiClient? apiClient})
: _storage = storage ?? const FlutterSecureStorage(),
api = apiClient ?? ApiClient();
final FlutterSecureStorage _storage;
final ApiClient api;
AppUser? user;
bool ready = false;
bool busy = false;
String? error;
Future<void> bootstrap() async {
try {
final stored = await _storage.read(key: 'jwt_token');
if (stored == null || stored.isEmpty) return;
api.token = stored;
user = await api.me();
} catch (_) {
api.token = null;
user = null;
await _storage.delete(key: 'jwt_token');
} finally {
ready = true;
notifyListeners();
}
}
Future<void> staffLogin(String phone, String password) async {
await _run(() async {
final result = await api.staffLogin(phone, password);
await _storeSession(result.token, result.user);
});
}
Future<({String requestId, String secret})> startDealerLogin(String phone) async {
late ({String requestId, String secret}) started;
await _run(() async {
started = await api.dealerLogin(phone);
});
return started;
}
Future<String> pollDealerLogin(String requestId, String secret) async {
final status = await api.dealerLoginStatus(requestId, secret);
if (status.status == 'approved' && status.token != null && status.user != null) {
await _storeSession(status.token!, status.user!);
notifyListeners();
} else if (status.status == 'rejected') {
error = 'Login was rejected';
notifyListeners();
} else if (status.status == 'expired' || status.status == 'consumed') {
error = 'Login request expired. Try again.';
notifyListeners();
}
return status.status;
}
Future<void> _storeSession(String token, AppUser profile) async {
api.token = token;
await _storage.write(key: 'jwt_token', value: token);
user = profile;
}
Future<void> refreshProfile() async {
if (api.token == null) return;
user = await api.me();
notifyListeners();
}
Future<void> logout() async {
api.token = null;
user = null;
error = null;
await _storage.delete(key: 'jwt_token');
notifyListeners();
}
Future<void> _run(Future<void> Function() action) async {
busy = true;
error = null;
notifyListeners();
try {
await action();
} on ApiException catch (error) {
this.error = error.message;
rethrow;
} catch (error) {
this.error = 'Network request failed';
rethrow;
} finally {
busy = false;
notifyListeners();
}
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
ThemeData buildAppTheme() {
const seed = Color(0xFF0E4D6C);
final scheme = ColorScheme.fromSeed(seedColor: seed);
return ThemeData(
colorScheme: scheme,
useMaterial3: true,
scaffoldBackgroundColor: const Color(0xFFF4F7F8),
inputDecorationTheme: const InputDecorationTheme(
border: OutlineInputBorder(),
),
cardTheme: CardThemeData(
elevation: 0,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: scheme.outlineVariant),
),
),
);
}
Color badgeColor(String badge) {
switch (badge) {
case 'green':
return const Color(0xFF1B7F4E);
case 'yellow':
return const Color(0xFF8A6A12);
case 'red':
return const Color(0xFFB42318);
default:
return const Color(0xFF5C6B73);
}
}
+18
View File
@@ -0,0 +1,18 @@
import 'package:flutter/material.dart';
import '../services/api_client.dart';
void showError(BuildContext context, Object error) {
final message = switch (error) {
ApiException(:final message) => message,
String message => message,
_ => 'Something went wrong',
};
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
}
Future<void> showNotice(BuildContext context, String message) {
return ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(message)))
.closed;
}
+466
View File
@@ -0,0 +1,466 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
url: "https://pub.dev"
source: hosted
version: "1.0.9"
dio:
dependency: "direct main"
description:
name: dio
sha256: "852ec3b48cc431ac04fff978413c541502b67ffc3e26921e74e3d994694192c1"
url: "https://pub.dev"
source: hosted
version: "5.11.1"
dio_web_adapter:
dependency: transitive
description:
name: dio_web_adapter
sha256: "3a1b2cd7be71086f38504956e3ebcd2837288d231ff454bafa78021244102bfc"
url: "https://pub.dev"
source: hosted
version: "2.2.2"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
ffi:
dependency: transitive
description:
name: ffi
sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_secure_storage:
dependency: "direct main"
description:
name: flutter_secure_storage
sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea"
url: "https://pub.dev"
source: hosted
version: "9.2.4"
flutter_secure_storage_linux:
dependency: transitive
description:
name: flutter_secure_storage_linux
sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688
url: "https://pub.dev"
source: hosted
version: "1.2.3"
flutter_secure_storage_macos:
dependency: transitive
description:
name: flutter_secure_storage_macos
sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247"
url: "https://pub.dev"
source: hosted
version: "3.1.3"
flutter_secure_storage_platform_interface:
dependency: transitive
description:
name: flutter_secure_storage_platform_interface
sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8
url: "https://pub.dev"
source: hosted
version: "1.1.2"
flutter_secure_storage_web:
dependency: transitive
description:
name: flutter_secure_storage_web
sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9
url: "https://pub.dev"
source: hosted
version: "1.2.1"
flutter_secure_storage_windows:
dependency: transitive
description:
name: flutter_secure_storage_windows
sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709
url: "https://pub.dev"
source: hosted
version: "3.1.2"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
jni:
dependency: transitive
description:
name: jni
sha256: "6b9ca0602fef0230e83149afbe3f9f8227e94f2bbe84a1da4f14d699742f402d"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
jni_flutter:
dependency: transitive
description:
name: jni_flutter
sha256: c399ad579bfba12eb20c924f7a23160a42902f3023a871e1cad40a8a679656a4
url: "https://pub.dev"
source: hosted
version: "1.0.4"
jni_util:
dependency: transitive
description:
name: jni_util
sha256: "1ba86da04a5f2bf18fde2edb235587e70c5b0fc5bd4ba955f46b00942c3fc35f"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
url: "https://pub.dev"
source: hosted
version: "1.16.0"
mime:
dependency: transitive
description:
name: mime
sha256: bd47de35f07e27267e69c8c8b22edf9473bfee170a60d60fcc93730c5144b7f6
url: "https://pub.dev"
source: hosted
version: "2.1.0"
nested:
dependency: transitive
description:
name: nested
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
url: "https://pub.dev"
source: hosted
version: "1.0.0"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
path_provider:
dependency: transitive
description:
name: path_provider
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
url: "https://pub.dev"
source: hosted
version: "2.1.5"
path_provider_android:
dependency: transitive
description:
name: path_provider_android
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
provider:
dependency: "direct main"
description:
name: provider
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
url: "https://pub.dev"
source: hosted
version: "6.1.5+1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00"
url: "https://pub.dev"
source: hosted
version: "0.7.6"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "5f37239c4851efcef929cea7824e76df7f2f0970aef85d66bbc430afa40e72f0"
url: "https://pub.dev"
source: hosted
version: "15.3.0"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
win32:
dependency: transitive
description:
name: win32
sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e
url: "https://pub.dev"
source: hosted
version: "5.15.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
sdks:
dart: ">=3.9.2 <4.0.0"
flutter: ">=3.35.6"
+90
View File
@@ -0,0 +1,90 @@
name: dealer_network
description: Dealership inventory and operations client.
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.9.2
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dio: ^5.9.0
flutter_secure_storage: ^9.2.4
provider: ^6.1.5
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
assets:
- assets/images/senaro_logo.png
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+28
View File
@@ -0,0 +1,28 @@
import 'package:dealer_network/models/models.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('parses a stock item age badge', () {
final stock = StockItem.fromJson({
'id': '1',
'chassisNumber': 'CH-1',
'engineNumber': 'EN-1',
'model': 'gn125',
'color': 'Red',
'currentDealerId': 'd1',
'dealerName': 'Kandy Motors',
'dateReceived': '2026-01-01T00:00:00.000Z',
'currentStage': 'received',
'stageLogDates': {'received': '2026-01-01T00:00:00.000Z'},
'paymentType': null,
'leaseProviderType': null,
'financeCompanyName': null,
'ageInDays': 50,
'badge': 'red',
});
expect(stock.badge, 'red');
expect(stock.ageInDays, 50);
expect(stock.stageLogDates['received'], DateTime.parse('2026-01-01T00:00:00.000Z'));
});
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

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