Initial commit: Bharat ERP Flutter application.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Surendiran 2026-06-19 15:04:06 +05:30
commit 8f852ac4f6
243 changed files with 35223 additions and 0 deletions

3
.env.example Normal file
View File

@ -0,0 +1,3 @@
API_BASE_URL=https://demo.venbait.in/api/v1
API_TIMEOUT_SECONDS=30
DEV_BYPASS_AUTH=false

50
.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# 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
# Environment
.env
.env.*
!.env.example
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release

36
.metadata Normal file
View File

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

82
README.md Normal file
View File

@ -0,0 +1,82 @@
# Bharat ERP — AMS Phase 1
Production-ready **Asset Management System** for Web, Android, and iOS — the foundation for Bharat ERP.
## Phase 1 Modules
- **Authentication** — Login, logout, OTP, JWT sessions
- **Company Management** — Multi-company support
- **Branch Management** — Branch hierarchy per company
- **User Management** — Employee accounts and roles
- **Role & Permissions** — RBAC with dynamic menu
- **Dashboard** — KPIs and charts
- **Assets** — Categories, master, allocation, maintenance, disposal, QR
- **Reports** — Asset register, allocation, maintenance, warranty, disposal
- **Settings** — Theme, branding, preferences
## Tech Stack
Flutter · Riverpod · GoRouter · Dio · Freezed · Material 3 · Responsive Framework
## Getting Started
### Prerequisites
- Flutter SDK 3.9+
- Dart 3.9+
### Setup
```bash
# Clone and enter project
cd Bharat_ERP
# Environment files are already included:
# .env.development / .env.uat / .env.production
# Install dependencies
flutter pub get
# Generate Freezed/JSON models
dart run build_runner build --delete-conflicting-outputs
# Run with Development
flutter run -d chrome --dart-define=ENV=development
# Run with UAT
flutter run --dart-define=ENV=uat
# Run with Production
flutter run --dart-define=ENV=production
```
```bash
# Release builds
flutter build apk --release --dart-define=ENV=production
flutter build ios --release --dart-define=ENV=production
flutter build web --release --dart-define=ENV=production
```
## Project Structure
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for full architecture documentation.
```
lib/
├── core/ # Constants, network, theme, errors, utils
├── shared/ # Routes, widgets, global providers, shared models
└── modules/ # Feature modules (auth, company, assets, ...)
```
## Roles
| Role | Description |
|------|-------------|
| Super Admin | Full access across all companies |
| Company Admin | Full access within assigned company |
| Asset Manager | Asset operations within company |
| Employee | View assets and own allocations |
## License
Proprietary — Bharat ERP

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

View File

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

View File

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="bharat_erp"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<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.bharaterp.bharat_erp
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-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.9.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
include(":app")

0
assets/icons/.gitkeep Normal file
View File

0
assets/images/.gitkeep Normal file
View File

167
docs/ARCHITECTURE.md Normal file
View File

@ -0,0 +1,167 @@
# Bharat ERP — Architecture Guide
## Overview
Bharat ERP Phase 1 is an **Asset Management System (AMS)** built with Flutter for Web, Android, and iOS. The architecture is designed to be **ERP-ready** — new modules (Inventory, HR, Finance, etc.) can be plugged in without restructuring the foundation.
## Tech Stack
| Layer | Technology |
|-------|------------|
| Framework | Flutter 3.x |
| State Management | Riverpod |
| Routing | GoRouter |
| HTTP Client | Dio |
| Models | Freezed + JsonSerializable |
| UI | Material 3 + Responsive Framework |
| Local Storage | SharedPreferences + FlutterSecureStorage |
## Project Structure
```
lib/
├── main.dart # Entry point
├── app.dart # Root widget, theme, router
├── core/ # Cross-cutting concerns
│ ├── constants/ # API paths, app constants, enums
│ ├── errors/ # Failure types, exceptions
│ ├── network/ # Dio client, interceptors
│ ├── theme/ # Theme engine, branding
│ └── utils/ # Helpers, validators, extensions
├── shared/ # Shared across modules
│ ├── models/ # Common DTOs (pagination, API response)
│ ├── providers/ # Global providers (auth, theme, company)
│ ├── routes/ # GoRouter configuration
│ └── widgets/ # Reusable UI components
└── modules/ # Feature modules (Clean Architecture)
├── auth/
├── company/
├── branch/
├── users/
├── roles/
├── dashboard/
├── assets/
└── settings/
```
## Module Structure (Clean Architecture)
Each module follows the same internal layout:
```
modules/<module>/
├── data/
│ ├── datasources/ # Remote/local data sources
│ ├── models/ # DTOs (Freezed + JSON)
│ └── repositories/ # Repository implementations
├── domain/
│ ├── entities/ # Business entities
│ ├── repositories/ # Abstract repository contracts
│ └── usecases/ # Single-responsibility use cases
└── presentation/
├── providers/ # Riverpod providers & notifiers
├── screens/ # UI screens
└── widgets/ # Module-specific widgets
```
### Data Flow
```
Screen → Provider/Notifier → UseCase → Repository → DataSource → API
↑ ↓
└──────────── Entity / Failure ←───────┘
```
## Multi-Company Support
- Every API request includes `X-Company-Id` header (set by auth interceptor).
- Company context is stored in `CompanyProvider` after login.
- Branch context is optional via `X-Branch-Id` header.
- Super Admin can switch companies; other roles are scoped to their company.
## Role-Based Access Control (RBAC)
### Roles
| Role | Scope |
|------|-------|
| Super Admin | All companies, all modules |
| Company Admin | Single company, all modules |
| Asset Manager | Single company, asset operations |
| Employee | Single company, read + own allocations |
### Permissions
`create`, `read`, `update`, `delete`, `export`, `approve`
Permissions are checked via `PermissionGuard` widget and `hasPermission()` utility. The dynamic sidebar menu is built from the user's role permissions returned by the API.
## Authentication Flow
1. User submits credentials → `POST /auth/login`
2. API returns JWT access + refresh tokens
3. Tokens stored in FlutterSecureStorage
4. Dio interceptor attaches `Authorization: Bearer <token>`
5. On 401, refresh token flow is attempted
6. On refresh failure, user is redirected to login
## Theme Engine
- **Appearance modes**: Light, Dark, System
- **Company branding**: Logo, primary color, secondary color
- Theme persisted in SharedPreferences
- `AppTheme` generates Material 3 ColorScheme from branding colors
## Responsive Breakpoints
| Breakpoint | Width | Layout |
|------------|-------|--------|
| Mobile | < 600px | Bottom nav, single column |
| Tablet | 6001024px | Collapsible sidebar |
| Desktop | > 1024px | Persistent sidebar |
## API Conventions
- Base URL: configured via `.env`
- All responses wrapped in `ApiResponse<T>`
- Pagination via `PaginatedResponse<T>` with `page`, `limit`, `total`
- Errors return `{ "message": "...", "code": "..." }`
## Database Tables (Backend Reference)
```
companies, branches, users, roles, permissions, role_permissions,
asset_categories, assets, asset_allocations, asset_allocation_history,
asset_maintenance, asset_disposal, notifications, audit_logs
```
## Development Order
| Week | Focus |
|------|-------|
| 1 | Project setup, theme, auth, API layer |
| 2 | Company, branch, users, roles |
| 3 | Asset categories, master, allocation |
| 4 | Maintenance, disposal, QR |
| 5 | Dashboard, reports, audit, settings |
| 6 | Testing, optimization, deployment |
## Adding a New Module
1. Create `modules/<name>/` with data/domain/presentation layers
2. Add route constants in `core/constants/route_constants.dart`
3. Register routes in `shared/routes/app_router.dart`
4. Add menu item in `shared/widgets/app_shell.dart` with permission check
5. Create repository interface in domain, implementation in data
6. Wire providers in presentation layer
## Code Generation
```bash
dart run build_runner build --delete-conflicting-outputs
```
Generates `.freezed.dart` and `.g.dart` files for models.

34
ios/.gitignore vendored Executable 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>

2
ios/Flutter/Debug.xcconfig Executable file
View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

2
ios/Flutter/Release.xcconfig Executable file
View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"

43
ios/Podfile Executable file
View File

@ -0,0 +1,43 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
end
end

View File

@ -0,0 +1,619 @@
// !$*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)";
DEVELOPMENT_TEAM = ZLGZ4Z6CVJ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bharaterp.bharatErp;
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.bharaterp.bharatErp.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.bharaterp.bharatErp.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.bharaterp.bharatErp.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = ZLGZ4Z6CVJ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bharaterp.bharatErp;
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)";
DEVELOPMENT_TEAM = ZLGZ4Z6CVJ;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.bharaterp.bharatErp;
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>

13
ios/Runner/AppDelegate.swift Executable file
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: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

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

View File

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

View File

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

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

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Bharat Erp</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>bharat_erp</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>

View File

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

View File

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

36
lib/app.dart Normal file
View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:responsive_framework/responsive_framework.dart';
import 'core/constants/app_constants.dart';
import 'core/theme/theme_provider.dart';
import 'shared/routes/app_router.dart';
class BharatErpApp extends ConsumerWidget {
const BharatErpApp({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(routerProvider);
final themeMode = ref.watch(themeModeProvider);
final branding = ref.watch(brandingProvider);
return MaterialApp.router(
title: AppConstants.appName,
debugShowCheckedModeBanner: false,
theme: buildLightTheme(branding),
darkTheme: buildDarkTheme(branding),
themeMode: resolveThemeMode(themeMode),
routerConfig: router,
builder: (context, child) => ResponsiveBreakpoints.builder(
child: child!,
breakpoints: const [
Breakpoint(start: 0, end: 599, name: MOBILE),
Breakpoint(start: 600, end: 1023, name: TABLET),
Breakpoint(start: 1024, end: 1439, name: DESKTOP),
Breakpoint(start: 1440, end: double.infinity, name: '4K'),
],
),
);
}
}

View File

@ -0,0 +1,26 @@
import 'package:flutter/foundation.dart';
class AppEnv {
AppEnv._();
static const String development = 'development';
static const String uat = 'uat';
static const String production = 'production';
static String get current {
const env = String.fromEnvironment('APP_ENV', defaultValue: development);
return env;
}
static String get envFileName {
switch (current) {
case uat:
return '.env.uat';
case production:
return '.env.production';
case development:
default:
return kReleaseMode ? '.env.production' : '.env.development';
}
}
}

View File

@ -0,0 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
class DevConfig {
DevConfig._();
/// True when dev bypass is enabled via .env or running in debug mode.
static bool get bypassAuth {
final env = dotenv.env['DEV_BYPASS_AUTH'];
if (env != null) return env.toLowerCase() == 'true';
return kDebugMode;
}
/// UI preview without a working login API (screen gallery, explore mode).
static bool get screenPreviewEnabled {
if (bypassAuth) return true;
final env = dotenv.env['DEV_SCREEN_PREVIEW'];
if (env != null) return env.toLowerCase() == 'true';
return kDebugMode;
}
static const String demoEmail = 'admin@bharaterp.com';
static const String demoPassword = 'Admin@123';
}

View File

@ -0,0 +1,105 @@
class ApiEndpoints {
ApiEndpoints._();
// Auth
static const String login = '/auth/login';
static const String logout = '/auth/logout';
static const String refreshToken = '/auth/refresh';
static const String forgotPassword = '/auth/forgot-password';
static const String changePassword = '/auth/change-password';
static const String verifyOtp = '/auth/verify-otp';
static const String me = '/auth/me';
// Companies
static const String companies = '/companies';
static String companyById(String id) => '/companies/$id';
static String companySettings(String id) => '/companies/$id/settings';
// Branches
static const String branches = '/branches';
static String branchById(String id) => '/branches/$id';
// Users
static const String users = '/users';
static const String usersSummary = '/users/summary';
static const String usersFilters = '/users/filters';
static const String usersExport = '/users/export';
static String userById(String id) => '/users/$id';
static String deactivateUser(String id) => '/users/$id/deactivate';
// Roles & Permissions
static const String roles = '/roles';
static const String rolesPermissions = '/roles/permissions';
static const String rolesCards = '/roles/cards';
static String roleById(String id) => '/roles/$id';
static const String permissions = '/permissions';
static String rolePermissions(String roleId) => '/roles/$roleId/permissions';
static String rolePermissionMatrix(String roleId) =>
'/roles/$roleId/permission-matrix';
// Asset Categories
static const String assetCategories = '/masters/asset-categories';
static String assetCategoryById(String id) => '/masters/asset-categories/$id';
// Masters
static const String departments = '/masters/departments';
static String departmentById(String id) => '/masters/departments/$id';
static const String designations = '/masters/designations';
static String designationById(String id) => '/masters/designations/$id';
static const String plants = '/masters/plants';
static String plantById(String id) => '/masters/plants/$id';
static const String uom = '/masters/uom';
static String uomById(String id) => '/masters/uom/$id';
static const String warehouses = '/masters/warehouses';
static String warehouseById(String id) => '/masters/warehouses/$id';
// Assets
static const String assets = '/assets';
static String assetById(String id) => '/assets/$id';
static String assetQrCode(String id) => '/assets/$id/qr-code';
static const String assetSearch = '/assets/search';
// Asset Allocations
static const String assetAllocations = '/asset-allocations';
static String assetAllocationById(String id) => '/asset-allocations/$id';
static String returnAsset(String id) => '/asset-allocations/$id/return';
static String reassignAsset(String id) => '/asset-allocations/$id/reassign';
static String transferAsset(String id) => '/asset-allocations/$id/transfer';
static const String allocationHistory = '/asset-allocations/history';
// Asset Maintenance
static const String assetMaintenance = '/asset-maintenance';
static String maintenanceById(String id) => '/asset-maintenance/$id';
// Asset Disposal
static const String assetDisposal = '/asset-disposal';
static String disposalById(String id) => '/asset-disposal/$id';
static String approveDisposal(String id) => '/asset-disposal/$id/approve';
// Dashboard
static const String dashboardKpis = '/dashboard/kpis';
static const String dashboardCharts = '/dashboard/charts';
// Reports
static const String reportAssetRegister = '/reports/asset-register';
static const String reportAllocation = '/reports/allocation';
static const String reportMaintenance = '/reports/maintenance';
static const String reportWarranty = '/reports/warranty';
static const String reportDisposal = '/reports/disposal';
// Settings
static const String settings = '/settings';
static const String settingsBranding = '/settings/branding';
static const String settingsGeneral = '/settings/general';
static const String settingsCompany = '/settings/company-profile';
static const String settingsAsset = '/settings/asset';
static const String settingsNotifications = '/settings/notifications';
static const String settingsEmail = '/settings/email';
static const String settingsSecurity = '/settings/security';
// Audit
static const String auditLogs = '/audit-logs';
// Notifications
static const String notifications = '/notifications';
}

View File

@ -0,0 +1,13 @@
class AppConstants {
AppConstants._();
static const String appName = 'Bharat ERP';
static const String appVersion = '1.0.0';
static const String appTagline = 'Asset Management System';
static const int defaultPageSize = 20;
static const int maxPageSize = 100;
static const Duration animationDuration = Duration(milliseconds: 300);
static const Duration snackBarDuration = Duration(seconds: 3);
}

View File

@ -0,0 +1,194 @@
enum NavigationLayout {
sidebar('sidebar', 'Side Menu'),
top('top', 'Top Menu');
const NavigationLayout(this.value, this.label);
final String value;
final String label;
static NavigationLayout fromValue(String value) {
return NavigationLayout.values.firstWhere(
(layout) => layout.value == value,
orElse: () => NavigationLayout.sidebar,
);
}
}
enum UserRole {
superAdmin('super_admin', 'Super Admin'),
companyAdmin('company_admin', 'Company Admin'),
assetManager('asset_manager', 'Asset Manager'),
employee('employee', 'Employee');
const UserRole(this.value, this.label);
final String value;
final String label;
static UserRole fromValue(String value) {
final trimmed = value.trim();
final normalized = trimmed.toLowerCase().replaceAll(RegExp(r'[\s-]+'), '_');
for (final role in UserRole.values) {
if (role.value == normalized ||
role.value == trimmed ||
role.label.toLowerCase() == trimmed.toLowerCase()) {
return role;
}
}
if (normalized.contains('super') && normalized.contains('admin')) {
return UserRole.superAdmin;
}
if (normalized.contains('company') && normalized.contains('admin')) {
return UserRole.companyAdmin;
}
if (normalized.contains('asset') && normalized.contains('manager')) {
return UserRole.assetManager;
}
return UserRole.employee;
}
}
enum PermissionAction {
create('create'),
read('read'),
update('update'),
delete('delete'),
export('export'),
approve('approve');
const PermissionAction(this.value);
final String value;
}
enum EntityStatus {
active('active', 'Active'),
inactive('inactive', 'Inactive');
const EntityStatus(this.value, this.label);
final String value;
final String label;
static EntityStatus fromValue(String value) {
return EntityStatus.values.firstWhere(
(s) => s.value == value,
orElse: () => EntityStatus.active,
);
}
}
enum AssetStatus {
available('available', 'Available'),
allocated('allocated', 'Allocated'),
maintenance('maintenance', 'Maintenance'),
lost('lost', 'Lost'),
disposed('disposed', 'Disposed'),
retired('retired', 'Retired');
const AssetStatus(this.value, this.label);
final String value;
final String label;
static AssetStatus fromValue(String value) {
return AssetStatus.values.firstWhere(
(s) => s.value == value,
orElse: () => AssetStatus.available,
);
}
}
enum MaintenanceStatus {
open('open', 'Open'),
inProgress('in_progress', 'In Progress'),
completed('completed', 'Completed'),
cancelled('cancelled', 'Cancelled');
const MaintenanceStatus(this.value, this.label);
final String value;
final String label;
static MaintenanceStatus fromValue(String value) {
return MaintenanceStatus.values.firstWhere(
(s) => s.value == value,
orElse: () => MaintenanceStatus.open,
);
}
}
enum DisposalStatus {
pending('pending', 'Pending'),
approved('approved', 'Approved'),
rejected('rejected', 'Rejected'),
completed('completed', 'Completed');
const DisposalStatus(this.value, this.label);
final String value;
final String label;
static DisposalStatus fromValue(String value) {
return DisposalStatus.values.firstWhere(
(s) => s.value == value,
orElse: () => DisposalStatus.pending,
);
}
}
enum AssetCategoryType {
laptop('laptop', 'Laptop'),
desktop('desktop', 'Desktop'),
monitor('monitor', 'Monitor'),
mobile('mobile', 'Mobile'),
printer('printer', 'Printer'),
scanner('scanner', 'Scanner'),
furniture('furniture', 'Furniture'),
vehicle('vehicle', 'Vehicle'),
softwareLicense('software_license', 'Software License'),
other('other', 'Other');
const AssetCategoryType(this.value, this.label);
final String value;
final String label;
static AssetCategoryType fromValue(String value) {
return AssetCategoryType.values.firstWhere(
(c) => c.value == value,
orElse: () => AssetCategoryType.other,
);
}
}
enum ThemeModeOption {
light('light'),
dark('dark'),
system('system');
const ThemeModeOption(this.value);
final String value;
static ThemeModeOption fromValue(String value) {
return ThemeModeOption.values.firstWhere(
(m) => m.value == value,
orElse: () => ThemeModeOption.system,
);
}
}
enum ReportFormat {
pdf('pdf', 'PDF'),
excel('excel', 'Excel'),
csv('csv', 'CSV');
const ReportFormat(this.value, this.label);
final String value;
final String label;
}
enum ExportFormat {
pdf('pdf'),
excel('excel'),
csv('csv');
const ExportFormat(this.value);
final String value;
}

View File

@ -0,0 +1,80 @@
class RouteConstants {
RouteConstants._();
// Auth
static const String login = '/login';
static const String forgotPassword = '/forgot-password';
static const String resetPassword = '/reset-password';
static const String verifyOtp = '/verify-otp';
static const String changePassword = '/change-password';
// Shell
static const String shell = '/';
static const String dashboard = '/dashboard';
// Company
static const String companies = '/companies';
static const String companyAdd = '/companies/add';
static const String companyEdit = '/companies/:id/edit';
// Branch
static const String branches = '/branches';
static const String branchAdd = '/branches/add';
static const String branchEdit = '/branches/:id/edit';
// Users & Roles (RBAC)
static const String usersRoleManagement = '/users-roles';
// Users
static const String users = '/users';
static const String userAdd = '/users/add';
static const String userEdit = '/users/:id/edit';
static const String userDetail = '/users/:id';
// Roles
static const String roles = '/roles';
static const String roleEdit = '/roles/:id/edit';
static const String rolePermissions = '/roles/:id/permissions';
// Profile
static const String profile = '/profile';
// Assets
static const String assets = '/assets';
static const String assetAdd = '/assets/add';
static const String assetEdit = '/assets/:id/edit';
static const String assetDetail = '/assets/:id';
static const String assetCategories = '/assets/categories';
static const String assetAllocations = '/assets/allocations';
static const String assetMaintenance = '/assets/maintenance';
static const String assetDisposal = '/assets/disposal';
static const String assetQrScan = '/assets/qr-scan';
static const String assetQrGenerate = '/assets/qr-generate';
// Master Data
static const String masterData = '/master-data';
static const String departments = '/master/departments';
static const String locations = '/master/locations';
static const String uom = '/master/uom';
// Reports
static const String reports = '/reports';
// Settings
static const String settings = '/settings';
static const String settingsGeneral = '/settings/general';
static const String settingsCompanyProfile = '/settings/company-profile';
static const String settingsAppearance = '/settings/appearance';
static const String settingsRoles = '/settings/roles';
static const String settingsAsset = '/settings/asset';
static const String settingsNotifications = '/settings/notifications';
static const String settingsEmail = '/settings/email';
static const String settingsSecurity = '/settings/security';
static const String settingsBranding = '/settings/branding';
// Audit
static const String auditLogs = '/audit-logs';
// Dev
static const String screenGallery = '/dev/screens';
}

View File

@ -0,0 +1,15 @@
class StorageKeys {
StorageKeys._();
static const String accessToken = 'access_token';
static const String refreshToken = 'refresh_token';
static const String themeMode = 'theme_mode';
static const String companyId = 'company_id';
static const String branchId = 'branch_id';
static const String brandingPrimaryColor = 'branding_primary_color';
static const String brandingSecondaryColor = 'branding_secondary_color';
static const String brandingLogoUrl = 'branding_logo_url';
static const String appSettings = 'app_settings';
static const String rememberMe = 'remember_me';
static const String rememberedEmail = 'remembered_email';
}

View File

@ -0,0 +1,72 @@
class ServerException implements Exception {
const ServerException({
required this.message,
this.code,
this.statusCode,
});
final String message;
final String? code;
final int? statusCode;
@override
String toString() => 'ServerException: $message (code: $code, status: $statusCode)';
}
class NetworkException implements Exception {
const NetworkException([this.message = 'No internet connection']);
final String message;
@override
String toString() => 'NetworkException: $message';
}
class UnauthorizedException implements Exception {
const UnauthorizedException([this.message = 'Unauthorized']);
final String message;
@override
String toString() => 'UnauthorizedException: $message';
}
class ValidationException implements Exception {
const ValidationException({
required this.message,
this.errors,
});
final String message;
final Map<String, List<String>>? errors;
@override
String toString() => 'ValidationException: $message';
}
class ForbiddenException implements Exception {
const ForbiddenException([this.message = 'Access denied']);
final String message;
@override
String toString() => 'ForbiddenException: $message';
}
class ConflictException implements Exception {
const ConflictException([this.message = 'Conflict']);
final String message;
@override
String toString() => 'ConflictException: $message';
}
class CacheException implements Exception {
const CacheException([this.message = 'Cache error']);
final String message;
@override
String toString() => 'CacheException: $message';
}

View File

@ -0,0 +1,37 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'failure.freezed.dart';
@freezed
class Failure with _$Failure {
const factory Failure.server({
required String message,
String? code,
int? statusCode,
}) = ServerFailure;
const factory Failure.network({
@Default('No internet connection') String message,
}) = NetworkFailure;
const factory Failure.unauthorized({
@Default('Session expired. Please login again.') String message,
}) = UnauthorizedFailure;
const factory Failure.validation({
required String message,
Map<String, List<String>>? errors,
}) = ValidationFailure;
const factory Failure.notFound({
@Default('Resource not found') String message,
}) = NotFoundFailure;
const factory Failure.cache({
@Default('Cache error') String message,
}) = CacheFailure;
const factory Failure.unknown({
@Default('An unexpected error occurred') String message,
}) = UnknownFailure;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
import 'package:dio/dio.dart';
import '../errors/exceptions.dart';
import '../../shared/models/user_model.dart';
/// Parses `{ success, message, data?, meta?, errors? }` API responses.
class ApiEnvelope {
ApiEnvelope._();
static Map<String, dynamic> data(Response<Map<String, dynamic>> response) {
final body = response.data;
if (body == null) {
throw const ServerException(message: 'Empty server response');
}
ensureSuccess(body, statusCode: response.statusCode);
final data = body['data'];
if (data is Map<String, dynamic>) return data;
if (data == null) {
throw const ServerException(message: 'Response missing data');
}
throw ServerException(
message: 'Unexpected response data: ${data.runtimeType}',
);
}
static void ensureSuccess(
Map<String, dynamic> body, {
int? statusCode,
}) {
if (body['success'] == false) {
throw ServerException(
message: (body['message'] as String?)?.trim().isNotEmpty == true
? body['message'] as String
: 'Request failed',
statusCode: statusCode,
);
}
}
/// Login / refresh token pair (`accessToken`, `refreshToken` per API guide).
static AuthTokens parseTokens(Map<String, dynamic> data) {
final tokenSource = data['tokens'] is Map<String, dynamic>
? data['tokens'] as Map<String, dynamic>
: data;
final accessToken = tokenSource['accessToken'] as String? ??
tokenSource['access_token'] as String? ??
tokenSource['token'] as String?;
final refreshToken = tokenSource['refreshToken'] as String? ??
tokenSource['refresh_token'] as String? ??
'';
if (accessToken == null || accessToken.isEmpty) {
throw const ServerException(message: 'Response missing accessToken');
}
return AuthTokens(
accessToken: accessToken,
refreshToken: refreshToken,
expiresIn: (tokenSource['expires_in'] as num?)?.toInt(),
);
}
}

View File

@ -0,0 +1,107 @@
import 'package:dio/dio.dart';
import '../errors/exceptions.dart';
import '../errors/failure.dart';
typedef Result<T> = ({T? data, Failure? failure});
Result<T> handleDioError<T>(Object error) {
if (error is DioException) {
final inner = error.error;
if (inner is NetworkException) {
return (data: null, failure: Failure.network(message: inner.message));
}
if (inner is UnauthorizedException) {
return (data: null, failure: Failure.unauthorized(message: inner.message));
}
if (inner is ForbiddenException) {
return (
data: null,
failure: Failure.server(message: inner.message, statusCode: 403),
);
}
if (inner is ConflictException) {
return (
data: null,
failure: Failure.server(message: inner.message, statusCode: 409),
);
}
if (inner is ValidationException) {
return (
data: null,
failure: Failure.validation(message: inner.message, errors: inner.errors),
);
}
if (inner is ServerException) {
return (
data: null,
failure: Failure.server(
message: inner.message,
code: inner.code,
statusCode: inner.statusCode,
),
);
}
}
if (error is NetworkException) {
return (data: null, failure: Failure.network(message: error.message));
}
if (error is UnauthorizedException) {
return (data: null, failure: Failure.unauthorized(message: error.message));
}
if (error is ForbiddenException) {
return (
data: null,
failure: Failure.server(message: error.message, statusCode: 403),
);
}
if (error is ConflictException) {
return (
data: null,
failure: Failure.server(message: error.message, statusCode: 409),
);
}
if (error is ValidationException) {
return (
data: null,
failure: Failure.validation(message: error.message, errors: error.errors),
);
}
if (error is ServerException) {
return (
data: null,
failure: Failure.server(
message: error.message,
code: error.code,
statusCode: error.statusCode,
),
);
}
return (data: null, failure: Failure.unknown(message: error.toString()));
}
Future<Result<T>> safeApiCall<T>(Future<T> Function() call) async {
try {
final data = await call();
return (data: data, failure: null);
} catch (e) {
return handleDioError<T>(e);
}
}
bool isForbiddenFailure(Failure? failure) =>
failure is ServerFailure && failure.statusCode == 403;
bool isConflictFailure(Failure? failure) =>
failure is ServerFailure && failure.statusCode == 409;
String validationErrorMessage(Failure? failure) {
if (failure is! ValidationFailure) return failure?.message ?? 'Request failed';
final errors = failure.errors;
if (errors == null || errors.isEmpty) return failure.message;
return errors.entries
.map((e) => '${e.key}: ${e.value.join(', ')}')
.join('\n');
}

View File

@ -0,0 +1,89 @@
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constants/api_endpoints.dart';
import '../errors/exceptions.dart';
import '../../shared/providers/auth_provider.dart';
import 'token_refresh_service.dart';
import 'token_storage.dart';
class AuthInterceptor extends Interceptor {
AuthInterceptor(this._ref);
final Ref _ref;
static const _publicPaths = {
ApiEndpoints.login,
ApiEndpoints.refreshToken,
ApiEndpoints.logout,
ApiEndpoints.forgotPassword,
ApiEndpoints.verifyOtp,
};
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final tokenStorage = _ref.read(tokenStorageProvider);
final accessToken = await tokenStorage.getAccessToken();
final companyId = await tokenStorage.getCompanyId();
final branchId = await tokenStorage.getBranchId();
if (accessToken != null && accessToken.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $accessToken';
}
if (companyId != null) {
options.headers['X-Company-Id'] = companyId;
}
if (branchId != null) {
options.headers['X-Branch-Id'] = branchId;
}
handler.next(options);
}
@override
Future<void> onError(
DioException err,
ErrorInterceptorHandler handler,
) async {
final path = err.requestOptions.path;
final isPublic = _publicPaths.any((p) => path.endsWith(p));
final isRefreshCall = path.endsWith(ApiEndpoints.refreshToken);
if (err.response?.statusCode == 401 && !isPublic && !isRefreshCall) {
try {
final tokens = await _ref.read(tokenRefreshServiceProvider).refresh();
err.requestOptions.headers['Authorization'] =
'Bearer ${tokens.accessToken}';
final retryDio = Dio(
BaseOptions(
baseUrl: err.requestOptions.baseUrl,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer ${tokens.accessToken}',
},
),
);
final retryResponse = await retryDio.fetch(err.requestOptions);
handler.resolve(retryResponse);
return;
} catch (_) {
await _ref.read(tokenStorageProvider).clearTokens();
_ref.read(authStateProvider.notifier).onSessionExpired();
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: const UnauthorizedException('Session expired'),
),
);
return;
}
}
handler.next(err);
}
}

View File

@ -0,0 +1,32 @@
import 'package:dio/dio.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'auth_interceptor.dart';
import 'error_interceptor.dart';
final dioProvider = Provider<Dio>((ref) {
final dio = Dio(
BaseOptions(
baseUrl: dotenv.env['API_BASE_URL'] ?? 'https://api.bharaterp.example.com/v1',
connectTimeout: Duration(
seconds: int.tryParse(dotenv.env['API_TIMEOUT_SECONDS'] ?? '30') ?? 30,
),
receiveTimeout: Duration(
seconds: int.tryParse(dotenv.env['API_TIMEOUT_SECONDS'] ?? '30') ?? 30,
),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
dio.interceptors.addAll([
AuthInterceptor(ref),
ErrorInterceptor(),
LogInterceptor(requestBody: true, responseBody: true),
]);
return dio;
});

View File

@ -0,0 +1,128 @@
import 'package:dio/dio.dart';
import '../errors/exceptions.dart';
class ErrorInterceptor extends Interceptor {
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
case DioExceptionType.connectionError:
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: const NetworkException('Connection timeout'),
),
);
return;
case DioExceptionType.badResponse:
final statusCode = err.response?.statusCode;
final data = err.response?.data;
if (statusCode == 401) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
response: err.response,
error: UnauthorizedException(
_messageFromBody(data, fallback: 'Unauthorized'),
),
),
);
return;
}
if (statusCode == 403) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
response: err.response,
error: ForbiddenException(
_messageFromBody(data, fallback: 'Access denied'),
),
),
);
return;
}
if (statusCode == 409) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
response: err.response,
error: ConflictException(
_messageFromBody(data, fallback: 'Conflict'),
),
),
);
return;
}
if (statusCode == 422 && data is Map<String, dynamic>) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
response: err.response,
error: ValidationException(
message: _messageFromBody(data, fallback: 'Validation failed'),
errors: _parseErrors(data['errors']),
),
),
);
return;
}
handler.reject(
DioException(
requestOptions: err.requestOptions,
response: err.response,
error: ServerException(
message: _messageFromBody(data, fallback: 'Server error'),
code: data is Map<String, dynamic> ? data['code'] as String? : null,
statusCode: statusCode,
),
),
);
return;
default:
handler.next(err);
}
}
String _messageFromBody(dynamic data, {required String fallback}) {
if (data is Map<String, dynamic>) {
final message = data['message'] as String?;
if (message != null && message.trim().isNotEmpty) return message.trim();
}
return fallback;
}
Map<String, List<String>>? _parseErrors(dynamic errors) {
if (errors is List) {
final map = <String, List<String>>{};
for (final item in errors) {
if (item is Map) {
final field = item['field']?.toString() ?? 'general';
final message = item['message']?.toString() ?? 'Invalid value';
map.putIfAbsent(field, () => []).add(message);
}
}
return map.isEmpty ? null : map;
}
if (errors is Map<String, dynamic>) {
return errors.map(
(key, value) => MapEntry(
key,
value is List
? value.map((e) => e.toString()).toList()
: [value.toString()],
),
);
}
return null;
}
}

View File

@ -0,0 +1,60 @@
import 'package:dio/dio.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../constants/api_endpoints.dart';
import '../errors/exceptions.dart';
import 'api_envelope.dart';
import 'token_storage.dart';
import '../../shared/models/user_model.dart';
final tokenRefreshServiceProvider = Provider<TokenRefreshService>((ref) {
return TokenRefreshService(tokenStorage: ref.watch(tokenStorageProvider));
});
/// Single-flight token refresh concurrent 401s share one refresh call.
class TokenRefreshService {
TokenRefreshService({required this.tokenStorage});
final TokenStorage tokenStorage;
Future<AuthTokens>? _inFlight;
Future<AuthTokens> refresh() {
_inFlight ??= _refresh();
return _inFlight!;
}
Future<AuthTokens> _refresh() async {
try {
final refreshToken = await tokenStorage.getRefreshToken();
if (refreshToken == null || refreshToken.isEmpty) {
throw const UnauthorizedException('No refresh token');
}
final dio = Dio(
BaseOptions(
baseUrl: dotenv.env['API_BASE_URL'] ??
'https://demo.venbait.in/api/v1',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
final response = await dio.post<Map<String, dynamic>>(
ApiEndpoints.refreshToken,
data: {'refresh_token': refreshToken},
);
final tokens = ApiEnvelope.parseTokens(ApiEnvelope.data(response));
await tokenStorage.saveTokens(
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
);
return tokens;
} finally {
_inFlight = null;
}
}
}

View File

@ -0,0 +1,52 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../constants/storage_keys.dart';
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
return const FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
});
final tokenStorageProvider = Provider<TokenStorage>((ref) {
return TokenStorage(ref.watch(secureStorageProvider));
});
class TokenStorage {
TokenStorage(this._storage);
final FlutterSecureStorage _storage;
Future<String?> getAccessToken() => _storage.read(key: StorageKeys.accessToken);
Future<String?> getRefreshToken() => _storage.read(key: StorageKeys.refreshToken);
Future<void> saveTokens({
required String accessToken,
required String refreshToken,
}) async {
await _storage.write(key: StorageKeys.accessToken, value: accessToken);
await _storage.write(key: StorageKeys.refreshToken, value: refreshToken);
}
Future<void> clearTokens() async {
await _storage.delete(key: StorageKeys.accessToken);
await _storage.delete(key: StorageKeys.refreshToken);
}
Future<String?> getCompanyId() => _storage.read(key: StorageKeys.companyId);
Future<void> saveCompanyId(String companyId) =>
_storage.write(key: StorageKeys.companyId, value: companyId);
Future<void> clearCompanyId() => _storage.delete(key: StorageKeys.companyId);
Future<String?> getBranchId() => _storage.read(key: StorageKeys.branchId);
Future<void> saveBranchId(String branchId) =>
_storage.write(key: StorageKeys.branchId, value: branchId);
Future<void> clearBranchId() => _storage.delete(key: StorageKeys.branchId);
}

View File

@ -0,0 +1,61 @@
/// Parses live API permission-matrix payloads from `GET /roles/{id}/permission-matrix`.
class PermissionMatrixApiParser {
PermissionMatrixApiParser._();
static List<String> toPermissionKeys(Map<String, dynamic> data) {
final modules = data['modules'] as List<dynamic>? ?? const [];
final permissions = <String>{};
for (final raw in modules) {
if (raw is! Map<String, dynamic>) continue;
final moduleCode = (raw['code'] as String?)?.trim();
if (moduleCode == null || moduleCode.isEmpty) continue;
final modulePerms = raw['permissions'] as Map<String, dynamic>? ?? const {};
for (final entry in modulePerms.entries) {
if (!_isGranted(entry.value)) continue;
permissions.addAll(_keysForAction(moduleCode, entry.key));
}
}
return permissions.toList();
}
static bool _isGranted(Object? value) {
if (value is Map) return value['granted'] == true;
return value == true;
}
static List<String> _keysForAction(String moduleCode, String action) {
final module = moduleCode.toLowerCase();
final upper = moduleCode.toUpperCase();
return switch (action) {
'view' => [
'$module.read',
'$module:read',
'$upper:READ',
'$upper:read',
],
'edit' => [
'$module.create',
'$module.update',
'$module:create',
'$module:update',
'$upper:CREATE',
'$upper:UPDATE',
],
'approve' => [
'$module.approve',
'$module:approve',
'$upper:APPROVE',
],
'export' => [
'$module.export',
'$module:export',
'$upper:EXPORT',
],
_ => const <String>[],
};
}
}

View File

@ -0,0 +1,40 @@
import '../../shared/models/user_management_models.dart';
/// Converts API permission matrix rows into FE permission keys (`module.action`).
class PermissionResolver {
PermissionResolver._();
static List<String> fromMatrix(PermissionMatrixModel matrix) {
final permissions = <String>[];
for (final row in matrix.matrix) {
permissions.addAll(fromRow(row.module, row.actions));
}
return permissions;
}
static List<String> fromRow(String module, PermissionMatrixActions actions) {
final moduleKey = module.trim().toLowerCase();
final perms = <String>[];
if (actions.view) {
perms.add('$moduleKey.read');
perms.add('$moduleKey:read');
}
if (actions.edit) {
perms.add('$moduleKey.create');
perms.add('$moduleKey.update');
perms.add('$moduleKey:create');
perms.add('$moduleKey:update');
}
if (actions.approve) {
perms.add('$moduleKey.approve');
perms.add('$moduleKey:approve');
}
if (actions.export) {
perms.add('$moduleKey.export');
perms.add('$moduleKey:export');
}
return perms;
}
}

View File

@ -0,0 +1,22 @@
import 'package:flutter/material.dart';
class AppColors {
AppColors._();
static const Color primary = Color(0xFF1565C0);
static const Color secondary = Color(0xFF00897B);
static const Color error = Color(0xFFD32F2F);
static const Color warning = Color(0xFFF57C00);
static const Color success = Color(0xFF388E3C);
static const Color info = Color(0xFF1976D2);
static const Color lightBackground = Color(0xFFF5F7FA);
static const Color darkBackground = Color(0xFF121212);
static const Color lightSurface = Color(0xFFFFFFFF);
static const Color darkSurface = Color(0xFF1E1E1E);
static const Color card = Color(0xFFFFFFFF);
static const Color textPrimary = Color(0xFF212121);
static const Color textSecondary = Color(0xFF757575);
static const Color textOnDark = Color(0xFFE0E0E0);
}

View File

@ -0,0 +1,229 @@
import 'package:flutter/material.dart';
import 'app_colors.dart';
import 'app_typography.dart';
import 'branding_config.dart';
class AppTheme {
AppTheme._();
static ThemeData light({BrandingConfig? branding}) {
final primary = branding?.primaryColor ?? AppColors.primary;
final secondary = branding?.secondaryColor ?? AppColors.secondary;
final colorScheme = ColorScheme.fromSeed(
seedColor: primary,
secondary: secondary,
brightness: Brightness.light,
surface: AppColors.lightSurface,
);
return _buildTheme(colorScheme, Brightness.light);
}
static ThemeData dark({BrandingConfig? branding}) {
final primary = branding?.primaryColor ?? AppColors.primary;
final secondary = branding?.secondaryColor ?? AppColors.secondary;
final colorScheme = ColorScheme.fromSeed(
seedColor: primary,
secondary: secondary,
brightness: Brightness.dark,
surface: AppColors.darkSurface,
);
return _buildTheme(colorScheme, Brightness.dark);
}
/// Theme for white cards, form fields, and picker sheets (unchanged in dark mode).
static ThemeData cardContentTheme(ThemeData theme) {
final scheme = theme.brightness == Brightness.light
? theme.colorScheme
: ColorScheme.fromSeed(
seedColor: theme.colorScheme.primary,
secondary: theme.colorScheme.secondary,
brightness: Brightness.light,
surface: AppColors.card,
);
final base = theme.brightness == Brightness.light
? theme
: theme.copyWith(
brightness: Brightness.light,
colorScheme: scheme,
textTheme: AppTypography.textTheme(scheme),
primaryTextTheme: AppTypography.textTheme(scheme),
);
return base.copyWith(
scaffoldBackgroundColor: AppColors.card,
inputDecorationTheme: whiteInputDecorationTheme(scheme),
bottomSheetTheme: const BottomSheetThemeData(
backgroundColor: AppColors.card,
surfaceTintColor: Colors.transparent,
dragHandleColor: Color(0xFFBDBDBD),
),
listTileTheme: ListTileThemeData(
tileColor: AppColors.card,
selectedTileColor: scheme.primary.withValues(alpha: 0.08),
textColor: scheme.onSurface,
iconColor: scheme.onSurfaceVariant,
),
dividerTheme: DividerThemeData(
color: scheme.outline.withValues(alpha: 0.15),
),
);
}
static InputDecorationTheme whiteInputDecorationTheme(ColorScheme scheme) {
final textTheme = AppTypography.textTheme(scheme);
return InputDecorationTheme(
filled: true,
fillColor: AppColors.card,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scheme.outline.withValues(alpha: 0.35)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scheme.primary, width: 2),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scheme.outline.withValues(alpha: 0.2)),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scheme.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: scheme.error, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: textTheme.bodyMedium,
hintStyle: textTheme.bodyMedium?.copyWith(color: scheme.onSurfaceVariant),
helperStyle: textTheme.bodySmall,
errorStyle: textTheme.bodySmall?.copyWith(color: scheme.error),
);
}
static ThemeData _buildTheme(ColorScheme colorScheme, Brightness brightness) {
final isDark = brightness == Brightness.dark;
final textTheme = AppTypography.textTheme(colorScheme);
return ThemeData(
useMaterial3: true,
fontFamily: AppTypography.fontFamily,
colorScheme: colorScheme,
brightness: brightness,
textTheme: textTheme,
primaryTextTheme: textTheme,
scaffoldBackgroundColor:
isDark ? AppColors.darkBackground : AppColors.lightBackground,
appBarTheme: AppBarTheme(
centerTitle: false,
elevation: 0,
scrolledUnderElevation: 1,
backgroundColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
foregroundColor: isDark ? AppColors.textOnDark : AppColors.textPrimary,
titleTextStyle: textTheme.titleLarge,
toolbarTextStyle: textTheme.bodyLarge,
),
cardTheme: CardThemeData(
color: isDark ? AppColors.darkSurface : AppColors.lightSurface,
surfaceTintColor: Colors.transparent,
elevation: isDark ? 2 : 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
clipBehavior: Clip.antiAlias,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: isDark
? colorScheme.surfaceContainerHighest
: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.outline.withValues(alpha: 0.5)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
labelStyle: textTheme.bodyMedium,
hintStyle: textTheme.bodyMedium?.copyWith(color: colorScheme.onSurfaceVariant),
helperStyle: textTheme.bodySmall,
errorStyle: textTheme.bodySmall?.copyWith(color: colorScheme.error),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
textStyle: textTheme.labelLarge,
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: textTheme.labelLarge,
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
minimumSize: const Size(0, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: textTheme.labelLarge,
),
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
selectedIconTheme: IconThemeData(color: colorScheme.primary),
selectedLabelTextStyle: textTheme.labelMedium?.copyWith(color: colorScheme.primary),
unselectedLabelTextStyle: textTheme.labelMedium,
),
drawerTheme: DrawerThemeData(
backgroundColor: isDark ? AppColors.darkSurface : AppColors.lightSurface,
),
dividerTheme: DividerThemeData(
color: colorScheme.outline.withValues(alpha: 0.2),
),
snackBarTheme: SnackBarThemeData(
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
contentTextStyle: textTheme.bodyMedium?.copyWith(color: colorScheme.onInverseSurface),
),
dataTableTheme: DataTableThemeData(
headingRowColor: WidgetStateProperty.all(
colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
),
headingTextStyle: textTheme.labelLarge,
dataTextStyle: textTheme.bodyMedium,
),
chipTheme: ChipThemeData(
labelStyle: textTheme.labelSmall,
),
dialogTheme: DialogThemeData(
titleTextStyle: textTheme.titleLarge,
contentTextStyle: textTheme.bodyMedium,
),
listTileTheme: ListTileThemeData(
titleTextStyle: textTheme.bodyLarge,
subtitleTextStyle: textTheme.bodySmall,
),
bottomNavigationBarTheme: BottomNavigationBarThemeData(
selectedLabelStyle: textTheme.labelSmall,
unselectedLabelStyle: textTheme.labelSmall,
),
tabBarTheme: TabBarThemeData(
labelStyle: textTheme.labelLarge,
unselectedLabelStyle: textTheme.labelMedium,
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
/// ERP typography Figtree (Google Font).
/// Weights: Regular 400, Medium 500, SemiBold 600.
class AppTypography {
AppTypography._();
static const FontWeight regular = FontWeight.w400;
static const FontWeight medium = FontWeight.w500;
static const FontWeight semiBold = FontWeight.w600;
static String get fontFamily => GoogleFonts.figtree().fontFamily ?? 'Figtree';
static TextTheme textTheme(ColorScheme colorScheme) {
TextStyle token({
required double size,
FontWeight weight = regular,
Color? color,
double? height,
}) {
return GoogleFonts.figtree(
fontSize: size,
fontWeight: weight,
letterSpacing: 0,
height: height,
color: color ?? colorScheme.onSurface,
);
}
final onSurface = colorScheme.onSurface;
final onSurfaceVariant = colorScheme.onSurfaceVariant;
return TextTheme(
// Display
displayLarge: token(size: 57, weight: semiBold, color: onSurface),
displayMedium: token(size: 45, weight: semiBold, color: onSurface),
displaySmall: token(size: 36, weight: semiBold, color: onSurface),
// Headline
headlineLarge: token(size: 20, weight: semiBold, color: onSurface),
headlineMedium: token(size: 16, weight: regular, color: onSurface),
headlineSmall: token(size: 18, weight: semiBold, color: onSurface),
// Title
titleLarge: token(size: 22, weight: semiBold, color: onSurface),
titleMedium: token(size: 16, weight: medium, color: onSurface),
titleSmall: token(size: 14, weight: medium, color: onSurface),
// Body
bodyLarge: token(size: 16, weight: regular, color: onSurface),
bodyMedium: token(size: 16, weight: regular, color: onSurface),
bodySmall: token(size: 12, weight: regular, color: onSurfaceVariant),
// Label
labelLarge: token(size: 14, weight: medium, color: onSurface),
labelMedium: token(size: 12, weight: medium, color: onSurfaceVariant),
labelSmall: token(size: 10, weight: regular, color: onSurfaceVariant),
);
}
}

View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'branding_config.freezed.dart';
part 'branding_config.g.dart';
@freezed
class BrandingConfig with _$BrandingConfig {
const factory BrandingConfig({
String? logoUrl,
@Default(0xFF1565C0) int primaryColorValue,
@Default(0xFF00897B) int secondaryColorValue,
String? companyName,
}) = _BrandingConfig;
factory BrandingConfig.fromJson(Map<String, dynamic> json) =>
_$BrandingConfigFromJson(json);
}
extension BrandingConfigX on BrandingConfig {
Color get primaryColor => Color(primaryColorValue);
Color get secondaryColor => Color(secondaryColorValue);
}

View File

@ -0,0 +1,253 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'branding_config.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
BrandingConfig _$BrandingConfigFromJson(Map<String, dynamic> json) {
return _BrandingConfig.fromJson(json);
}
/// @nodoc
mixin _$BrandingConfig {
String? get logoUrl => throw _privateConstructorUsedError;
int get primaryColorValue => throw _privateConstructorUsedError;
int get secondaryColorValue => throw _privateConstructorUsedError;
String? get companyName => throw _privateConstructorUsedError;
/// Serializes this BrandingConfig to a JSON map.
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
/// Create a copy of BrandingConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$BrandingConfigCopyWith<BrandingConfig> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $BrandingConfigCopyWith<$Res> {
factory $BrandingConfigCopyWith(
BrandingConfig value,
$Res Function(BrandingConfig) then,
) = _$BrandingConfigCopyWithImpl<$Res, BrandingConfig>;
@useResult
$Res call({
String? logoUrl,
int primaryColorValue,
int secondaryColorValue,
String? companyName,
});
}
/// @nodoc
class _$BrandingConfigCopyWithImpl<$Res, $Val extends BrandingConfig>
implements $BrandingConfigCopyWith<$Res> {
_$BrandingConfigCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of BrandingConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? logoUrl = freezed,
Object? primaryColorValue = null,
Object? secondaryColorValue = null,
Object? companyName = freezed,
}) {
return _then(
_value.copyWith(
logoUrl: freezed == logoUrl
? _value.logoUrl
: logoUrl // ignore: cast_nullable_to_non_nullable
as String?,
primaryColorValue: null == primaryColorValue
? _value.primaryColorValue
: primaryColorValue // ignore: cast_nullable_to_non_nullable
as int,
secondaryColorValue: null == secondaryColorValue
? _value.secondaryColorValue
: secondaryColorValue // ignore: cast_nullable_to_non_nullable
as int,
companyName: freezed == companyName
? _value.companyName
: companyName // ignore: cast_nullable_to_non_nullable
as String?,
)
as $Val,
);
}
}
/// @nodoc
abstract class _$$BrandingConfigImplCopyWith<$Res>
implements $BrandingConfigCopyWith<$Res> {
factory _$$BrandingConfigImplCopyWith(
_$BrandingConfigImpl value,
$Res Function(_$BrandingConfigImpl) then,
) = __$$BrandingConfigImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
String? logoUrl,
int primaryColorValue,
int secondaryColorValue,
String? companyName,
});
}
/// @nodoc
class __$$BrandingConfigImplCopyWithImpl<$Res>
extends _$BrandingConfigCopyWithImpl<$Res, _$BrandingConfigImpl>
implements _$$BrandingConfigImplCopyWith<$Res> {
__$$BrandingConfigImplCopyWithImpl(
_$BrandingConfigImpl _value,
$Res Function(_$BrandingConfigImpl) _then,
) : super(_value, _then);
/// Create a copy of BrandingConfig
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? logoUrl = freezed,
Object? primaryColorValue = null,
Object? secondaryColorValue = null,
Object? companyName = freezed,
}) {
return _then(
_$BrandingConfigImpl(
logoUrl: freezed == logoUrl
? _value.logoUrl
: logoUrl // ignore: cast_nullable_to_non_nullable
as String?,
primaryColorValue: null == primaryColorValue
? _value.primaryColorValue
: primaryColorValue // ignore: cast_nullable_to_non_nullable
as int,
secondaryColorValue: null == secondaryColorValue
? _value.secondaryColorValue
: secondaryColorValue // ignore: cast_nullable_to_non_nullable
as int,
companyName: freezed == companyName
? _value.companyName
: companyName // ignore: cast_nullable_to_non_nullable
as String?,
),
);
}
}
/// @nodoc
@JsonSerializable()
class _$BrandingConfigImpl implements _BrandingConfig {
const _$BrandingConfigImpl({
this.logoUrl,
this.primaryColorValue = 0xFF1565C0,
this.secondaryColorValue = 0xFF00897B,
this.companyName,
});
factory _$BrandingConfigImpl.fromJson(Map<String, dynamic> json) =>
_$$BrandingConfigImplFromJson(json);
@override
final String? logoUrl;
@override
@JsonKey()
final int primaryColorValue;
@override
@JsonKey()
final int secondaryColorValue;
@override
final String? companyName;
@override
String toString() {
return 'BrandingConfig(logoUrl: $logoUrl, primaryColorValue: $primaryColorValue, secondaryColorValue: $secondaryColorValue, companyName: $companyName)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$BrandingConfigImpl &&
(identical(other.logoUrl, logoUrl) || other.logoUrl == logoUrl) &&
(identical(other.primaryColorValue, primaryColorValue) ||
other.primaryColorValue == primaryColorValue) &&
(identical(other.secondaryColorValue, secondaryColorValue) ||
other.secondaryColorValue == secondaryColorValue) &&
(identical(other.companyName, companyName) ||
other.companyName == companyName));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(
runtimeType,
logoUrl,
primaryColorValue,
secondaryColorValue,
companyName,
);
/// Create a copy of BrandingConfig
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$BrandingConfigImplCopyWith<_$BrandingConfigImpl> get copyWith =>
__$$BrandingConfigImplCopyWithImpl<_$BrandingConfigImpl>(
this,
_$identity,
);
@override
Map<String, dynamic> toJson() {
return _$$BrandingConfigImplToJson(this);
}
}
abstract class _BrandingConfig implements BrandingConfig {
const factory _BrandingConfig({
final String? logoUrl,
final int primaryColorValue,
final int secondaryColorValue,
final String? companyName,
}) = _$BrandingConfigImpl;
factory _BrandingConfig.fromJson(Map<String, dynamic> json) =
_$BrandingConfigImpl.fromJson;
@override
String? get logoUrl;
@override
int get primaryColorValue;
@override
int get secondaryColorValue;
@override
String? get companyName;
/// Create a copy of BrandingConfig
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$BrandingConfigImplCopyWith<_$BrandingConfigImpl> get copyWith =>
throw _privateConstructorUsedError;
}

View File

@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'branding_config.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
_$BrandingConfigImpl _$$BrandingConfigImplFromJson(Map<String, dynamic> json) =>
_$BrandingConfigImpl(
logoUrl: json['logoUrl'] as String?,
primaryColorValue:
(json['primaryColorValue'] as num?)?.toInt() ?? 0xFF1565C0,
secondaryColorValue:
(json['secondaryColorValue'] as num?)?.toInt() ?? 0xFF00897B,
companyName: json['companyName'] as String?,
);
Map<String, dynamic> _$$BrandingConfigImplToJson(
_$BrandingConfigImpl instance,
) => <String, dynamic>{
'logoUrl': instance.logoUrl,
'primaryColorValue': instance.primaryColorValue,
'secondaryColorValue': instance.secondaryColorValue,
'companyName': instance.companyName,
};

View File

@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../constants/enums.dart';
import '../constants/storage_keys.dart';
import 'app_theme.dart';
import 'branding_config.dart';
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
throw UnimplementedError('SharedPreferences must be overridden in main.dart');
});
final themeModeProvider = StateNotifierProvider<ThemeModeNotifier, ThemeModeOption>((ref) {
return ThemeModeNotifier(ref.watch(sharedPreferencesProvider));
});
final brandingProvider = StateNotifierProvider<BrandingNotifier, BrandingConfig>((ref) {
return BrandingNotifier(ref.watch(sharedPreferencesProvider));
});
class ThemeModeNotifier extends StateNotifier<ThemeModeOption> {
ThemeModeNotifier(this._prefs) : super(ThemeModeOption.system) {
_load();
}
final SharedPreferences _prefs;
void _load() {
final saved = _prefs.getString(StorageKeys.themeMode);
if (saved != null) {
state = ThemeModeOption.fromValue(saved);
}
}
Future<void> setThemeMode(ThemeModeOption mode) async {
state = mode;
await _prefs.setString(StorageKeys.themeMode, mode.value);
}
}
class BrandingNotifier extends StateNotifier<BrandingConfig> {
BrandingNotifier(this._prefs) : super(const BrandingConfig()) {
_load();
}
final SharedPreferences _prefs;
void _load() {
final primary = _prefs.getInt(StorageKeys.brandingPrimaryColor);
final secondary = _prefs.getInt(StorageKeys.brandingSecondaryColor);
final logoUrl = _prefs.getString(StorageKeys.brandingLogoUrl);
state = BrandingConfig(
primaryColorValue: primary ?? state.primaryColorValue,
secondaryColorValue: secondary ?? state.secondaryColorValue,
logoUrl: logoUrl,
);
}
Future<void> updateBranding(BrandingConfig config) async {
state = config;
await _prefs.setInt(StorageKeys.brandingPrimaryColor, config.primaryColorValue);
await _prefs.setInt(StorageKeys.brandingSecondaryColor, config.secondaryColorValue);
final logo = config.logoUrl?.trim();
if (logo != null && logo.isNotEmpty) {
await _prefs.setString(StorageKeys.brandingLogoUrl, logo);
} else {
await _prefs.remove(StorageKeys.brandingLogoUrl);
}
}
}
ThemeMode resolveThemeMode(ThemeModeOption option) {
return switch (option) {
ThemeModeOption.light => ThemeMode.light,
ThemeModeOption.dark => ThemeMode.dark,
ThemeModeOption.system => ThemeMode.system,
};
}
ThemeData buildLightTheme(BrandingConfig branding) => AppTheme.light(branding: branding);
ThemeData buildDarkTheme(BrandingConfig branding) => AppTheme.dark(branding: branding);

View File

@ -0,0 +1,41 @@
import 'package:intl/intl.dart';
class DateFormatter {
DateFormatter._();
static final _dateFormat = DateFormat('dd MMM yyyy');
static final _dateTimeFormat = DateFormat('dd MMM yyyy, hh:mm a');
static final _apiDateFormat = DateFormat('yyyy-MM-dd');
static String displayDate(DateTime? date) {
if (date == null) return '-';
return _dateFormat.format(date);
}
static String displayDateTime(DateTime? date) {
if (date == null) return '-';
return _dateTimeFormat.format(date);
}
static String toApiDate(DateTime date) => _apiDateFormat.format(date);
static DateTime? parseApiDate(String? value) {
if (value == null || value.isEmpty) return null;
return DateTime.tryParse(value);
}
}
class CurrencyFormatter {
CurrencyFormatter._();
static final _formatter = NumberFormat.currency(
locale: 'en_IN',
symbol: '',
decimalDigits: 2,
);
static String format(double? amount) {
if (amount == null) return '-';
return _formatter.format(amount);
}
}

View File

@ -0,0 +1,31 @@
import 'dart:convert';
/// Lightweight JWT payload decoder (no signature verification on client).
class JwtUtils {
JwtUtils._();
static Map<String, dynamic> decodePayload(String token) {
final parts = token.split('.');
if (parts.length != 3) {
throw const FormatException('Invalid JWT format');
}
final normalized = base64Url.normalize(parts[1]);
final decoded = utf8.decode(base64Url.decode(normalized));
final payload = jsonDecode(decoded);
if (payload is! Map<String, dynamic>) {
throw const FormatException('Invalid JWT payload');
}
return payload;
}
static String? subject(String token) {
final sub = decodePayload(token)['sub'];
return sub?.toString();
}
static String? roleId(String token) {
final roleId = decodePayload(token)['role_id'];
return roleId?.toString();
}
}

View File

@ -0,0 +1,38 @@
import '../errors/failure.dart';
import '../network/api_handler.dart';
import '../../core/constants/enums.dart';
bool hasPermission({
required List<String> userPermissions,
required String module,
required PermissionAction action,
}) {
final normalizedModule = module.trim().toLowerCase();
final actionValue = action.value;
final candidates = {
'$normalizedModule.$actionValue',
'$normalizedModule:$actionValue',
'${normalizedModule.toUpperCase()}:$actionValue',
'${normalizedModule.toUpperCase()}:${actionValue.toUpperCase()}',
'${module.toUpperCase()}:$actionValue',
};
if (userPermissions.contains('*')) return true;
for (final key in candidates) {
if (userPermissions.contains(key)) return true;
}
return userPermissions.contains('$normalizedModule.*') ||
userPermissions.contains('${normalizedModule.toUpperCase()}:*');
}
bool isSuperAdmin(UserRole role) => role == UserRole.superAdmin;
bool isAdmin(UserRole role) =>
role == UserRole.superAdmin || role == UserRole.companyAdmin;
String failureMessage(Failure failure) => failure.message;
bool isAccessDenied(Failure? failure) => isForbiddenFailure(failure);

View File

@ -0,0 +1,24 @@
import 'package:flutter/material.dart';
import 'package:responsive_framework/responsive_framework.dart';
class AppBreakpoints {
AppBreakpoints._();
static const double mobile = 0;
static const double tablet = 600;
static const double desktop = 1024;
static const double wide = 1440;
}
extension ResponsiveContext on BuildContext {
bool get isMobile => ResponsiveBreakpoints.of(this).isMobile;
bool get isTablet => ResponsiveBreakpoints.of(this).isTablet;
bool get isDesktop => ResponsiveBreakpoints.of(this).largerThan(TABLET);
bool get isWide => ResponsiveBreakpoints.of(this).largerThan(DESKTOP);
double get contentMaxWidth {
if (isWide) return 1400;
if (isDesktop) return 1200;
return double.infinity;
}
}

View File

@ -0,0 +1,47 @@
class Validators {
Validators._();
static String? required(String? value, {String fieldName = 'This field'}) {
if (value == null || value.trim().isEmpty) {
return '$fieldName is required';
}
return null;
}
static String? email(String? value) {
if (value == null || value.trim().isEmpty) return 'Email is required';
final regex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
if (!regex.hasMatch(value.trim())) return 'Enter a valid email address';
return null;
}
static String? phone(String? value) {
if (value == null || value.trim().isEmpty) return 'Phone is required';
final regex = RegExp(r'^[6-9]\d{9}$');
if (!regex.hasMatch(value.trim())) return 'Enter a valid 10-digit mobile number';
return null;
}
static String? password(String? value) {
if (value == null || value.isEmpty) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
if (!RegExp(r'[A-Z]').hasMatch(value)) return 'Must contain an uppercase letter';
if (!RegExp(r'[a-z]').hasMatch(value)) return 'Must contain a lowercase letter';
if (!RegExp(r'[0-9]').hasMatch(value)) return 'Must contain a number';
return null;
}
static String? gstNumber(String? value) {
if (value == null || value.trim().isEmpty) return null;
final regex = RegExp(r'^\d{2}[A-Z]{5}\d{4}[A-Z]{1}[A-Z\d]{1}Z[A-Z\d]{1}$');
if (!regex.hasMatch(value.trim().toUpperCase())) return 'Enter a valid GST number';
return null;
}
static String? minLength(String? value, int min, {String fieldName = 'Field'}) {
if (value == null || value.length < min) {
return '$fieldName must be at least $min characters';
}
return null;
}
}

23
lib/main.dart Normal file
View File

@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'app.dart';
import 'core/config/app_env.dart';
import 'core/theme/theme_provider.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: AppEnv.envFileName);
final prefs = await SharedPreferences.getInstance();
runApp(
ProviderScope(
overrides: [
sharedPreferencesProvider.overrideWithValue(prefs),
],
child: const BharatErpApp(),
),
);
}

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