Initial commit of Doormile CRM Mobile with UI modernizations

This commit is contained in:
2026-06-26 13:43:37 +05:30
commit 3965046984
183 changed files with 18135 additions and 0 deletions

45
.gitignore vendored Normal file
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

45
.metadata Normal file
View File

@@ -0,0 +1,45 @@
# 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: "bd7a4a6b5576630823ca344e3e684c53aa1a0f46"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
- platform: android
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
- platform: ios
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
- platform: linux
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
- platform: macos
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
- platform: web
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
- platform: windows
create_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
base_revision: bd7a4a6b5576630823ca344e3e684c53aa1a0f46
# 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'

22
README.md Normal file
View File

@@ -0,0 +1,22 @@
# doormile_flutter
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.
PS E:\Doormile\Doormile-app\crm_flutter> claude
Resume this session with:
claude --resume 0ea33e07-ef7d-4b60-99ee-23d954cb58ac

28
analysis_options.yaml Normal file
View File

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

14
android/.gitignore vendored Normal file
View File

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

View File

@@ -0,0 +1,75 @@
import java.util.Properties
import java.io.FileInputStream
val keystoreProperties = Properties()
val keystorePropertiesFile = File(rootDir, "key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
plugins {
id("com.android.application")
id("kotlin-android")
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.doormile.crm"
compileSdk = flutter.compileSdkVersion
ndkVersion = "28.2.13676358"
signingConfigs {
create("release") {
keyAlias = keystoreProperties.getProperty("keyAlias")
keyPassword = keystoreProperties.getProperty("keyPassword")
storeFile = file(keystoreProperties.getProperty("storeFile"))
storePassword = keystoreProperties.getProperty("storePassword")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
applicationId = "com.doormile.crm"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release")
isMinifyEnabled = true
isShrinkResources = true
ndk {
debugSymbolLevel = "SYMBOL_TABLE"
}
}
}
packaging {
jniLibs {
excludes += setOf("**/libjsc.so")
useLegacyPackaging = true
}
resources {
excludes += setOf("META-INF/*")
}
}
}
flutter {
source = "../.."
}

View File

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

View File

@@ -0,0 +1,50 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<application
android:label="Doormile CRM"
android:name="${applicationName}"
android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android: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>

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,11 @@
<?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" />
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launcher_icon" />
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View File

@@ -0,0 +1,11 @@
<?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" />
<item>
<bitmap
android:gravity="center"
android:src="@mipmap/launcher_icon" />
</item>
</layer-list>

View File

@@ -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="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

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

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

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

View File

@@ -0,0 +1,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
android/build.gradle.kts Normal file
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)
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,7 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
kotlin.incremental=false

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.14-all.zip

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.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")

252
api.txt Normal file
View File

@@ -0,0 +1,252 @@
like how we are doing thi specifer using mobile or wb
To specify whether the survey was taken on mobile or web, you just need to hardcode the registration_source identifier directly into the API request code of your respective frontends.
This is the most reliable way to handle it, because your mobile codebase uniquely knows it's the mobile app, and your web codebase uniquely knows it's the web app.
Here is exactly how you handle it in both environments:
1. In your Flutter (Mobile) App
When you build the JSON body to send your POST request via the http package or dio, you simply inject "mobile" and grab the current logged-in user's ID from your local state:
dart
// Inside your Flutter API service
Future<void> submitSurvey(String firstName, double lat, double lon, int currentUserId) async {
final payload = {
"first_name": firstName,
// ... other fields
"survey_lat": lat,
"survey_long": lon,
// Explicitly tag this as coming from the mobile app
"registration_source": "mobile",
"registered_by_id": currentUserId
};
await http.post(
Uri.parse('https://api.doormile.com/api/v1/crm/clients'),
headers: {"Content-Type": "application/json"},
body: jsonEncode(payload),
);
}
2. In your React / Web App
Similarly, inside your Web frontend's API submission function, you explicitly hardcode "web" as the source:
javascript
// Inside your Web frontend API utility
async function submitSurvey(formData, lat, lon, currentUserId) {
const payload = {
first_name: formData.firstName,
// ... other fields
survey_lat: lat,
survey_long: lon,
// Explicitly tag this as coming from the web dashboard
registration_source: "web",
registered_by_id: currentUserId
};
await fetch('https://api.doormile.com/api/v1/crm/clients', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
Alternative Option: Backend "User-Agent" Detection
If you don't want to pass it manually in the JSON payload, we can modify the Golang backend to automatically detect the platform by reading the HTTP Headers.
When a request hits your API, Fiber can check the User-Agent header (Flutter usually sends Dart/<version> (dart:io), while Web sends standard browser data like Mozilla/5.0...). Let me know if you would prefer the backend to automate this detection instead!
12:16 PM
Here are the **GET** and **POST** APIs that are now fully functional and mapped in your backend.
All endpoints use the base URL path: `http://localhost:8081/api/v1/crm/clients`
### 1. Create a New Client (POST)
**Endpoint:** `POST /api/v1/crm/clients`
**Description:** Registers a new client in `doormile_clients`, hashes their password, sets up their auth credentials in `doormile_auth`, and optionally accepts a vector for future AI search.
**Request Body (JSON):**
```json
{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"password": "securepassword123",
"phone": "+1234567890",
"address": "123 Main St, Tech City",
"vector": [0.1, 0.5, -0.3] // Optional: For Qdrant AI embedding integration
}
```
**Success Response (201 Created):**
```json
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
"address": "123 Main St, Tech City",
"role": "user"
}
```
---
### 2. Fetch All Clients (GET)
**Endpoint:** `GET /api/v1/crm/clients`
**Description:** Returns a list of all registered clients alongside their authentication details (like email and role) by performing a SQL join behind the scenes.
**Request Body:** None
**Success Response (200 OK):**
```json
[
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
"address": "123 Main St, Tech City",
"role": "user"
},
{
"id": 2,
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone": "+1987654321",
"address": "456 Commerce Rd",
"role": "user"
}
]
```
---
### 3. Fetch a Specific Client (GET)
**Endpoint:** `GET /api/v1/crm/clients/:id`
**Description:** Returns the details for a specific client matching the provided ID in the URL.
**Request Body:** None
**Success Response (200 OK) for `/api/v1/crm/clients/1`:**
```json
{
"id": 1,
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"phone": "+1234567890",
"address": "123 Main St, Tech City",
"role": "user"
}
```
### Implementation Notes:
* You can find the exact backend logic for these in `controllers/clientController.go`.
* The structured structs representing these API JSON payloads are located in `dto/client.go`.
Edited client.go
Edited client.go
Edited clientController.go
Ran command: `& "C:\Program Files\Go\bin\go.exe" run main.go`
This makes perfect sense—having an exact audit trail of exactly *where*, *when*, and *by whom* the survey was captured (alongside whether it was captured in the field on `mobile` or from the office on `web`) is crucial for data integrity.
I have completely refactored the models, DTOs, and API logic to rename the columns and add these tracking fields. Additionally, I successfully booted up the backend in the background to automatically migrate these changes into your active PostgreSQL database!
Here is exactly what was updated:
1. **Renamed GPS Columns**: `latitude` and `longitude` were renamed strictly to `surveylat` and `surveylong` across the database columns and the JSON payload schemas.
2. **Registration Origin Tracking**: Added a `registration_source` string field (so you can pass "mobile" or "web").
3. **Creator Identifier**: Added a `registered_by_id` (Integer) field to uniquely track the specific logged-in user who submitted the survey payload.
### Your Updated POST Payload Schema
Your `POST /api/v1/crm/clients` payload originating from both your Mobile Flutter App and your Web App should now look exactly like this:
```json
{
"first_name": "John",
"last_name": "Doe",
"email": "john.doe@example.com",
"password": "securepassword123",
"phone": "+1234567890",
"address": "123 Main St, Tech City",
"survey_lat": 37.7749,
"survey_long": -122.4194,
"pincode": "94103",
"registration_source": "mobile",
"registered_by_id": 42
}
```
Whenever you retrieve a client record using `GET /api/v1/crm/clients/:id`, it will return these exact fields so you'll always know the survey's origin platform, the exact location it was plotted, and the user who processed it!
To specify whether the survey was taken on **mobile** or **web**, you just need to hardcode the `registration_source` identifier directly into the API request code of your respective frontends.
This is the most reliable way to handle it, because your mobile codebase uniquely knows it's the mobile app, and your web codebase uniquely knows it's the web app.
Here is exactly how you handle it in both environments:
### 1. In your Flutter (Mobile) App
When you build the JSON body to send your POST request via the `http` package or `dio`, you simply inject `"mobile"` and grab the current logged-in user's ID from your local state:
```dart
// Inside your Flutter API service
Future<void> submitSurvey(String firstName, double lat, double lon, int currentUserId) async {
final payload = {
"first_name": firstName,
// ... other fields
"survey_lat": lat,
"survey_long": lon,
// Explicitly tag this as coming from the mobile app
"registration_source": "mobile",
"registered_by_id": currentUserId
};
await http.post(
Uri.parse('https://api.doormile.com/api/v1/crm/clients'),
headers: {"Content-Type": "application/json"},
body: jsonEncode(payload),
);
}
```
### 2. In your React / Web App
Similarly, inside your Web frontend's API submission function, you explicitly hardcode `"web"` as the source:
```javascript
// Inside your Web frontend API utility
async function submitSurvey(formData, lat, lon, currentUserId) {
const payload = {
first_name: formData.firstName,
// ... other fields
survey_lat: lat,
survey_long: lon,
// Explicitly tag this as coming from the web dashboard
registration_source: "web",
registered_by_id: currentUserId
};
await fetch('https://api.doormile.com/api/v1/crm/clients', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
}
```
### Alternative Option: Backend "User-Agent" Detection
If you don't want to pass it manually in the JSON payload, we can modify the Golang backend to automatically detect the platform by reading the HTTP Headers.
When a request hits your API, Fiber can check the `User-Agent` header (Flutter usually sends `Dart/<version> (dart:io)`, while Web sends standard browser data like `Mozilla/5.0...`). Let me know if you would prefer the backend to automate this detection instead!

BIN
assets/_icon_preview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

BIN
assets/doormile_d_only.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

BIN
assets/doormile_fg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/preloader.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
assets/test_merge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

2551
buildlog.txt Normal file

File diff suppressed because one or more lines are too long

34
ios/.gitignore vendored Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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>

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 943 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.

View File

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

View File

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

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

@@ -0,0 +1,51 @@
<?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>Doormile CRM</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>Doormile CRM</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>NSLocationWhenInUseUsageDescription</key>
<string>DoorMile CRM records your GPS location when a survey is submitted so the field visit can be logged accurately.</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

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

View File

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

291
lib/app.dart Normal file
View File

@@ -0,0 +1,291 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'models/client.dart';
import 'models/activity.dart';
import 'models/app_stats.dart';
import 'screens/dashboard_screen.dart';
import 'screens/clients_screen.dart';
import 'screens/survey_screen.dart';
import 'screens/surveys_list_screen.dart';
import 'screens/pricing_screen.dart';
import 'services/crm_api_service.dart';
import 'services/auth_service.dart';
class AppShell extends StatefulWidget {
final AuthUser currentUser;
final VoidCallback onLogout;
const AppShell({
super.key,
required this.currentUser,
required this.onLogout,
});
@override
State<AppShell> createState() => _AppShellState();
}
class _AppShellState extends State<AppShell> {
int _currentIndex = 0;
bool _backendReady = false;
List<Client> _clients = [];
List<Activity> _activities = [];
List<dynamic> _surveys = [];
List<dynamic> _pricing = [];
AppStats _stats = const AppStats(
totalClients: 0,
avgVolume: 0,
revenueOpportunity: 0,
todayEntries: 0,
pendingFollowups: 0,
);
final _api = CrmApiService();
Timer? _pollingTimer;
@override
void initState() {
super.initState();
_api.setToken(widget.currentUser.token);
_loadFromBackend();
_startPolling();
}
void _startPolling() {
_pollingTimer = Timer.periodic(const Duration(seconds: 10), (_) {
_loadFromBackend(silent: true);
});
}
@override
void dispose() {
_pollingTimer?.cancel();
super.dispose();
}
Future<void> _loadFromBackend({bool silent = false}) async {
try {
final results = await Future.wait([
_api.loadClients(),
_api.fetchSurveys(),
_api.fetchPricing(),
]);
final clients = results[0] as List<Client>;
final surveys = results[1];
final pricing = results[2];
if (!mounted) return;
setState(() {
_clients = clients;
_surveys = surveys;
_pricing = pricing;
_stats = _computeStats(clients);
_backendReady = true;
});
} catch (_) {
if (mounted && !silent) setState(() => _backendReady = false);
}
}
AppStats _computeStats(List<Client> clients) {
if (clients.isEmpty) {
return const AppStats(
totalClients: 0,
avgVolume: 0,
revenueOpportunity: 0,
todayEntries: 0,
pendingFollowups: 0,
);
}
final totalVolume = clients.fold(0.0, (acc, c) => acc + c.parcelVolume);
final avg = (totalVolume / clients.length).round();
return AppStats(
totalClients: clients.length,
avgVolume: avg,
revenueOpportunity: double.parse((totalVolume * 0.01).toStringAsFixed(2)),
todayEntries: 0,
pendingFollowups: clients.where((c) => c.dataConsent == DataConsent.basicOnly).length,
);
}
void _handleAddClient(Client incoming) async {
final newClient = incoming.copyWith(
recordedByName: widget.currentUser.name,
recordedByEmail: widget.currentUser.email,
recordedByRole: widget.currentUser.role,
);
// Optimistic UI update with a temp ID
setState(() {
_clients = [newClient, ..._clients];
_activities = [
Activity(
id: 'act_${DateTime.now().millisecondsSinceEpoch}',
clientName: newClient.name,
action: 'Survey submitted just now',
icon: ActivityIcon.shipping,
status: ActivityStatus.newStatus,
),
..._activities,
];
_stats = _computeStats(_clients);
});
// Persist to backend and swap temp ID with server-assigned ID
final saved = await _api.createClient(newClient);
if (saved != null && mounted) {
setState(() {
_clients = _clients
.map((c) => c.id == newClient.id ? saved : c)
.toList();
});
}
}
void _handleUpdateNotes(String clientId, String newNotes) async {
setState(() {
_clients = _clients.map((c) {
if (c.id == clientId) {
return c.copyWith(notes: newNotes, lastUpdated: 'Updated just now');
}
return c;
}).toList();
final updated = _clients.firstWhere((c) => c.id == clientId);
_activities = [
Activity(
id: 'act_${DateTime.now().millisecondsSinceEpoch}',
clientName: updated.name,
action: 'Profile updated by Rep',
icon: ActivityIcon.edit,
),
..._activities,
];
});
final updated = _clients.firstWhere((c) => c.id == clientId);
await _api.updateClient(updated);
}
void _handleCompleteClientProfile(
String clientId,
double parcelVolume,
int activeContracts,
String provider,
String notes,
) async {
String efficiency = 'High Efficiency';
if (provider == 'Blue Dart Express') efficiency = 'Premium Service';
if (provider == 'Gati-KWE') efficiency = 'Cost Leader';
if (provider == 'Delhivery') efficiency = 'Local Last Mile';
setState(() {
_clients = _clients.map((c) {
if (c.id != clientId) return c;
return c.copyWith(
parcelVolume: parcelVolume,
activeContracts: activeContracts,
provider: provider,
efficiency: efficiency,
notes: notes,
dataConsent: DataConsent.full,
lastUpdated: 'Just now',
surveySubmitted: true,
recordedByName: widget.currentUser.name,
recordedByEmail: widget.currentUser.email,
recordedByRole: widget.currentUser.role,
);
}).toList();
_stats = _computeStats(_clients);
});
final updated = _clients.firstWhere((c) => c.id == clientId);
await _api.updateClient(updated);
}
void _navigateTo(int index) {
if (index == 4) {
Navigator.push(context, MaterialPageRoute(
builder: (ctx) => Scaffold(
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.black87),
title: const Text('Add Client', style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold)),
),
body: SurveyScreen(
clients: _clients,
onAddClient: _handleAddClient,
onNavigate: (i) {
Navigator.pop(ctx);
_navigateTo(i);
},
backendReady: _backendReady,
isActive: true,
),
),
));
return;
}
setState(() => _currentIndex = index);
}
@override
Widget build(BuildContext context) {
final screens = [
DashboardScreen(
stats: _stats,
activities: _activities,
clients: _clients,
surveys: _surveys,
pricing: _pricing,
onNavigate: _navigateTo,
onDataChanged: _loadFromBackend,
backendReady: _backendReady,
onLogout: widget.onLogout,
isActive: _currentIndex == 0,
),
ClientsScreen(
clients: _clients,
onUpdateClientNotes: _handleUpdateNotes,
onCompleteClientProfile: _handleCompleteClientProfile,
onNavigate: _navigateTo,
backendReady: _backendReady,
isActive: _currentIndex == 1,
),
SurveysListScreen(
surveys: _surveys,
onDataChanged: _loadFromBackend,
isActive: _currentIndex == 2,
),
PricingScreen(
pricing: _pricing,
backendReady: _backendReady,
onDataChanged: _loadFromBackend,
isActive: _currentIndex == 3,
),
];
return Scaffold(
body: SafeArea(
child: IndexedStack(
index: _currentIndex,
children: screens,
),
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: _navigateTo,
height: 68,
destinations: const [
NavigationDestination(icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), label: 'Home'),
NavigationDestination(icon: Icon(Icons.people_outline), selectedIcon: Icon(Icons.people), label: 'Clients'),
NavigationDestination(icon: Icon(Icons.storefront_outlined), selectedIcon: Icon(Icons.storefront), label: 'Surveys'),
NavigationDestination(icon: Icon(Icons.local_shipping_outlined), selectedIcon: Icon(Icons.local_shipping), label: 'Pricing'),
],
),
);
}
}

166
lib/data/india_cities.dart Normal file
View File

@@ -0,0 +1,166 @@
/// A selectable Indian city shown flight-booking style: "Chennai (MAA)".
class IndiaCity {
final String name;
final String code; // airport-style 3-letter code
final String state;
const IndiaCity(this.name, this.code, this.state);
String get label => '$name ($code)';
/// True if [query] matches the city name, code, or state.
bool matches(String query) {
final q = query.trim().toLowerCase();
if (q.isEmpty) return true;
return name.toLowerCase().contains(q) ||
code.toLowerCase().contains(q) ||
state.toLowerCase().contains(q);
}
}
/// Major Indian cities with flight-style codes. Tamil Nadu listed first
/// (primary operating region), then the rest of India.
const List<IndiaCity> indiaCities = [
// ── Tamil Nadu ──────────────────────────────────────────────────────────
IndiaCity('Chennai', 'MAA', 'Tamil Nadu'),
IndiaCity('Coimbatore', 'CJB', 'Tamil Nadu'),
IndiaCity('Madurai', 'IXM', 'Tamil Nadu'),
IndiaCity('Tiruchirappalli', 'TRZ', 'Tamil Nadu'),
IndiaCity('Salem', 'SXV', 'Tamil Nadu'),
IndiaCity('Tuticorin', 'TCR', 'Tamil Nadu'),
IndiaCity('Tirupur', 'TUP', 'Tamil Nadu'),
IndiaCity('Erode', 'ERD', 'Tamil Nadu'),
IndiaCity('Vellore', 'VLR', 'Tamil Nadu'),
IndiaCity('Tirunelveli', 'TEN', 'Tamil Nadu'),
IndiaCity('Thanjavur', 'TNJ', 'Tamil Nadu'),
IndiaCity('Dindigul', 'DGL', 'Tamil Nadu'),
IndiaCity('Hosur', 'HSR', 'Tamil Nadu'),
IndiaCity('Nagercoil', 'NGL', 'Tamil Nadu'),
IndiaCity('Karur', 'KRR', 'Tamil Nadu'),
IndiaCity('Namakkal', 'NMK', 'Tamil Nadu'),
IndiaCity('Kanchipuram', 'KCP', 'Tamil Nadu'),
IndiaCity('Cuddalore', 'CDL', 'Tamil Nadu'),
IndiaCity('Thoothukudi', 'TOO', 'Tamil Nadu'),
// ── Karnataka ───────────────────────────────────────────────────────────
IndiaCity('Bengaluru', 'BLR', 'Karnataka'),
IndiaCity('Mysuru', 'MYQ', 'Karnataka'),
IndiaCity('Mangaluru', 'IXE', 'Karnataka'),
IndiaCity('Hubli', 'HBX', 'Karnataka'),
IndiaCity('Belagavi', 'IXG', 'Karnataka'),
IndiaCity('Kalaburagi', 'GBI', 'Karnataka'),
IndiaCity('Davangere', 'DVG', 'Karnataka'),
IndiaCity('Ballari', 'BEP', 'Karnataka'),
// ── Kerala ──────────────────────────────────────────────────────────────
IndiaCity('Kochi', 'COK', 'Kerala'),
IndiaCity('Thiruvananthapuram', 'TRV', 'Kerala'),
IndiaCity('Kozhikode', 'CCJ', 'Kerala'),
IndiaCity('Kannur', 'CNN', 'Kerala'),
IndiaCity('Thrissur', 'TCR2', 'Kerala'),
IndiaCity('Kollam', 'QLN', 'Kerala'),
// ── Telangana & Andhra Pradesh ──────────────────────────────────────────
IndiaCity('Hyderabad', 'HYD', 'Telangana'),
IndiaCity('Warangal', 'WGC', 'Telangana'),
IndiaCity('Visakhapatnam', 'VTZ', 'Andhra Pradesh'),
IndiaCity('Vijayawada', 'VGA', 'Andhra Pradesh'),
IndiaCity('Tirupati', 'TIR', 'Andhra Pradesh'),
IndiaCity('Guntur', 'GNT', 'Andhra Pradesh'),
IndiaCity('Rajahmundry', 'RJA', 'Andhra Pradesh'),
IndiaCity('Nellore', 'NLR', 'Andhra Pradesh'),
// ── Maharashtra ─────────────────────────────────────────────────────────
IndiaCity('Mumbai', 'BOM', 'Maharashtra'),
IndiaCity('Pune', 'PNQ', 'Maharashtra'),
IndiaCity('Nagpur', 'NAG', 'Maharashtra'),
IndiaCity('Nashik', 'ISK', 'Maharashtra'),
IndiaCity('Aurangabad', 'IXU', 'Maharashtra'),
IndiaCity('Kolhapur', 'KLH', 'Maharashtra'),
IndiaCity('Solapur', 'SSE', 'Maharashtra'),
// ── Delhi NCR & North ───────────────────────────────────────────────────
IndiaCity('New Delhi', 'DEL', 'Delhi'),
IndiaCity('Gurugram', 'GGN', 'Haryana'),
IndiaCity('Noida', 'NOI', 'Uttar Pradesh'),
IndiaCity('Faridabad', 'FBD', 'Haryana'),
IndiaCity('Chandigarh', 'IXC', 'Chandigarh'),
IndiaCity('Amritsar', 'ATQ', 'Punjab'),
IndiaCity('Ludhiana', 'LUH', 'Punjab'),
IndiaCity('Jalandhar', 'JLR2', 'Punjab'),
// ── Gujarat ─────────────────────────────────────────────────────────────
IndiaCity('Ahmedabad', 'AMD', 'Gujarat'),
IndiaCity('Surat', 'STV', 'Gujarat'),
IndiaCity('Vadodara', 'BDQ', 'Gujarat'),
IndiaCity('Rajkot', 'RAJ', 'Gujarat'),
IndiaCity('Bhavnagar', 'BHU', 'Gujarat'),
IndiaCity('Jamnagar', 'JGA', 'Gujarat'),
// ── Rajasthan ───────────────────────────────────────────────────────────
IndiaCity('Jaipur', 'JAI', 'Rajasthan'),
IndiaCity('Jodhpur', 'JDH', 'Rajasthan'),
IndiaCity('Udaipur', 'UDR', 'Rajasthan'),
IndiaCity('Kota', 'KTU', 'Rajasthan'),
IndiaCity('Bikaner', 'BKB', 'Rajasthan'),
// ── Uttar Pradesh & Central ─────────────────────────────────────────────
IndiaCity('Lucknow', 'LKO', 'Uttar Pradesh'),
IndiaCity('Kanpur', 'KNU', 'Uttar Pradesh'),
IndiaCity('Varanasi', 'VNS', 'Uttar Pradesh'),
IndiaCity('Agra', 'AGR', 'Uttar Pradesh'),
IndiaCity('Prayagraj', 'IXD', 'Uttar Pradesh'),
IndiaCity('Gorakhpur', 'GOP', 'Uttar Pradesh'),
IndiaCity('Bhopal', 'BHO', 'Madhya Pradesh'),
IndiaCity('Indore', 'IDR', 'Madhya Pradesh'),
IndiaCity('Gwalior', 'GWL', 'Madhya Pradesh'),
IndiaCity('Jabalpur', 'JLR', 'Madhya Pradesh'),
IndiaCity('Raipur', 'RPR', 'Chhattisgarh'),
// ── East & North-East ───────────────────────────────────────────────────
IndiaCity('Kolkata', 'CCU', 'West Bengal'),
IndiaCity('Siliguri', 'IXB', 'West Bengal'),
IndiaCity('Durgapur', 'RDP', 'West Bengal'),
IndiaCity('Patna', 'PAT', 'Bihar'),
IndiaCity('Gaya', 'GAY', 'Bihar'),
IndiaCity('Ranchi', 'IXR', 'Jharkhand'),
IndiaCity('Jamshedpur', 'JSD', 'Jharkhand'),
IndiaCity('Bhubaneswar', 'BBI', 'Odisha'),
IndiaCity('Cuttack', 'CTC', 'Odisha'),
IndiaCity('Guwahati', 'GAU', 'Assam'),
IndiaCity('Dibrugarh', 'DIB', 'Assam'),
IndiaCity('Agartala', 'IXA', 'Tripura'),
IndiaCity('Imphal', 'IMF', 'Manipur'),
IndiaCity('Shillong', 'SHL', 'Meghalaya'),
IndiaCity('Aizawl', 'AJL', 'Mizoram'),
IndiaCity('Dimapur', 'DMU', 'Nagaland'),
// ── Hills, UTs & others ─────────────────────────────────────────────────
IndiaCity('Dehradun', 'DED', 'Uttarakhand'),
IndiaCity('Haridwar', 'HDW', 'Uttarakhand'),
IndiaCity('Shimla', 'SLV', 'Himachal Pradesh'),
IndiaCity('Srinagar', 'SXR', 'Jammu & Kashmir'),
IndiaCity('Jammu', 'IXJ', 'Jammu & Kashmir'),
IndiaCity('Leh', 'IXL', 'Ladakh'),
IndiaCity('Panaji', 'GOI', 'Goa'),
IndiaCity('Puducherry', 'PNY', 'Puducherry'),
IndiaCity('Port Blair', 'IXZ', 'Andaman & Nicobar Islands'),
];
/// Finds a city by (loose) name match — used to map a GPS reverse-geocode
/// result to a known city. Returns null if nothing reasonable matches.
IndiaCity? findCityByName(String? name) {
if (name == null || name.trim().isEmpty) return null;
final n = name.trim().toLowerCase();
for (final c in indiaCities) {
if (c.name.toLowerCase() == n) return c;
}
// Loose contains match (e.g. "Coimbatore District" → "Coimbatore")
for (final c in indiaCities) {
if (n.contains(c.name.toLowerCase()) ||
c.name.toLowerCase().contains(n)) {
return c;
}
}
return null;
}

View File

@@ -0,0 +1,221 @@
/// All Indian states + Union Territories, alphabetically sorted.
const List<String> indianStates = [
'Andaman & Nicobar Islands',
'Andhra Pradesh',
'Arunachal Pradesh',
'Assam',
'Bihar',
'Chandigarh',
'Chhattisgarh',
'Dadra & Nagar Haveli',
'Daman & Diu',
'Delhi',
'Goa',
'Gujarat',
'Haryana',
'Himachal Pradesh',
'Jammu & Kashmir',
'Jharkhand',
'Karnataka',
'Kerala',
'Ladakh',
'Lakshadweep',
'Madhya Pradesh',
'Maharashtra',
'Manipur',
'Meghalaya',
'Mizoram',
'Nagaland',
'Odisha',
'Puducherry',
'Punjab',
'Rajasthan',
'Sikkim',
'Tamil Nadu',
'Telangana',
'Tripura',
'Uttar Pradesh',
'Uttarakhand',
'West Bengal',
];
/// Major cities per state. Falls back to an empty list for states not listed.
const Map<String, List<String>> stateCities = {
'Tamil Nadu': [
'Chennai', 'Coimbatore', 'Madurai', 'Salem', 'Tirupur', 'Erode',
'Tiruchirappalli', 'Tirunelveli', 'Vellore', 'Dindigul', 'Thanjavur',
'Kanchipuram', 'Namakkal', 'Karur', 'Dharmapuri', 'Krishnagiri',
'Cuddalore', 'Villupuram', 'Nagapattinam', 'Thoothukudi', 'Virudhunagar',
'Sivaganga', 'Ramanathapuram', 'Theni', 'Perambalur', 'Puducherry',
],
'Karnataka': [
'Bengaluru', 'Mysuru', 'Mangaluru', 'Hubli', 'Belagavi',
'Davangere', 'Shivamogga', 'Tumakuru', 'Kalaburagi', 'Udupi',
'Ballari', 'Bidar', 'Vijayapura', 'Raichur', 'Dharwad', 'Hassan',
],
'Kerala': [
'Kochi', 'Thiruvananthapuram', 'Kozhikode', 'Thrissur', 'Kannur',
'Kollam', 'Palakkad', 'Malappuram', 'Alappuzha', 'Kottayam',
'Idukki', 'Wayanad', 'Kasaragod', 'Pathanamthitta',
],
'Andhra Pradesh': [
'Vijayawada', 'Visakhapatnam', 'Tirupati', 'Guntur', 'Nellore',
'Kakinada', 'Rajahmundry', 'Kadapa', 'Kurnool', 'Ongole',
'Anantapur', 'Vizianagaram', 'Eluru',
],
'Telangana': [
'Hyderabad', 'Warangal', 'Nizamabad', 'Karimnagar', 'Khammam',
'Mahbubnagar', 'Nalgonda', 'Adilabad', 'Secunderabad', 'Siddipet',
],
'Maharashtra': [
'Mumbai', 'Pune', 'Nagpur', 'Nashik', 'Aurangabad', 'Thane',
'Solapur', 'Amravati', 'Kolhapur', 'Nanded', 'Sangli', 'Jalgaon',
'Akola', 'Latur', 'Chandrapur',
],
'Delhi': [
'New Delhi', 'North Delhi', 'South Delhi', 'East Delhi', 'West Delhi',
'Noida', 'Gurgaon', 'Faridabad', 'Ghaziabad', 'Dwarka',
],
'Gujarat': [
'Ahmedabad', 'Surat', 'Vadodara', 'Rajkot', 'Gandhinagar',
'Bhavnagar', 'Jamnagar', 'Junagadh', 'Anand', 'Bharuch',
'Morbi', 'Nadiad', 'Surendranagar',
],
'Rajasthan': [
'Jaipur', 'Jodhpur', 'Udaipur', 'Kota', 'Ajmer',
'Bikaner', 'Alwar', 'Bharatpur', 'Sikar', 'Sri Ganganagar',
'Pali', 'Tonk', 'Barmer',
],
'West Bengal': [
'Kolkata', 'Howrah', 'Durgapur', 'Asansol', 'Siliguri',
'Bardhaman', 'Malda', 'Kharagpur', 'Haldia', 'Jalpaiguri',
],
'Uttar Pradesh': [
'Lucknow', 'Kanpur', 'Varanasi', 'Agra', 'Meerut',
'Prayagraj', 'Bareilly', 'Moradabad', 'Gorakhpur', 'Aligarh',
'Noida', 'Ghaziabad', 'Firozabad', 'Saharanpur', 'Mathura',
],
'Madhya Pradesh': [
'Bhopal', 'Indore', 'Gwalior', 'Jabalpur', 'Ujjain',
'Sagar', 'Dewas', 'Satna', 'Ratlam', 'Rewa', 'Singrauli',
],
'Punjab': [
'Ludhiana', 'Amritsar', 'Jalandhar', 'Patiala', 'Bathinda',
'Mohali', 'Hoshiarpur', 'Gurdaspur', 'Pathankot', 'Firozpur',
],
'Haryana': [
'Gurugram', 'Faridabad', 'Hisar', 'Panipat', 'Ambala',
'Karnal', 'Rohtak', 'Bhiwani', 'Sirsa', 'Sonipat',
],
'Bihar': [
'Patna', 'Gaya', 'Bhagalpur', 'Muzaffarpur', 'Purnia',
'Darbhanga', 'Arrah', 'Begusarai', 'Munger', 'Chhapra',
],
'Odisha': [
'Bhubaneswar', 'Cuttack', 'Rourkela', 'Brahmapur', 'Puri',
'Sambalpur', 'Balasore', 'Baripada', 'Jharsuguda',
],
'Assam': [
'Guwahati', 'Silchar', 'Dibrugarh', 'Jorhat', 'Nagaon',
'Tinsukia', 'Tezpur', 'Bongaigaon', 'Karimganj',
],
'Jharkhand': [
'Ranchi', 'Jamshedpur', 'Dhanbad', 'Bokaro', 'Deoghar',
'Hazaribagh', 'Giridih', 'Dumka', 'Phusro',
],
'Chhattisgarh': [
'Raipur', 'Bhilai', 'Bilaspur', 'Korba', 'Rajnandgaon',
'Durg', 'Jagdalpur', 'Ambikapur', 'Raigarh',
],
'Uttarakhand': [
'Dehradun', 'Haridwar', 'Rishikesh', 'Roorkee', 'Haldwani',
'Nainital', 'Mussoorie', 'Kashipur', 'Rudrapur',
],
'Himachal Pradesh': [
'Shimla', 'Dharamshala', 'Solan', 'Mandi', 'Kullu',
'Manali', 'Baddi', 'Bilaspur', 'Kangra',
],
'Goa': [
'Panaji', 'Margao', 'Vasco da Gama', 'Mapusa', 'Ponda', 'Calangute',
],
'Puducherry': ['Puducherry', 'Karaikal', 'Mahe', 'Yanam'],
'Jammu & Kashmir': [
'Srinagar', 'Jammu', 'Anantnag', 'Baramulla', 'Sopore', 'Udhampur',
],
'Chandigarh': ['Chandigarh'],
'Ladakh': ['Leh', 'Kargil'],
'Andaman & Nicobar Islands': ['Port Blair'],
'Manipur': ['Imphal', 'Thoubal', 'Bishnupur', 'Churachandpur'],
'Meghalaya': ['Shillong', 'Tura', 'Jowai'],
'Mizoram': ['Aizawl', 'Lunglei', 'Champhai'],
'Nagaland': ['Kohima', 'Dimapur', 'Mokokchung'],
'Arunachal Pradesh': ['Itanagar', 'Naharlagun', 'Pasighat', 'Tawang'],
'Sikkim': ['Gangtok', 'Namchi', 'Gyalshing'],
'Tripura': ['Agartala', 'Dharmanagar', 'Udaipur', 'Kailasahar'],
};
const Map<String, List<String>> categorisedProviders = {
'National & Pan-India Courier Services (Parcels & Express)': [
'Blue Dart',
'Delhivery',
'DTDC',
'India Post / Speed Post',
'The Professional Couriers',
'XpressBees',
'Ecom Express',
'Shadowfax',
],
'National Heavyweight Cargo & B2B Surface Freight': [
'Safexpress',
'VRL Logistics',
'TCI (Transport Corporation of India)',
'Om Logistics',
'Best Roadways',
],
'Southern India Dual-Purpose & Regional Networks (Tamil Nadu, Telangana, AP, Karnataka)': [
'MSS (Mettur Super Services / Mettur Transports)',
'ABT Travels & Logistics',
'Navata Road Transport',
'KRS (Kerala Roadways)',
'Parveen Travels / Parveen Express',
'SRM Transports',
'KPN Travels & KPN Speed Parcel',
'SRS Travels',
],
'Hyper-Local & District Operators in Tamil Nadu': [
'Hindusthan Travels',
'City Travels',
'Essaar Travels',
'No. 1 Air Travels',
'A1 Travels',
'Krish Travels',
'Hebron Transports',
'Horma Travels',
'Vivegam Travels',
'PSS Transport',
'SRT (Renugambal Travels)',
'Thamarai Bus Transports',
'Rathimeena Travels',
'Ganesh Travels',
'John Kennedy Bus Service',
'Arun Travel',
'Saaji Meera Roadways',
'ARC Parcel Service',
'Chakra Travels & Parcel Service',
'MVA Parcel And Bus Service',
],
'Northern & Central India Regional Operators (Delhi NCR, Rajasthan, UP, MP, Punjab, Haryana)': [
'Shrinath Travels & Cargo',
'Hans Travels',
'Zingbus',
'Kalpana Travels / City Land Travels',
'Trackon Couriers',
'North India Transways',
'RSRTC Cargo',
'UPSRTC Cargo',
],
};
final List<String> allProvidersFlat =
categorisedProviders.values.expand((list) => list).toList();

View File

@@ -0,0 +1,46 @@
import '../models/client.dart';
import '../models/activity.dart';
import '../models/app_stats.dart';
const AppStats initialStats = AppStats(
totalClients: 1,
avgVolume: 200,
revenueOpportunity: 0.1,
todayEntries: 1,
pendingFollowups: 0,
);
const List<Client> initialClients = [
Client(
id: 'suriya_001',
name: 'Suriya',
city: 'Coimbatore',
businessType: BusinessType.ecommerce,
parcelVolume: 200,
activeContracts: 5,
provider: 'DTDC Express',
efficiency: 'High Efficiency',
status: ClientStatus.newClient,
lastUpdated: 'Just now',
surveySubmitted: true,
notes: '',
dataConsent: DataConsent.full,
phone: '',
frequency: 'Weekly',
businessState: 'Tamil Nadu',
transitType: 'within_state',
transitFrom: 'Coimbatore',
transitTo: 'Chennai',
neighbourhood: 'RS Puram',
),
];
const List<Activity> initialActivities = [
Activity(
id: 'a1',
clientName: 'Suriya',
action: 'Profile created — staging entry',
icon: ActivityIcon.shipping,
status: ActivityStatus.newStatus,
),
];

130
lib/main.dart Normal file
View File

@@ -0,0 +1,130 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'app.dart';
import 'screens/login_screen.dart';
import 'screens/update_screen.dart';
import 'services/auth_service.dart';
import 'package:in_app_update/in_app_update.dart';
import 'dart:io';
import 'theme/app_theme.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.dark,
),
);
AuthUser? initialUser;
try {
initialUser = await AuthService().getSavedUser();
} catch (_) {}
runApp(DoorMileApp(initialUser: initialUser));
}
class DoorMileApp extends StatelessWidget {
final AuthUser? initialUser;
const DoorMileApp({super.key, this.initialUser});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Doormile CRM',
debugShowCheckedModeBanner: false,
theme: buildAppTheme(),
home: AuthGate(initialUser: initialUser),
builder: (context, child) {
final mediaQuery = MediaQuery.of(context);
return Container(
color: AppColors.scaffold,
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 500),
child: MediaQuery(
data: mediaQuery.copyWith(
textScaler: mediaQuery.textScaler.clamp(
minScaleFactor: 0.85,
maxScaleFactor: 1.15,
),
),
child: child!,
),
),
),
);
},
);
}
}
class AuthGate extends StatefulWidget {
final AuthUser? initialUser;
const AuthGate({super.key, this.initialUser});
@override
State<AuthGate> createState() => _AuthGateState();
}
class _AuthGateState extends State<AuthGate> {
AuthUser? _user;
bool _updateRequired = false;
@override
void initState() {
super.initState();
_user = widget.initialUser;
_checkForUpdates();
}
Future<void> _checkForUpdates() async {
try {
if (Platform.isAndroid) {
final info = await InAppUpdate.checkForUpdate();
if (info.updateAvailability == UpdateAvailability.updateAvailable) {
setState(() => _updateRequired = true);
await InAppUpdate.performImmediateUpdate();
}
}
} catch (e) {
// Ignored: Not a supported Android device or Play Store not accessible
}
}
Future<void> _login(AuthUser user) async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_user_data', jsonEncode(user.toJson()));
await prefs.setString('user_name', user.name);
} catch (_) {}
setState(() => _user = user);
}
Future<void> _logout() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.remove('auth_user_data');
await prefs.remove('user_name');
} catch (_) {}
setState(() => _user = null);
}
@override
Widget build(BuildContext context) {
if (_updateRequired) {
return const UpdateRequiredScreen();
}
if (_user == null) {
return LoginScreen(
onLoggedIn: _login,
);
}
return AppShell(currentUser: _user!, onLogout: _logout);
}
}

19
lib/models/activity.dart Normal file
View File

@@ -0,0 +1,19 @@
enum ActivityIcon { shipping, call, edit }
enum ActivityStatus { newStatus, pending, completed }
class Activity {
final String id;
final String clientName;
final String action;
final ActivityIcon icon;
final ActivityStatus? status;
const Activity({
required this.id,
required this.clientName,
required this.action,
required this.icon,
this.status,
});
}

31
lib/models/app_stats.dart Normal file
View File

@@ -0,0 +1,31 @@
class AppStats {
final int totalClients;
final int avgVolume;
final double revenueOpportunity;
final int todayEntries;
final int pendingFollowups;
const AppStats({
required this.totalClients,
required this.avgVolume,
required this.revenueOpportunity,
required this.todayEntries,
required this.pendingFollowups,
});
AppStats copyWith({
int? totalClients,
int? avgVolume,
double? revenueOpportunity,
int? todayEntries,
int? pendingFollowups,
}) {
return AppStats(
totalClients: totalClients ?? this.totalClients,
avgVolume: avgVolume ?? this.avgVolume,
revenueOpportunity: revenueOpportunity ?? this.revenueOpportunity,
todayEntries: todayEntries ?? this.todayEntries,
pendingFollowups: pendingFollowups ?? this.pendingFollowups,
);
}
}

173
lib/models/client.dart Normal file
View File

@@ -0,0 +1,173 @@
enum DataConsent { basicOnly, full }
enum BusinessType { ecommerce, manufacturing, wholesale, retail }
enum ClientStatus { newClient, pending, completed, updated }
extension BusinessTypeLabel on BusinessType {
String get label {
switch (this) {
case BusinessType.ecommerce:
return 'E-commerce';
case BusinessType.manufacturing:
return 'Manufacturing';
case BusinessType.wholesale:
return 'Wholesale';
case BusinessType.retail:
return 'Retail';
}
}
}
extension ClientStatusLabel on ClientStatus {
String get label {
switch (this) {
case ClientStatus.newClient:
return 'NEW';
case ClientStatus.pending:
return 'PENDING';
case ClientStatus.completed:
return 'COMPLETED';
case ClientStatus.updated:
return 'UPDATED';
}
}
}
class Client {
final String id;
final String name;
final String city; // free-text, e.g. "New York", "Austin"
final BusinessType businessType;
final double parcelVolume;
final int activeContracts;
final String provider;
final String efficiency;
final ClientStatus status;
final String lastUpdated;
final bool surveySubmitted;
final String notes;
final DataConsent dataConsent;
// Extended basic-info fields
final String phone; // contact number
final String frequency; // shipment frequency: Daily / Weekly / etc.
final String businessState; // Indian state of the business
final String transitType; // 'within_state' | 'state_to_state' | ''
final String transitFrom; // city (within_state) or state name (state_to_state)
final String transitTo; // city (within_state) or state name (state_to_state)
final String neighbourhood; // local area / neighbourhood
final String logisticsSegment; // 'First Mile' | 'Middle Mile' | 'Last Mile' | ''
final String pincode; // client postal/PIN code — optional manual entry
// Device GPS at the moment of survey submission — stored for field audit, not displayed in app UI.
final double surveyLat;
final double surveyLng;
final String surveyAddress; // reverse-geocoded full address
final String surveyZone; // suburb / neighbourhood from geocoder
final String surveyPincode; // postal code from GPS reverse-geocode
// Field-agent attribution — who collected this record (from login session).
final String recordedByName; // e.g. "Kamesh"
final String recordedByEmail; // login email of the agent
final String recordedByRole; // 'admin' | 'rep' | 'support'
const Client({
required this.id,
required this.name,
required this.city,
required this.businessType,
required this.parcelVolume,
required this.activeContracts,
required this.provider,
required this.efficiency,
required this.status,
required this.lastUpdated,
required this.surveySubmitted,
this.notes = '',
this.dataConsent = DataConsent.full,
this.phone = '',
this.frequency = '',
this.businessState = '',
this.transitType = '',
this.transitFrom = '',
this.transitTo = '',
this.neighbourhood = '',
this.logisticsSegment = '',
this.pincode = '',
this.surveyLat = 0.0,
this.surveyLng = 0.0,
this.surveyAddress = '',
this.surveyZone = '',
this.surveyPincode = '',
this.recordedByName = '',
this.recordedByEmail = '',
this.recordedByRole = '',
});
Client copyWith({
String? id,
String? name,
String? city,
BusinessType? businessType,
double? parcelVolume,
int? activeContracts,
String? provider,
String? efficiency,
ClientStatus? status,
String? lastUpdated,
bool? surveySubmitted,
String? notes,
DataConsent? dataConsent,
String? phone,
String? frequency,
String? businessState,
String? transitType,
String? transitFrom,
String? transitTo,
String? neighbourhood,
String? logisticsSegment,
String? pincode,
double? surveyLat,
double? surveyLng,
String? surveyAddress,
String? surveyZone,
String? surveyPincode,
String? recordedByName,
String? recordedByEmail,
String? recordedByRole,
}) {
return Client(
id: id ?? this.id,
name: name ?? this.name,
city: city ?? this.city,
businessType: businessType ?? this.businessType,
parcelVolume: parcelVolume ?? this.parcelVolume,
activeContracts: activeContracts ?? this.activeContracts,
provider: provider ?? this.provider,
efficiency: efficiency ?? this.efficiency,
status: status ?? this.status,
lastUpdated: lastUpdated ?? this.lastUpdated,
surveySubmitted: surveySubmitted ?? this.surveySubmitted,
notes: notes ?? this.notes,
dataConsent: dataConsent ?? this.dataConsent,
phone: phone ?? this.phone,
frequency: frequency ?? this.frequency,
businessState: businessState ?? this.businessState,
transitType: transitType ?? this.transitType,
transitFrom: transitFrom ?? this.transitFrom,
transitTo: transitTo ?? this.transitTo,
neighbourhood: neighbourhood ?? this.neighbourhood,
logisticsSegment: logisticsSegment ?? this.logisticsSegment,
pincode: pincode ?? this.pincode,
surveyLat: surveyLat ?? this.surveyLat,
surveyLng: surveyLng ?? this.surveyLng,
surveyAddress: surveyAddress ?? this.surveyAddress,
surveyZone: surveyZone ?? this.surveyZone,
surveyPincode: surveyPincode ?? this.surveyPincode,
recordedByName: recordedByName ?? this.recordedByName,
recordedByEmail: recordedByEmail ?? this.recordedByEmail,
recordedByRole: recordedByRole ?? this.recordedByRole,
);
}
}

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