commit 8f852ac4f608689adad6229224de212d931d8c9f Author: SurendarSuri30 Date: Fri Jun 19 15:04:06 2026 +0530 Initial commit: Bharat ERP Flutter application. Co-authored-by: Cursor diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..993fb9a --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +API_BASE_URL=https://demo.venbait.in/api/v1 +API_TIMEOUT_SECONDS=30 +DEV_BYPASS_AUTH=false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..64cfe05 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..534977f --- /dev/null +++ b/.metadata @@ -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' diff --git a/README.md b/README.md new file mode 100644 index 0000000..a6ffa8e --- /dev/null +++ b/README.md @@ -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 diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/analysis_options.yaml @@ -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 diff --git a/android/.gitignore b/android/.gitignore new file mode 100755 index 0000000..be3943c --- /dev/null +++ b/android/.gitignore @@ -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 diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 0000000..a20f905 --- /dev/null +++ b/android/app/build.gradle.kts @@ -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 = "../.." +} diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b3ba8a9 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/kotlin/com/bharaterp/bharat_erp/MainActivity.kt b/android/app/src/main/kotlin/com/bharaterp/bharat_erp/MainActivity.kt new file mode 100644 index 0000000..8c642f8 --- /dev/null +++ b/android/app/src/main/kotlin/com/bharaterp/bharat_erp/MainActivity.kt @@ -0,0 +1,5 @@ +package com.bharaterp.bharat_erp + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100755 index 0000000..f74085f --- /dev/null +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml new file mode 100755 index 0000000..304732f --- /dev/null +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100755 index 0000000..db77bb4 Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100755 index 0000000..17987b7 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100755 index 0000000..09d4391 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100755 index 0000000..d5f1c8d Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100755 index 0000000..4d6372e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml new file mode 100755 index 0000000..06952be --- /dev/null +++ b/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100755 index 0000000..cb1ef88 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/android/build.gradle.kts @@ -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("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts new file mode 100644 index 0000000..fb605bc --- /dev/null +++ b/android/settings.gradle.kts @@ -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") diff --git a/assets/icons/.gitkeep b/assets/icons/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/images/.gitkeep b/assets/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..ea2c5bf --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -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// +├── 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 ` +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 | 600–1024px | Collapsible sidebar | +| Desktop | > 1024px | Persistent sidebar | + +## API Conventions + +- Base URL: configured via `.env` +- All responses wrapped in `ApiResponse` +- Pagination via `PaginatedResponse` 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//` 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. diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100755 index 0000000..7a7f987 --- /dev/null +++ b/ios/.gitignore @@ -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 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist new file mode 100755 index 0000000..1dc6cf7 --- /dev/null +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 13.0 + + diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig new file mode 100755 index 0000000..ec97fc6 --- /dev/null +++ b/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig new file mode 100755 index 0000000..c4855bf --- /dev/null +++ b/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100755 index 0000000..620e46e --- /dev/null +++ b/ios/Podfile @@ -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 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..f6fc8ca --- /dev/null +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -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 = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 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 = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 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 = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* 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 = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 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 = ""; + }; +/* 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 = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* 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 */; +} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..919434a --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100755 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..e3773d4 --- /dev/null +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..1d526a1 --- /dev/null +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 0000000..18d9810 --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100755 index 0000000..f9b0d7c --- /dev/null +++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift new file mode 100755 index 0000000..6266644 --- /dev/null +++ b/ios/Runner/AppDelegate.swift @@ -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) + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..d36b1fa --- /dev/null +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -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" + } +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100755 index 0000000..0bedcf2 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -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" + } +} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100755 index 0000000..89c2725 --- /dev/null +++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -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. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100755 index 0000000..f2e259c --- /dev/null +++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard new file mode 100755 index 0000000..f3c2851 --- /dev/null +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist new file mode 100644 index 0000000..f15f9ba --- /dev/null +++ b/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Bharat Erp + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + bharat_erp + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + + + diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h new file mode 100755 index 0000000..308a2a5 --- /dev/null +++ b/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/ios/RunnerTests/RunnerTests.swift @@ -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. + } + +} diff --git a/lib/app.dart b/lib/app.dart new file mode 100644 index 0000000..82b161b --- /dev/null +++ b/lib/app.dart @@ -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'), + ], + ), + ); + } +} diff --git a/lib/core/config/app_env.dart b/lib/core/config/app_env.dart new file mode 100644 index 0000000..6f7b893 --- /dev/null +++ b/lib/core/config/app_env.dart @@ -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'; + } + } +} diff --git a/lib/core/config/dev_config.dart b/lib/core/config/dev_config.dart new file mode 100644 index 0000000..ae26ef3 --- /dev/null +++ b/lib/core/config/dev_config.dart @@ -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'; +} diff --git a/lib/core/constants/api_endpoints.dart b/lib/core/constants/api_endpoints.dart new file mode 100644 index 0000000..5687089 --- /dev/null +++ b/lib/core/constants/api_endpoints.dart @@ -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'; +} diff --git a/lib/core/constants/app_constants.dart b/lib/core/constants/app_constants.dart new file mode 100644 index 0000000..1dea5a0 --- /dev/null +++ b/lib/core/constants/app_constants.dart @@ -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); +} diff --git a/lib/core/constants/enums.dart b/lib/core/constants/enums.dart new file mode 100644 index 0000000..89155fc --- /dev/null +++ b/lib/core/constants/enums.dart @@ -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; +} diff --git a/lib/core/constants/route_constants.dart b/lib/core/constants/route_constants.dart new file mode 100644 index 0000000..abe245a --- /dev/null +++ b/lib/core/constants/route_constants.dart @@ -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'; +} diff --git a/lib/core/constants/storage_keys.dart b/lib/core/constants/storage_keys.dart new file mode 100644 index 0000000..5ec0263 --- /dev/null +++ b/lib/core/constants/storage_keys.dart @@ -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'; +} diff --git a/lib/core/errors/exceptions.dart b/lib/core/errors/exceptions.dart new file mode 100644 index 0000000..3e50fb9 --- /dev/null +++ b/lib/core/errors/exceptions.dart @@ -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>? 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'; +} diff --git a/lib/core/errors/failure.dart b/lib/core/errors/failure.dart new file mode 100644 index 0000000..75b8fae --- /dev/null +++ b/lib/core/errors/failure.dart @@ -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>? 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; +} diff --git a/lib/core/errors/failure.freezed.dart b/lib/core/errors/failure.freezed.dart new file mode 100644 index 0000000..b45d27a --- /dev/null +++ b/lib/core/errors/failure.freezed.dart @@ -0,0 +1,1464 @@ +// 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 'failure.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +/// @nodoc +mixin _$Failure { + String get message => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult when({ + required TResult Function(String message, String? code, int? statusCode) + server, + required TResult Function(String message) network, + required TResult Function(String message) unauthorized, + required TResult Function(String message, Map>? errors) + validation, + required TResult Function(String message) notFound, + required TResult Function(String message) cache, + required TResult Function(String message) unknown, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String message, String? code, int? statusCode)? server, + TResult? Function(String message)? network, + TResult? Function(String message)? unauthorized, + TResult? Function(String message, Map>? errors)? + validation, + TResult? Function(String message)? notFound, + TResult? Function(String message)? cache, + TResult? Function(String message)? unknown, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String message, String? code, int? statusCode)? server, + TResult Function(String message)? network, + TResult Function(String message)? unauthorized, + TResult Function(String message, Map>? errors)? + validation, + TResult Function(String message)? notFound, + TResult Function(String message)? cache, + TResult Function(String message)? unknown, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(ServerFailure value) server, + required TResult Function(NetworkFailure value) network, + required TResult Function(UnauthorizedFailure value) unauthorized, + required TResult Function(ValidationFailure value) validation, + required TResult Function(NotFoundFailure value) notFound, + required TResult Function(CacheFailure value) cache, + required TResult Function(UnknownFailure value) unknown, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ServerFailure value)? server, + TResult? Function(NetworkFailure value)? network, + TResult? Function(UnauthorizedFailure value)? unauthorized, + TResult? Function(ValidationFailure value)? validation, + TResult? Function(NotFoundFailure value)? notFound, + TResult? Function(CacheFailure value)? cache, + TResult? Function(UnknownFailure value)? unknown, + }) => throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ServerFailure value)? server, + TResult Function(NetworkFailure value)? network, + TResult Function(UnauthorizedFailure value)? unauthorized, + TResult Function(ValidationFailure value)? validation, + TResult Function(NotFoundFailure value)? notFound, + TResult Function(CacheFailure value)? cache, + TResult Function(UnknownFailure value)? unknown, + required TResult orElse(), + }) => throw _privateConstructorUsedError; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $FailureCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FailureCopyWith<$Res> { + factory $FailureCopyWith(Failure value, $Res Function(Failure) then) = + _$FailureCopyWithImpl<$Res, Failure>; + @useResult + $Res call({String message}); +} + +/// @nodoc +class _$FailureCopyWithImpl<$Res, $Val extends Failure> + implements $FailureCopyWith<$Res> { + _$FailureCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? message = null}) { + return _then( + _value.copyWith( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ServerFailureImplCopyWith<$Res> + implements $FailureCopyWith<$Res> { + factory _$$ServerFailureImplCopyWith( + _$ServerFailureImpl value, + $Res Function(_$ServerFailureImpl) then, + ) = __$$ServerFailureImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message, String? code, int? statusCode}); +} + +/// @nodoc +class __$$ServerFailureImplCopyWithImpl<$Res> + extends _$FailureCopyWithImpl<$Res, _$ServerFailureImpl> + implements _$$ServerFailureImplCopyWith<$Res> { + __$$ServerFailureImplCopyWithImpl( + _$ServerFailureImpl _value, + $Res Function(_$ServerFailureImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + Object? code = freezed, + Object? statusCode = freezed, + }) { + return _then( + _$ServerFailureImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + code: freezed == code + ? _value.code + : code // ignore: cast_nullable_to_non_nullable + as String?, + statusCode: freezed == statusCode + ? _value.statusCode + : statusCode // ignore: cast_nullable_to_non_nullable + as int?, + ), + ); + } +} + +/// @nodoc + +class _$ServerFailureImpl implements ServerFailure { + const _$ServerFailureImpl({ + required this.message, + this.code, + this.statusCode, + }); + + @override + final String message; + @override + final String? code; + @override + final int? statusCode; + + @override + String toString() { + return 'Failure.server(message: $message, code: $code, statusCode: $statusCode)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ServerFailureImpl && + (identical(other.message, message) || other.message == message) && + (identical(other.code, code) || other.code == code) && + (identical(other.statusCode, statusCode) || + other.statusCode == statusCode)); + } + + @override + int get hashCode => Object.hash(runtimeType, message, code, statusCode); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ServerFailureImplCopyWith<_$ServerFailureImpl> get copyWith => + __$$ServerFailureImplCopyWithImpl<_$ServerFailureImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String message, String? code, int? statusCode) + server, + required TResult Function(String message) network, + required TResult Function(String message) unauthorized, + required TResult Function(String message, Map>? errors) + validation, + required TResult Function(String message) notFound, + required TResult Function(String message) cache, + required TResult Function(String message) unknown, + }) { + return server(message, code, statusCode); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String message, String? code, int? statusCode)? server, + TResult? Function(String message)? network, + TResult? Function(String message)? unauthorized, + TResult? Function(String message, Map>? errors)? + validation, + TResult? Function(String message)? notFound, + TResult? Function(String message)? cache, + TResult? Function(String message)? unknown, + }) { + return server?.call(message, code, statusCode); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String message, String? code, int? statusCode)? server, + TResult Function(String message)? network, + TResult Function(String message)? unauthorized, + TResult Function(String message, Map>? errors)? + validation, + TResult Function(String message)? notFound, + TResult Function(String message)? cache, + TResult Function(String message)? unknown, + required TResult orElse(), + }) { + if (server != null) { + return server(message, code, statusCode); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ServerFailure value) server, + required TResult Function(NetworkFailure value) network, + required TResult Function(UnauthorizedFailure value) unauthorized, + required TResult Function(ValidationFailure value) validation, + required TResult Function(NotFoundFailure value) notFound, + required TResult Function(CacheFailure value) cache, + required TResult Function(UnknownFailure value) unknown, + }) { + return server(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ServerFailure value)? server, + TResult? Function(NetworkFailure value)? network, + TResult? Function(UnauthorizedFailure value)? unauthorized, + TResult? Function(ValidationFailure value)? validation, + TResult? Function(NotFoundFailure value)? notFound, + TResult? Function(CacheFailure value)? cache, + TResult? Function(UnknownFailure value)? unknown, + }) { + return server?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ServerFailure value)? server, + TResult Function(NetworkFailure value)? network, + TResult Function(UnauthorizedFailure value)? unauthorized, + TResult Function(ValidationFailure value)? validation, + TResult Function(NotFoundFailure value)? notFound, + TResult Function(CacheFailure value)? cache, + TResult Function(UnknownFailure value)? unknown, + required TResult orElse(), + }) { + if (server != null) { + return server(this); + } + return orElse(); + } +} + +abstract class ServerFailure implements Failure { + const factory ServerFailure({ + required final String message, + final String? code, + final int? statusCode, + }) = _$ServerFailureImpl; + + @override + String get message; + String? get code; + int? get statusCode; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ServerFailureImplCopyWith<_$ServerFailureImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$NetworkFailureImplCopyWith<$Res> + implements $FailureCopyWith<$Res> { + factory _$$NetworkFailureImplCopyWith( + _$NetworkFailureImpl value, + $Res Function(_$NetworkFailureImpl) then, + ) = __$$NetworkFailureImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$NetworkFailureImplCopyWithImpl<$Res> + extends _$FailureCopyWithImpl<$Res, _$NetworkFailureImpl> + implements _$$NetworkFailureImplCopyWith<$Res> { + __$$NetworkFailureImplCopyWithImpl( + _$NetworkFailureImpl _value, + $Res Function(_$NetworkFailureImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? message = null}) { + return _then( + _$NetworkFailureImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$NetworkFailureImpl implements NetworkFailure { + const _$NetworkFailureImpl({this.message = 'No internet connection'}); + + @override + @JsonKey() + final String message; + + @override + String toString() { + return 'Failure.network(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NetworkFailureImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$NetworkFailureImplCopyWith<_$NetworkFailureImpl> get copyWith => + __$$NetworkFailureImplCopyWithImpl<_$NetworkFailureImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String message, String? code, int? statusCode) + server, + required TResult Function(String message) network, + required TResult Function(String message) unauthorized, + required TResult Function(String message, Map>? errors) + validation, + required TResult Function(String message) notFound, + required TResult Function(String message) cache, + required TResult Function(String message) unknown, + }) { + return network(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String message, String? code, int? statusCode)? server, + TResult? Function(String message)? network, + TResult? Function(String message)? unauthorized, + TResult? Function(String message, Map>? errors)? + validation, + TResult? Function(String message)? notFound, + TResult? Function(String message)? cache, + TResult? Function(String message)? unknown, + }) { + return network?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String message, String? code, int? statusCode)? server, + TResult Function(String message)? network, + TResult Function(String message)? unauthorized, + TResult Function(String message, Map>? errors)? + validation, + TResult Function(String message)? notFound, + TResult Function(String message)? cache, + TResult Function(String message)? unknown, + required TResult orElse(), + }) { + if (network != null) { + return network(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ServerFailure value) server, + required TResult Function(NetworkFailure value) network, + required TResult Function(UnauthorizedFailure value) unauthorized, + required TResult Function(ValidationFailure value) validation, + required TResult Function(NotFoundFailure value) notFound, + required TResult Function(CacheFailure value) cache, + required TResult Function(UnknownFailure value) unknown, + }) { + return network(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ServerFailure value)? server, + TResult? Function(NetworkFailure value)? network, + TResult? Function(UnauthorizedFailure value)? unauthorized, + TResult? Function(ValidationFailure value)? validation, + TResult? Function(NotFoundFailure value)? notFound, + TResult? Function(CacheFailure value)? cache, + TResult? Function(UnknownFailure value)? unknown, + }) { + return network?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ServerFailure value)? server, + TResult Function(NetworkFailure value)? network, + TResult Function(UnauthorizedFailure value)? unauthorized, + TResult Function(ValidationFailure value)? validation, + TResult Function(NotFoundFailure value)? notFound, + TResult Function(CacheFailure value)? cache, + TResult Function(UnknownFailure value)? unknown, + required TResult orElse(), + }) { + if (network != null) { + return network(this); + } + return orElse(); + } +} + +abstract class NetworkFailure implements Failure { + const factory NetworkFailure({final String message}) = _$NetworkFailureImpl; + + @override + String get message; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$NetworkFailureImplCopyWith<_$NetworkFailureImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$UnauthorizedFailureImplCopyWith<$Res> + implements $FailureCopyWith<$Res> { + factory _$$UnauthorizedFailureImplCopyWith( + _$UnauthorizedFailureImpl value, + $Res Function(_$UnauthorizedFailureImpl) then, + ) = __$$UnauthorizedFailureImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$UnauthorizedFailureImplCopyWithImpl<$Res> + extends _$FailureCopyWithImpl<$Res, _$UnauthorizedFailureImpl> + implements _$$UnauthorizedFailureImplCopyWith<$Res> { + __$$UnauthorizedFailureImplCopyWithImpl( + _$UnauthorizedFailureImpl _value, + $Res Function(_$UnauthorizedFailureImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? message = null}) { + return _then( + _$UnauthorizedFailureImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$UnauthorizedFailureImpl implements UnauthorizedFailure { + const _$UnauthorizedFailureImpl({ + this.message = 'Session expired. Please login again.', + }); + + @override + @JsonKey() + final String message; + + @override + String toString() { + return 'Failure.unauthorized(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UnauthorizedFailureImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UnauthorizedFailureImplCopyWith<_$UnauthorizedFailureImpl> get copyWith => + __$$UnauthorizedFailureImplCopyWithImpl<_$UnauthorizedFailureImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String message, String? code, int? statusCode) + server, + required TResult Function(String message) network, + required TResult Function(String message) unauthorized, + required TResult Function(String message, Map>? errors) + validation, + required TResult Function(String message) notFound, + required TResult Function(String message) cache, + required TResult Function(String message) unknown, + }) { + return unauthorized(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String message, String? code, int? statusCode)? server, + TResult? Function(String message)? network, + TResult? Function(String message)? unauthorized, + TResult? Function(String message, Map>? errors)? + validation, + TResult? Function(String message)? notFound, + TResult? Function(String message)? cache, + TResult? Function(String message)? unknown, + }) { + return unauthorized?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String message, String? code, int? statusCode)? server, + TResult Function(String message)? network, + TResult Function(String message)? unauthorized, + TResult Function(String message, Map>? errors)? + validation, + TResult Function(String message)? notFound, + TResult Function(String message)? cache, + TResult Function(String message)? unknown, + required TResult orElse(), + }) { + if (unauthorized != null) { + return unauthorized(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ServerFailure value) server, + required TResult Function(NetworkFailure value) network, + required TResult Function(UnauthorizedFailure value) unauthorized, + required TResult Function(ValidationFailure value) validation, + required TResult Function(NotFoundFailure value) notFound, + required TResult Function(CacheFailure value) cache, + required TResult Function(UnknownFailure value) unknown, + }) { + return unauthorized(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ServerFailure value)? server, + TResult? Function(NetworkFailure value)? network, + TResult? Function(UnauthorizedFailure value)? unauthorized, + TResult? Function(ValidationFailure value)? validation, + TResult? Function(NotFoundFailure value)? notFound, + TResult? Function(CacheFailure value)? cache, + TResult? Function(UnknownFailure value)? unknown, + }) { + return unauthorized?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ServerFailure value)? server, + TResult Function(NetworkFailure value)? network, + TResult Function(UnauthorizedFailure value)? unauthorized, + TResult Function(ValidationFailure value)? validation, + TResult Function(NotFoundFailure value)? notFound, + TResult Function(CacheFailure value)? cache, + TResult Function(UnknownFailure value)? unknown, + required TResult orElse(), + }) { + if (unauthorized != null) { + return unauthorized(this); + } + return orElse(); + } +} + +abstract class UnauthorizedFailure implements Failure { + const factory UnauthorizedFailure({final String message}) = + _$UnauthorizedFailureImpl; + + @override + String get message; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UnauthorizedFailureImplCopyWith<_$UnauthorizedFailureImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ValidationFailureImplCopyWith<$Res> + implements $FailureCopyWith<$Res> { + factory _$$ValidationFailureImplCopyWith( + _$ValidationFailureImpl value, + $Res Function(_$ValidationFailureImpl) then, + ) = __$$ValidationFailureImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message, Map>? errors}); +} + +/// @nodoc +class __$$ValidationFailureImplCopyWithImpl<$Res> + extends _$FailureCopyWithImpl<$Res, _$ValidationFailureImpl> + implements _$$ValidationFailureImplCopyWith<$Res> { + __$$ValidationFailureImplCopyWithImpl( + _$ValidationFailureImpl _value, + $Res Function(_$ValidationFailureImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? message = null, Object? errors = freezed}) { + return _then( + _$ValidationFailureImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + errors: freezed == errors + ? _value._errors + : errors // ignore: cast_nullable_to_non_nullable + as Map>?, + ), + ); + } +} + +/// @nodoc + +class _$ValidationFailureImpl implements ValidationFailure { + const _$ValidationFailureImpl({ + required this.message, + final Map>? errors, + }) : _errors = errors; + + @override + final String message; + final Map>? _errors; + @override + Map>? get errors { + final value = _errors; + if (value == null) return null; + if (_errors is EqualUnmodifiableMapView) return _errors; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + String toString() { + return 'Failure.validation(message: $message, errors: $errors)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ValidationFailureImpl && + (identical(other.message, message) || other.message == message) && + const DeepCollectionEquality().equals(other._errors, _errors)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + message, + const DeepCollectionEquality().hash(_errors), + ); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ValidationFailureImplCopyWith<_$ValidationFailureImpl> get copyWith => + __$$ValidationFailureImplCopyWithImpl<_$ValidationFailureImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String message, String? code, int? statusCode) + server, + required TResult Function(String message) network, + required TResult Function(String message) unauthorized, + required TResult Function(String message, Map>? errors) + validation, + required TResult Function(String message) notFound, + required TResult Function(String message) cache, + required TResult Function(String message) unknown, + }) { + return validation(message, errors); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String message, String? code, int? statusCode)? server, + TResult? Function(String message)? network, + TResult? Function(String message)? unauthorized, + TResult? Function(String message, Map>? errors)? + validation, + TResult? Function(String message)? notFound, + TResult? Function(String message)? cache, + TResult? Function(String message)? unknown, + }) { + return validation?.call(message, errors); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String message, String? code, int? statusCode)? server, + TResult Function(String message)? network, + TResult Function(String message)? unauthorized, + TResult Function(String message, Map>? errors)? + validation, + TResult Function(String message)? notFound, + TResult Function(String message)? cache, + TResult Function(String message)? unknown, + required TResult orElse(), + }) { + if (validation != null) { + return validation(message, errors); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ServerFailure value) server, + required TResult Function(NetworkFailure value) network, + required TResult Function(UnauthorizedFailure value) unauthorized, + required TResult Function(ValidationFailure value) validation, + required TResult Function(NotFoundFailure value) notFound, + required TResult Function(CacheFailure value) cache, + required TResult Function(UnknownFailure value) unknown, + }) { + return validation(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ServerFailure value)? server, + TResult? Function(NetworkFailure value)? network, + TResult? Function(UnauthorizedFailure value)? unauthorized, + TResult? Function(ValidationFailure value)? validation, + TResult? Function(NotFoundFailure value)? notFound, + TResult? Function(CacheFailure value)? cache, + TResult? Function(UnknownFailure value)? unknown, + }) { + return validation?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ServerFailure value)? server, + TResult Function(NetworkFailure value)? network, + TResult Function(UnauthorizedFailure value)? unauthorized, + TResult Function(ValidationFailure value)? validation, + TResult Function(NotFoundFailure value)? notFound, + TResult Function(CacheFailure value)? cache, + TResult Function(UnknownFailure value)? unknown, + required TResult orElse(), + }) { + if (validation != null) { + return validation(this); + } + return orElse(); + } +} + +abstract class ValidationFailure implements Failure { + const factory ValidationFailure({ + required final String message, + final Map>? errors, + }) = _$ValidationFailureImpl; + + @override + String get message; + Map>? get errors; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ValidationFailureImplCopyWith<_$ValidationFailureImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$NotFoundFailureImplCopyWith<$Res> + implements $FailureCopyWith<$Res> { + factory _$$NotFoundFailureImplCopyWith( + _$NotFoundFailureImpl value, + $Res Function(_$NotFoundFailureImpl) then, + ) = __$$NotFoundFailureImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$NotFoundFailureImplCopyWithImpl<$Res> + extends _$FailureCopyWithImpl<$Res, _$NotFoundFailureImpl> + implements _$$NotFoundFailureImplCopyWith<$Res> { + __$$NotFoundFailureImplCopyWithImpl( + _$NotFoundFailureImpl _value, + $Res Function(_$NotFoundFailureImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? message = null}) { + return _then( + _$NotFoundFailureImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$NotFoundFailureImpl implements NotFoundFailure { + const _$NotFoundFailureImpl({this.message = 'Resource not found'}); + + @override + @JsonKey() + final String message; + + @override + String toString() { + return 'Failure.notFound(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NotFoundFailureImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$NotFoundFailureImplCopyWith<_$NotFoundFailureImpl> get copyWith => + __$$NotFoundFailureImplCopyWithImpl<_$NotFoundFailureImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String message, String? code, int? statusCode) + server, + required TResult Function(String message) network, + required TResult Function(String message) unauthorized, + required TResult Function(String message, Map>? errors) + validation, + required TResult Function(String message) notFound, + required TResult Function(String message) cache, + required TResult Function(String message) unknown, + }) { + return notFound(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String message, String? code, int? statusCode)? server, + TResult? Function(String message)? network, + TResult? Function(String message)? unauthorized, + TResult? Function(String message, Map>? errors)? + validation, + TResult? Function(String message)? notFound, + TResult? Function(String message)? cache, + TResult? Function(String message)? unknown, + }) { + return notFound?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String message, String? code, int? statusCode)? server, + TResult Function(String message)? network, + TResult Function(String message)? unauthorized, + TResult Function(String message, Map>? errors)? + validation, + TResult Function(String message)? notFound, + TResult Function(String message)? cache, + TResult Function(String message)? unknown, + required TResult orElse(), + }) { + if (notFound != null) { + return notFound(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ServerFailure value) server, + required TResult Function(NetworkFailure value) network, + required TResult Function(UnauthorizedFailure value) unauthorized, + required TResult Function(ValidationFailure value) validation, + required TResult Function(NotFoundFailure value) notFound, + required TResult Function(CacheFailure value) cache, + required TResult Function(UnknownFailure value) unknown, + }) { + return notFound(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ServerFailure value)? server, + TResult? Function(NetworkFailure value)? network, + TResult? Function(UnauthorizedFailure value)? unauthorized, + TResult? Function(ValidationFailure value)? validation, + TResult? Function(NotFoundFailure value)? notFound, + TResult? Function(CacheFailure value)? cache, + TResult? Function(UnknownFailure value)? unknown, + }) { + return notFound?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ServerFailure value)? server, + TResult Function(NetworkFailure value)? network, + TResult Function(UnauthorizedFailure value)? unauthorized, + TResult Function(ValidationFailure value)? validation, + TResult Function(NotFoundFailure value)? notFound, + TResult Function(CacheFailure value)? cache, + TResult Function(UnknownFailure value)? unknown, + required TResult orElse(), + }) { + if (notFound != null) { + return notFound(this); + } + return orElse(); + } +} + +abstract class NotFoundFailure implements Failure { + const factory NotFoundFailure({final String message}) = _$NotFoundFailureImpl; + + @override + String get message; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$NotFoundFailureImplCopyWith<_$NotFoundFailureImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$CacheFailureImplCopyWith<$Res> + implements $FailureCopyWith<$Res> { + factory _$$CacheFailureImplCopyWith( + _$CacheFailureImpl value, + $Res Function(_$CacheFailureImpl) then, + ) = __$$CacheFailureImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$CacheFailureImplCopyWithImpl<$Res> + extends _$FailureCopyWithImpl<$Res, _$CacheFailureImpl> + implements _$$CacheFailureImplCopyWith<$Res> { + __$$CacheFailureImplCopyWithImpl( + _$CacheFailureImpl _value, + $Res Function(_$CacheFailureImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? message = null}) { + return _then( + _$CacheFailureImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$CacheFailureImpl implements CacheFailure { + const _$CacheFailureImpl({this.message = 'Cache error'}); + + @override + @JsonKey() + final String message; + + @override + String toString() { + return 'Failure.cache(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CacheFailureImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CacheFailureImplCopyWith<_$CacheFailureImpl> get copyWith => + __$$CacheFailureImplCopyWithImpl<_$CacheFailureImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String message, String? code, int? statusCode) + server, + required TResult Function(String message) network, + required TResult Function(String message) unauthorized, + required TResult Function(String message, Map>? errors) + validation, + required TResult Function(String message) notFound, + required TResult Function(String message) cache, + required TResult Function(String message) unknown, + }) { + return cache(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String message, String? code, int? statusCode)? server, + TResult? Function(String message)? network, + TResult? Function(String message)? unauthorized, + TResult? Function(String message, Map>? errors)? + validation, + TResult? Function(String message)? notFound, + TResult? Function(String message)? cache, + TResult? Function(String message)? unknown, + }) { + return cache?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String message, String? code, int? statusCode)? server, + TResult Function(String message)? network, + TResult Function(String message)? unauthorized, + TResult Function(String message, Map>? errors)? + validation, + TResult Function(String message)? notFound, + TResult Function(String message)? cache, + TResult Function(String message)? unknown, + required TResult orElse(), + }) { + if (cache != null) { + return cache(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ServerFailure value) server, + required TResult Function(NetworkFailure value) network, + required TResult Function(UnauthorizedFailure value) unauthorized, + required TResult Function(ValidationFailure value) validation, + required TResult Function(NotFoundFailure value) notFound, + required TResult Function(CacheFailure value) cache, + required TResult Function(UnknownFailure value) unknown, + }) { + return cache(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ServerFailure value)? server, + TResult? Function(NetworkFailure value)? network, + TResult? Function(UnauthorizedFailure value)? unauthorized, + TResult? Function(ValidationFailure value)? validation, + TResult? Function(NotFoundFailure value)? notFound, + TResult? Function(CacheFailure value)? cache, + TResult? Function(UnknownFailure value)? unknown, + }) { + return cache?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ServerFailure value)? server, + TResult Function(NetworkFailure value)? network, + TResult Function(UnauthorizedFailure value)? unauthorized, + TResult Function(ValidationFailure value)? validation, + TResult Function(NotFoundFailure value)? notFound, + TResult Function(CacheFailure value)? cache, + TResult Function(UnknownFailure value)? unknown, + required TResult orElse(), + }) { + if (cache != null) { + return cache(this); + } + return orElse(); + } +} + +abstract class CacheFailure implements Failure { + const factory CacheFailure({final String message}) = _$CacheFailureImpl; + + @override + String get message; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CacheFailureImplCopyWith<_$CacheFailureImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$UnknownFailureImplCopyWith<$Res> + implements $FailureCopyWith<$Res> { + factory _$$UnknownFailureImplCopyWith( + _$UnknownFailureImpl value, + $Res Function(_$UnknownFailureImpl) then, + ) = __$$UnknownFailureImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String message}); +} + +/// @nodoc +class __$$UnknownFailureImplCopyWithImpl<$Res> + extends _$FailureCopyWithImpl<$Res, _$UnknownFailureImpl> + implements _$$UnknownFailureImplCopyWith<$Res> { + __$$UnknownFailureImplCopyWithImpl( + _$UnknownFailureImpl _value, + $Res Function(_$UnknownFailureImpl) _then, + ) : super(_value, _then); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? message = null}) { + return _then( + _$UnknownFailureImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$UnknownFailureImpl implements UnknownFailure { + const _$UnknownFailureImpl({this.message = 'An unexpected error occurred'}); + + @override + @JsonKey() + final String message; + + @override + String toString() { + return 'Failure.unknown(message: $message)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UnknownFailureImpl && + (identical(other.message, message) || other.message == message)); + } + + @override + int get hashCode => Object.hash(runtimeType, message); + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UnknownFailureImplCopyWith<_$UnknownFailureImpl> get copyWith => + __$$UnknownFailureImplCopyWithImpl<_$UnknownFailureImpl>( + this, + _$identity, + ); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(String message, String? code, int? statusCode) + server, + required TResult Function(String message) network, + required TResult Function(String message) unauthorized, + required TResult Function(String message, Map>? errors) + validation, + required TResult Function(String message) notFound, + required TResult Function(String message) cache, + required TResult Function(String message) unknown, + }) { + return unknown(message); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(String message, String? code, int? statusCode)? server, + TResult? Function(String message)? network, + TResult? Function(String message)? unauthorized, + TResult? Function(String message, Map>? errors)? + validation, + TResult? Function(String message)? notFound, + TResult? Function(String message)? cache, + TResult? Function(String message)? unknown, + }) { + return unknown?.call(message); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(String message, String? code, int? statusCode)? server, + TResult Function(String message)? network, + TResult Function(String message)? unauthorized, + TResult Function(String message, Map>? errors)? + validation, + TResult Function(String message)? notFound, + TResult Function(String message)? cache, + TResult Function(String message)? unknown, + required TResult orElse(), + }) { + if (unknown != null) { + return unknown(message); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ServerFailure value) server, + required TResult Function(NetworkFailure value) network, + required TResult Function(UnauthorizedFailure value) unauthorized, + required TResult Function(ValidationFailure value) validation, + required TResult Function(NotFoundFailure value) notFound, + required TResult Function(CacheFailure value) cache, + required TResult Function(UnknownFailure value) unknown, + }) { + return unknown(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ServerFailure value)? server, + TResult? Function(NetworkFailure value)? network, + TResult? Function(UnauthorizedFailure value)? unauthorized, + TResult? Function(ValidationFailure value)? validation, + TResult? Function(NotFoundFailure value)? notFound, + TResult? Function(CacheFailure value)? cache, + TResult? Function(UnknownFailure value)? unknown, + }) { + return unknown?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ServerFailure value)? server, + TResult Function(NetworkFailure value)? network, + TResult Function(UnauthorizedFailure value)? unauthorized, + TResult Function(ValidationFailure value)? validation, + TResult Function(NotFoundFailure value)? notFound, + TResult Function(CacheFailure value)? cache, + TResult Function(UnknownFailure value)? unknown, + required TResult orElse(), + }) { + if (unknown != null) { + return unknown(this); + } + return orElse(); + } +} + +abstract class UnknownFailure implements Failure { + const factory UnknownFailure({final String message}) = _$UnknownFailureImpl; + + @override + String get message; + + /// Create a copy of Failure + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UnknownFailureImplCopyWith<_$UnknownFailureImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/core/network/api_envelope.dart b/lib/core/network/api_envelope.dart new file mode 100644 index 0000000..3f37d75 --- /dev/null +++ b/lib/core/network/api_envelope.dart @@ -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 data(Response> 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) return data; + if (data == null) { + throw const ServerException(message: 'Response missing data'); + } + throw ServerException( + message: 'Unexpected response data: ${data.runtimeType}', + ); + } + + static void ensureSuccess( + Map 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 data) { + final tokenSource = data['tokens'] is Map + ? data['tokens'] as Map + : 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(), + ); + } +} diff --git a/lib/core/network/api_handler.dart b/lib/core/network/api_handler.dart new file mode 100644 index 0000000..35831eb --- /dev/null +++ b/lib/core/network/api_handler.dart @@ -0,0 +1,107 @@ +import 'package:dio/dio.dart'; + +import '../errors/exceptions.dart'; +import '../errors/failure.dart'; + +typedef Result = ({T? data, Failure? failure}); + +Result handleDioError(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> safeApiCall(Future Function() call) async { + try { + final data = await call(); + return (data: data, failure: null); + } catch (e) { + return handleDioError(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'); +} diff --git a/lib/core/network/auth_interceptor.dart b/lib/core/network/auth_interceptor.dart new file mode 100644 index 0000000..7105e81 --- /dev/null +++ b/lib/core/network/auth_interceptor.dart @@ -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 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 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); + } +} diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart new file mode 100644 index 0000000..197a80a --- /dev/null +++ b/lib/core/network/dio_client.dart @@ -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((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; +}); diff --git a/lib/core/network/error_interceptor.dart b/lib/core/network/error_interceptor.dart new file mode 100644 index 0000000..6a343a1 --- /dev/null +++ b/lib/core/network/error_interceptor.dart @@ -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) { + 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 ? data['code'] as String? : null, + statusCode: statusCode, + ), + ), + ); + return; + default: + handler.next(err); + } + } + + String _messageFromBody(dynamic data, {required String fallback}) { + if (data is Map) { + final message = data['message'] as String?; + if (message != null && message.trim().isNotEmpty) return message.trim(); + } + return fallback; + } + + Map>? _parseErrors(dynamic errors) { + if (errors is List) { + final map = >{}; + 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) { + return errors.map( + (key, value) => MapEntry( + key, + value is List + ? value.map((e) => e.toString()).toList() + : [value.toString()], + ), + ); + } + + return null; + } +} diff --git a/lib/core/network/token_refresh_service.dart b/lib/core/network/token_refresh_service.dart new file mode 100644 index 0000000..80eb7e7 --- /dev/null +++ b/lib/core/network/token_refresh_service.dart @@ -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((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? _inFlight; + + Future refresh() { + _inFlight ??= _refresh(); + return _inFlight!; + } + + Future _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>( + 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; + } + } +} diff --git a/lib/core/network/token_storage.dart b/lib/core/network/token_storage.dart new file mode 100644 index 0000000..d70c14f --- /dev/null +++ b/lib/core/network/token_storage.dart @@ -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((ref) { + return const FlutterSecureStorage( + aOptions: AndroidOptions(encryptedSharedPreferences: true), + iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), + ); +}); + +final tokenStorageProvider = Provider((ref) { + return TokenStorage(ref.watch(secureStorageProvider)); +}); + +class TokenStorage { + TokenStorage(this._storage); + + final FlutterSecureStorage _storage; + + Future getAccessToken() => _storage.read(key: StorageKeys.accessToken); + + Future getRefreshToken() => _storage.read(key: StorageKeys.refreshToken); + + Future 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 clearTokens() async { + await _storage.delete(key: StorageKeys.accessToken); + await _storage.delete(key: StorageKeys.refreshToken); + } + + Future getCompanyId() => _storage.read(key: StorageKeys.companyId); + + Future saveCompanyId(String companyId) => + _storage.write(key: StorageKeys.companyId, value: companyId); + + Future clearCompanyId() => _storage.delete(key: StorageKeys.companyId); + + Future getBranchId() => _storage.read(key: StorageKeys.branchId); + + Future saveBranchId(String branchId) => + _storage.write(key: StorageKeys.branchId, value: branchId); + + Future clearBranchId() => _storage.delete(key: StorageKeys.branchId); +} diff --git a/lib/core/services/permission_matrix_api_parser.dart b/lib/core/services/permission_matrix_api_parser.dart new file mode 100644 index 0000000..2c76750 --- /dev/null +++ b/lib/core/services/permission_matrix_api_parser.dart @@ -0,0 +1,61 @@ +/// Parses live API permission-matrix payloads from `GET /roles/{id}/permission-matrix`. +class PermissionMatrixApiParser { + PermissionMatrixApiParser._(); + + static List toPermissionKeys(Map data) { + final modules = data['modules'] as List? ?? const []; + final permissions = {}; + + for (final raw in modules) { + if (raw is! Map) continue; + final moduleCode = (raw['code'] as String?)?.trim(); + if (moduleCode == null || moduleCode.isEmpty) continue; + + final modulePerms = raw['permissions'] as Map? ?? 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 _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 [], + }; + } +} diff --git a/lib/core/services/permission_resolver.dart b/lib/core/services/permission_resolver.dart new file mode 100644 index 0000000..890cdce --- /dev/null +++ b/lib/core/services/permission_resolver.dart @@ -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 fromMatrix(PermissionMatrixModel matrix) { + final permissions = []; + for (final row in matrix.matrix) { + permissions.addAll(fromRow(row.module, row.actions)); + } + return permissions; + } + + static List fromRow(String module, PermissionMatrixActions actions) { + final moduleKey = module.trim().toLowerCase(); + final perms = []; + + 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; + } +} diff --git a/lib/core/theme/app_colors.dart b/lib/core/theme/app_colors.dart new file mode 100644 index 0000000..b2fce5d --- /dev/null +++ b/lib/core/theme/app_colors.dart @@ -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); +} diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart new file mode 100644 index 0000000..d6ac62b --- /dev/null +++ b/lib/core/theme/app_theme.dart @@ -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, + ), + ); + } +} diff --git a/lib/core/theme/app_typography.dart b/lib/core/theme/app_typography.dart new file mode 100644 index 0000000..53811f5 --- /dev/null +++ b/lib/core/theme/app_typography.dart @@ -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), + ); + } +} diff --git a/lib/core/theme/branding_config.dart b/lib/core/theme/branding_config.dart new file mode 100644 index 0000000..bbd07b2 --- /dev/null +++ b/lib/core/theme/branding_config.dart @@ -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 json) => + _$BrandingConfigFromJson(json); +} + +extension BrandingConfigX on BrandingConfig { + Color get primaryColor => Color(primaryColorValue); + Color get secondaryColor => Color(secondaryColorValue); +} diff --git a/lib/core/theme/branding_config.freezed.dart b/lib/core/theme/branding_config.freezed.dart new file mode 100644 index 0000000..506745b --- /dev/null +++ b/lib/core/theme/branding_config.freezed.dart @@ -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 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 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 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 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 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 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 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; +} diff --git a/lib/core/theme/branding_config.g.dart b/lib/core/theme/branding_config.g.dart new file mode 100644 index 0000000..119e4cd --- /dev/null +++ b/lib/core/theme/branding_config.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'branding_config.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$BrandingConfigImpl _$$BrandingConfigImplFromJson(Map 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 _$$BrandingConfigImplToJson( + _$BrandingConfigImpl instance, +) => { + 'logoUrl': instance.logoUrl, + 'primaryColorValue': instance.primaryColorValue, + 'secondaryColorValue': instance.secondaryColorValue, + 'companyName': instance.companyName, +}; diff --git a/lib/core/theme/theme_provider.dart b/lib/core/theme/theme_provider.dart new file mode 100644 index 0000000..c67fed7 --- /dev/null +++ b/lib/core/theme/theme_provider.dart @@ -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((ref) { + throw UnimplementedError('SharedPreferences must be overridden in main.dart'); +}); + +final themeModeProvider = StateNotifierProvider((ref) { + return ThemeModeNotifier(ref.watch(sharedPreferencesProvider)); +}); + +final brandingProvider = StateNotifierProvider((ref) { + return BrandingNotifier(ref.watch(sharedPreferencesProvider)); +}); + +class ThemeModeNotifier extends StateNotifier { + 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 setThemeMode(ThemeModeOption mode) async { + state = mode; + await _prefs.setString(StorageKeys.themeMode, mode.value); + } +} + +class BrandingNotifier extends StateNotifier { + 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 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); diff --git a/lib/core/utils/formatters.dart b/lib/core/utils/formatters.dart new file mode 100644 index 0000000..a8927f3 --- /dev/null +++ b/lib/core/utils/formatters.dart @@ -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); + } +} diff --git a/lib/core/utils/jwt_utils.dart b/lib/core/utils/jwt_utils.dart new file mode 100644 index 0000000..17a3870 --- /dev/null +++ b/lib/core/utils/jwt_utils.dart @@ -0,0 +1,31 @@ +import 'dart:convert'; + +/// Lightweight JWT payload decoder (no signature verification on client). +class JwtUtils { + JwtUtils._(); + + static Map 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) { + 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(); + } +} diff --git a/lib/core/utils/permission_utils.dart b/lib/core/utils/permission_utils.dart new file mode 100644 index 0000000..d599c59 --- /dev/null +++ b/lib/core/utils/permission_utils.dart @@ -0,0 +1,38 @@ +import '../errors/failure.dart'; +import '../network/api_handler.dart'; +import '../../core/constants/enums.dart'; + +bool hasPermission({ + required List 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); diff --git a/lib/core/utils/responsive_utils.dart b/lib/core/utils/responsive_utils.dart new file mode 100644 index 0000000..1bf34a2 --- /dev/null +++ b/lib/core/utils/responsive_utils.dart @@ -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; + } +} diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart new file mode 100644 index 0000000..d3b3f7c --- /dev/null +++ b/lib/core/utils/validators.dart @@ -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; + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..9a22c41 --- /dev/null +++ b/lib/main.dart @@ -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 main() async { + WidgetsFlutterBinding.ensureInitialized(); + await dotenv.load(fileName: AppEnv.envFileName); + final prefs = await SharedPreferences.getInstance(); + + runApp( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + ], + child: const BharatErpApp(), + ), + ); +} diff --git a/lib/modules/assets/data/repositories/asset_repository_impl.dart b/lib/modules/assets/data/repositories/asset_repository_impl.dart new file mode 100644 index 0000000..0720ce1 --- /dev/null +++ b/lib/modules/assets/data/repositories/asset_repository_impl.dart @@ -0,0 +1,129 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../../../core/network/api_handler.dart'; +import '../../../../core/network/dio_client.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/asset_model.dart'; +import '../../domain/repositories/asset_repository.dart'; + +final assetRepositoryProvider = Provider((ref) { + return AssetRepositoryImpl(dio: ref.watch(dioProvider)); +}); + +class AssetRepositoryImpl implements AssetRepository { + AssetRepositoryImpl({required this.dio}); + + final Dio dio; + + @override + Future>> getAssets(PaginationParams params) async { + return safeApiCall(() async { + final response = await dio.get(ApiEndpoints.assets, queryParameters: { + 'page': params.page, + 'limit': params.limit, + if (params.search != null) 'search': params.search, + }); + return PaginatedResponse.fromJson( + response.data['data'] as Map, + (json) => AssetModel.fromJson(json! as Map), + ); + }); + } + + @override + Future> getAssetById(String id) async { + return safeApiCall(() async { + final response = await dio.get(ApiEndpoints.assetById(id)); + return AssetModel.fromJson(response.data['data'] as Map); + }); + } + + @override + Future> createAsset(Map data) async { + return safeApiCall(() async { + final response = await dio.post(ApiEndpoints.assets, data: data); + return AssetModel.fromJson(response.data['data'] as Map); + }); + } + + @override + Future> updateAsset(String id, Map data) async { + return safeApiCall(() async { + final response = await dio.put(ApiEndpoints.assetById(id), data: data); + return AssetModel.fromJson(response.data['data'] as Map); + }); + } + + @override + Future>> getCategories() async { + return safeApiCall(() async { + final response = await dio.get(ApiEndpoints.assetCategories); + final rawData = response.data['data']; + final list = rawData is List + ? rawData + : (rawData is Map ? rawData['items'] as List? : null) ?? + []; + return list + .map((e) => AssetCategoryModel.fromJson(e as Map)) + .toList(); + }); + } + + @override + Future>> getAllocations( + PaginationParams params, + ) async { + return safeApiCall(() async { + final response = await dio.get(ApiEndpoints.assetAllocations, queryParameters: { + 'page': params.page, + 'limit': params.limit, + }); + return PaginatedResponse.fromJson( + response.data['data'] as Map, + (json) => AssetAllocationModel.fromJson(json! as Map), + ); + }); + } + + @override + Future>> getMaintenance( + PaginationParams params, + ) async { + return safeApiCall(() async { + final response = await dio.get(ApiEndpoints.assetMaintenance, queryParameters: { + 'page': params.page, + 'limit': params.limit, + }); + return PaginatedResponse.fromJson( + response.data['data'] as Map, + (json) => AssetMaintenanceModel.fromJson(json! as Map), + ); + }); + } + + @override + Future>> getDisposals( + PaginationParams params, + ) async { + return safeApiCall(() async { + final response = await dio.get(ApiEndpoints.assetDisposal, queryParameters: { + 'page': params.page, + 'limit': params.limit, + }); + return PaginatedResponse.fromJson( + response.data['data'] as Map, + (json) => AssetDisposalModel.fromJson(json! as Map), + ); + }); + } + + @override + Future> searchByQrCode(String code) async { + return safeApiCall(() async { + final response = await dio.get(ApiEndpoints.assetSearch, queryParameters: {'q': code}); + return AssetModel.fromJson(response.data['data'] as Map); + }); + } +} diff --git a/lib/modules/assets/domain/repositories/asset_repository.dart b/lib/modules/assets/domain/repositories/asset_repository.dart new file mode 100644 index 0000000..a7089b5 --- /dev/null +++ b/lib/modules/assets/domain/repositories/asset_repository.dart @@ -0,0 +1,16 @@ +import '../../../../core/errors/failure.dart'; +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/asset_model.dart'; + +abstract class AssetRepository { + Future>> getAssets(PaginationParams params); + Future> getAssetById(String id); + Future> createAsset(Map data); + Future> updateAsset(String id, Map data); + Future>> getCategories(); + Future>> getAllocations(PaginationParams params); + Future>> getMaintenance(PaginationParams params); + Future>> getDisposals(PaginationParams params); + Future> searchByQrCode(String code); +} diff --git a/lib/modules/assets/presentation/providers/asset_categories_provider.dart b/lib/modules/assets/presentation/providers/asset_categories_provider.dart new file mode 100644 index 0000000..a203253 --- /dev/null +++ b/lib/modules/assets/presentation/providers/asset_categories_provider.dart @@ -0,0 +1,11 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../data/repositories/asset_repository_impl.dart'; +import '../../../../shared/models/asset_model.dart'; + +final assetCategoriesProvider = FutureProvider>((ref) async { + final repository = ref.watch(assetRepositoryProvider); + final result = await repository.getCategories(); + if (result.failure != null) throw result.failure!; + return result.data ?? []; +}); diff --git a/lib/modules/assets/presentation/screens/asset_allocations_screen.dart b/lib/modules/assets/presentation/screens/asset_allocations_screen.dart new file mode 100644 index 0000000..cb2d719 --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_allocations_screen.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class AssetAllocationsScreen extends StatelessWidget { + const AssetAllocationsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Asset Allocations', + subtitle: 'Assign, return, reassign, and transfer assets', + ), + Expanded( + child: EmptyStateView( + title: 'No allocations', + description: 'Assign assets to employees and track allocation history.', + icon: Icons.assignment_ind_outlined, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_categories_screen.dart b/lib/modules/assets/presentation/screens/asset_categories_screen.dart new file mode 100644 index 0000000..4a523f8 --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_categories_screen.dart @@ -0,0 +1,55 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../providers/asset_categories_provider.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class AssetCategoriesScreen extends ConsumerWidget { + const AssetCategoriesScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final categories = ref.watch(assetCategoriesProvider); + + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PageHeader( + title: 'Asset Categories', + subtitle: 'Predefined and custom categories', + ), + Expanded( + child: categories.when( + data: (items) { + if (items.isEmpty) { + return const Center(child: Text('No categories found')); + } + return ListView.separated( + itemCount: items.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final category = items[index]; + return AppCard( + child: ListTile( + leading: const Icon(Icons.category_outlined), + title: Text(category.name), + subtitle: Text(category.slug), + ), + ); + }, + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (_, __) => const Center( + child: Text('Failed to load categories from API'), + ), + ), + ) + ], + ), + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_detail_screen.dart b/lib/modules/assets/presentation/screens/asset_detail_screen.dart new file mode 100644 index 0000000..bd51651 --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_detail_screen.dart @@ -0,0 +1,96 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +import '../../../../shared/widgets/page_header.dart'; + +class AssetDetailScreen extends StatelessWidget { + const AssetDetailScreen({super.key, required this.assetId}); + + final String assetId; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Asset Details', + subtitle: 'ID: $assetId', + ), + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 2, + child: AppCard( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _DetailRow(label: 'Asset Name', value: '—'), + _DetailRow(label: 'Asset Code', value: '—'), + _DetailRow(label: 'Category', value: '—'), + _DetailRow(label: 'Serial Number', value: '—'), + _DetailRow(label: 'Status', value: '—'), + _DetailRow(label: 'Branch', value: '—'), + ], + ), + ), + ), + ), + const SizedBox(width: 24), + AppCard( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + const Text('QR Code'), + const SizedBox(height: 16), + QrImageView( + data: assetId, + version: QrVersions.auto, + size: 160, + ), + ], + ), + ), + ), + ], + ), + ], + ), + ); + } +} + +class _DetailRow extends StatelessWidget { + const _DetailRow({required this.label, required this.value}); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + children: [ + SizedBox( + width: 140, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded(child: Text(value, style: Theme.of(context).textTheme.bodyLarge)), + ], + ), + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_disposal_screen.dart b/lib/modules/assets/presentation/screens/asset_disposal_screen.dart new file mode 100644 index 0000000..1bb5efa --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_disposal_screen.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class AssetDisposalScreen extends StatelessWidget { + const AssetDisposalScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Asset Disposal', + subtitle: 'Disposal requests and approvals', + ), + Expanded( + child: EmptyStateView( + title: 'No disposal requests', + description: 'Submit disposal requests with reason and approval workflow.', + icon: Icons.delete_outline, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_form_screen.dart b/lib/modules/assets/presentation/screens/asset_form_screen.dart new file mode 100644 index 0000000..eb60504 --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_form_screen.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; + +class AssetFormScreen extends StatefulWidget { + const AssetFormScreen({super.key, this.assetId}); + + final String? assetId; + + @override + State createState() => _AssetFormScreenState(); +} + +class _AssetFormScreenState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _codeController = TextEditingController(); + final _brandController = TextEditingController(); + final _modelController = TextEditingController(); + final _serialController = TextEditingController(); + final _vendorController = TextEditingController(); + final _costController = TextEditingController(); + AssetCategoryType _category = AssetCategoryType.laptop; + AssetStatus _status = AssetStatus.available; + + bool get isEditing => widget.assetId != null; + + @override + void dispose() { + _nameController.dispose(); + _codeController.dispose(); + _brandController.dispose(); + _modelController.dispose(); + _serialController.dispose(); + _vendorController.dispose(); + _costController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(isEditing ? 'Edit Asset' : 'Add Asset')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _nameController, + label: 'Asset Name', + validator: (v) => Validators.required(v, fieldName: 'Asset name'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _codeController, + label: 'Asset Code', + validator: (v) => Validators.required(v, fieldName: 'Asset code'), + ), + const SizedBox(height: 16), + DropdownButtonFormField( + value: _category, + decoration: const InputDecoration(labelText: 'Category'), + items: AssetCategoryType.values + .map((c) => DropdownMenuItem(value: c, child: Text(c.label))) + .toList(), + onChanged: (v) => setState(() => _category = v!), + ), + const SizedBox(height: 16), + AppTextField(controller: _brandController, label: 'Brand'), + const SizedBox(height: 16), + AppTextField(controller: _modelController, label: 'Model'), + const SizedBox(height: 16), + AppTextField(controller: _serialController, label: 'Serial Number'), + const SizedBox(height: 16), + AppTextField( + controller: _costController, + label: 'Purchase Cost', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + AppTextField(controller: _vendorController, label: 'Vendor'), + const SizedBox(height: 16), + DropdownButtonFormField( + value: _status, + decoration: const InputDecoration(labelText: 'Status'), + items: AssetStatus.values + .map((s) => DropdownMenuItem(value: s, child: Text(s.label))) + .toList(), + onChanged: (v) => setState(() => _status = v!), + ), + const SizedBox(height: 24), + AppButton( + label: isEditing ? 'Update Asset' : 'Create Asset', + onPressed: () { + if (_formKey.currentState!.validate()) Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_list_screen.dart b/lib/modules/assets/presentation/screens/asset_list_screen.dart new file mode 100644 index 0000000..dd5a6d8 --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_list_screen.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class AssetListScreen extends StatelessWidget { + const AssetListScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Asset Master', + subtitle: 'Manage all company assets', + actions: [ + OutlinedButton.icon( + onPressed: () => context.go(RouteConstants.assetQrScan), + icon: const Icon(Icons.qr_code_scanner), + label: const Text('QR Scan'), + ), + const SizedBox(width: 8), + ElevatedButton.icon( + onPressed: () => context.push('${RouteConstants.assets}/add'), + icon: const Icon(Icons.add), + label: const Text('Add Asset'), + ), + ], + ), + const Expanded( + child: EmptyStateView( + title: 'No assets yet', + description: 'Add assets to start tracking laptops, devices, furniture, and more.', + icon: Icons.inventory_2_outlined, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_maintenance_screen.dart b/lib/modules/assets/presentation/screens/asset_maintenance_screen.dart new file mode 100644 index 0000000..744ca48 --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_maintenance_screen.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; + +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class AssetMaintenanceScreen extends StatelessWidget { + const AssetMaintenanceScreen({super.key}); + + @override + Widget build(BuildContext context) { + return const Padding( + padding: EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Asset Maintenance', + subtitle: 'Track repairs, service vendors, and costs', + ), + Expanded( + child: EmptyStateView( + title: 'No maintenance requests', + description: 'Create maintenance requests and view service history.', + icon: Icons.build_outlined, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_qr_generate_screen.dart b/lib/modules/assets/presentation/screens/asset_qr_generate_screen.dart new file mode 100644 index 0000000..3537c38 --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_qr_generate_screen.dart @@ -0,0 +1,53 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; +import 'package:qr_flutter/qr_flutter.dart'; + +import '../../../../shared/widgets/page_header.dart'; + +class AssetQrGenerateScreen extends StatelessWidget { + const AssetQrGenerateScreen({super.key}); + + @override + Widget build(BuildContext context) { + const mockData = '{"assetId":"A1001","assetCode":"LTP-0001","serialNumber":"SN-ERP-8892"}'; + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PageHeader( + title: 'Generate & Print QR', + subtitle: 'Generate QR codes for asset identification', + ), + const SizedBox(height: 16), + Expanded( + child: AppCard( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + QrImageView(data: mockData, size: 220), + const SizedBox(height: 12), + const Text('Asset Code: LTP-0001'), + const SizedBox(height: 16), + FilledButton.icon( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Print command hooked for platform print service.'), + ), + ); + }, + icon: const Icon(Icons.print_outlined), + label: const Text('Print QR'), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_qr_scan_screen.dart b/lib/modules/assets/presentation/screens/asset_qr_scan_screen.dart new file mode 100644 index 0000000..cfa7217 --- /dev/null +++ b/lib/modules/assets/presentation/screens/asset_qr_scan_screen.dart @@ -0,0 +1,47 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; + +import '../../../../shared/widgets/page_header.dart'; + +class AssetQrScanScreen extends StatelessWidget { + const AssetQrScanScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PageHeader( + title: 'QR Scan', + subtitle: 'Scan asset QR code to view details', + ), + Expanded( + child: AppCard( + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.qr_code_scanner, + size: 80, + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + const Text('Camera scanner requires a physical device'), + const SizedBox(height: 8), + Text( + 'Use mobile_scanner on Android/iOS', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/audit/presentation/screens/audit_logs_screen.dart b/lib/modules/audit/presentation/screens/audit_logs_screen.dart new file mode 100644 index 0000000..238b05e --- /dev/null +++ b/lib/modules/audit/presentation/screens/audit_logs_screen.dart @@ -0,0 +1,6 @@ +import '../../../../shared/widgets/placeholder_screen.dart'; + +class AuditLogsScreen extends PlaceholderScreen { + const AuditLogsScreen({super.key}) + : super(title: 'Audit Logs', description: 'System activity and audit trail'); +} diff --git a/lib/modules/auth/data/datasources/auth_remote_data_source.dart b/lib/modules/auth/data/datasources/auth_remote_data_source.dart new file mode 100644 index 0000000..e9ec9ca --- /dev/null +++ b/lib/modules/auth/data/datasources/auth_remote_data_source.dart @@ -0,0 +1,158 @@ +import 'package:dio/dio.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../../../core/network/api_envelope.dart'; +import '../../../../core/services/permission_matrix_api_parser.dart'; +import '../../../../core/utils/jwt_utils.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/models/user_model.dart'; + +class AuthRemoteDataSource { + AuthRemoteDataSource({required this.dio}); + + final Dio dio; + + Future login(LoginRequest request) async { + final response = await dio.post>( + ApiEndpoints.login, + data: { + 'email': request.email.trim(), + 'password': request.password, + if (request.companyCode != null) 'company_code': request.companyCode, + }, + ); + final data = ApiEnvelope.data(response); + final tokens = ApiEnvelope.parseTokens(data); + return LoginResponse( + tokens: tokens, + user: _placeholderUser(request.email), + ); + } + + Future refreshTokens(String refreshToken) async { + final response = await dio.post>( + ApiEndpoints.refreshToken, + data: {'refresh_token': refreshToken}, + ); + return ApiEnvelope.parseTokens(ApiEnvelope.data(response)); + } + + Future logout({required String refreshToken}) async { + final response = await dio.post>( + ApiEndpoints.logout, + data: {'refresh_token': refreshToken}, + ); + ApiEnvelope.ensureSuccess( + response.data ?? const {}, + statusCode: response.statusCode, + ); + } + + Future resolveCurrentUser(String accessToken) async { + final userId = JwtUtils.subject(accessToken); + if (userId == null || userId.isEmpty) { + throw const FormatException('JWT missing subject (sub)'); + } + + final roleId = JwtUtils.roleId(accessToken); + final profile = await _fetchUserProfile(userId); + final permissions = roleId == null + ? const [] + : await _fetchRolePermissions(roleId); + + return _mapProfileToUser(profile, permissions: permissions, roleId: roleId); + } + + Future getCurrentUser() async { + final response = await dio.get>(ApiEndpoints.me); + final data = ApiEnvelope.data(response); + return UserModel.fromLoginJson(data); + } + + Future forgotPassword(ForgotPasswordRequest request) async { + final response = await dio.post>( + ApiEndpoints.forgotPassword, + data: request.toJson(), + ); + ApiEnvelope.ensureSuccess( + response.data ?? const {}, + statusCode: response.statusCode, + ); + } + + Future verifyOtp(OtpVerifyRequest request) async { + final response = await dio.post>( + ApiEndpoints.verifyOtp, + data: request.toJson(), + ); + final data = ApiEnvelope.data(response); + final tokens = ApiEnvelope.parseTokens(data); + return LoginResponse( + tokens: tokens, + user: _placeholderUser(request.email), + ); + } + + Future changePassword(ChangePasswordRequest request) async { + final response = await dio.post>( + ApiEndpoints.changePassword, + data: request.toJson(), + ); + ApiEnvelope.ensureSuccess( + response.data ?? const {}, + statusCode: response.statusCode, + ); + } + + Future _fetchUserProfile(String userId) async { + final response = await dio.get>( + ApiEndpoints.userById(userId), + ); + return ManagedUserModel.fromJson(ApiEnvelope.data(response)); + } + + Future> _fetchRolePermissions(String roleId) async { + try { + final response = await dio.get>( + ApiEndpoints.rolePermissionMatrix(roleId), + ); + return PermissionMatrixApiParser.toPermissionKeys( + ApiEnvelope.data(response), + ); + } catch (_) { + return const []; + } + } + + UserModel _placeholderUser(String email) { + return UserModel( + id: '', + employeeId: '', + name: '', + email: email, + mobile: '', + role: 'employee', + ); + } + + UserModel _mapProfileToUser( + ManagedUserModel profile, { + required List permissions, + String? roleId, + }) { + return UserModel( + id: profile.id, + employeeId: profile.employeeCode, + name: profile.fullName, + email: profile.email, + mobile: profile.mobile, + role: profile.roleName ?? roleId ?? 'employee', + department: profile.departmentName, + status: profile.status, + permissions: permissions, + avatarUrl: profile.avatarUrl, + createdAt: profile.createdAt, + updatedAt: profile.updatedAt, + ); + } +} diff --git a/lib/modules/auth/data/repositories/auth_repository_impl.dart b/lib/modules/auth/data/repositories/auth_repository_impl.dart new file mode 100644 index 0000000..68b8a47 --- /dev/null +++ b/lib/modules/auth/data/repositories/auth_repository_impl.dart @@ -0,0 +1,121 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/network/api_handler.dart'; +import '../../../../core/network/dio_client.dart'; +import '../../../../core/network/token_storage.dart'; +import '../../../../shared/models/user_model.dart'; +import '../../domain/repositories/auth_repository.dart'; +import '../datasources/auth_remote_data_source.dart'; + +final authRemoteDataSourceProvider = Provider((ref) { + return AuthRemoteDataSource(dio: ref.watch(dioProvider)); +}); + +final authRepositoryProvider = Provider((ref) { + return AuthRepositoryImpl( + remote: ref.watch(authRemoteDataSourceProvider), + tokenStorage: ref.watch(tokenStorageProvider), + ); +}); + +class AuthRepositoryImpl implements AuthRepository { + AuthRepositoryImpl({ + required this.remote, + required this.tokenStorage, + }); + + final AuthRemoteDataSource remote; + final TokenStorage tokenStorage; + + @override + Future> login(LoginRequest request) async { + return safeApiCall(() async { + final loginResponse = await remote.login(request); + await _persistSession(loginResponse); + final user = await remote.resolveCurrentUser(loginResponse.tokens.accessToken); + return loginResponse.copyWith(user: user); + }); + } + + @override + Future> logout() async { + return safeApiCall(() async { + final refreshToken = await tokenStorage.getRefreshToken(); + try { + if (refreshToken != null && refreshToken.isNotEmpty) { + await remote.logout(refreshToken: refreshToken); + } + } finally { + await tokenStorage.clearTokens(); + await tokenStorage.clearCompanyId(); + await tokenStorage.clearBranchId(); + } + }); + } + + @override + Future> getCurrentUser() async { + return safeApiCall(() async { + final accessToken = await tokenStorage.getAccessToken(); + if (accessToken == null || accessToken.isEmpty) { + throw const FormatException('No access token'); + } + return remote.resolveCurrentUser(accessToken); + }); + } + + @override + Future> refreshSession() async { + return safeApiCall(() async { + final refreshToken = await tokenStorage.getRefreshToken(); + if (refreshToken == null || refreshToken.isEmpty) { + throw const FormatException('No refresh token'); + } + final tokens = await remote.refreshTokens(refreshToken); + await tokenStorage.saveTokens( + accessToken: tokens.accessToken, + refreshToken: tokens.refreshToken, + ); + return tokens; + }); + } + + @override + Future> forgotPassword(ForgotPasswordRequest request) async { + return safeApiCall(() => remote.forgotPassword(request)); + } + + @override + Future> verifyOtp(OtpVerifyRequest request) async { + return safeApiCall(() async { + final loginResponse = await remote.verifyOtp(request); + await _persistSession(loginResponse); + final user = await remote.resolveCurrentUser(loginResponse.tokens.accessToken); + return loginResponse.copyWith(user: user); + }); + } + + @override + Future> changePassword(ChangePasswordRequest request) async { + return safeApiCall(() => remote.changePassword(request)); + } + + @override + Future isAuthenticated() async { + final token = await tokenStorage.getAccessToken(); + return token != null && token.isNotEmpty; + } + + Future _persistSession(LoginResponse loginResponse) async { + await tokenStorage.saveTokens( + accessToken: loginResponse.tokens.accessToken, + refreshToken: loginResponse.tokens.refreshToken, + ); + if (loginResponse.user.companyId != null) { + await tokenStorage.saveCompanyId(loginResponse.user.companyId!); + } + if (loginResponse.user.branchId != null) { + await tokenStorage.saveBranchId(loginResponse.user.branchId!); + } + } +} diff --git a/lib/modules/auth/domain/repositories/auth_repository.dart b/lib/modules/auth/domain/repositories/auth_repository.dart new file mode 100644 index 0000000..79a9c0e --- /dev/null +++ b/lib/modules/auth/domain/repositories/auth_repository.dart @@ -0,0 +1,13 @@ +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/user_model.dart'; + +abstract class AuthRepository { + Future> login(LoginRequest request); + Future> logout(); + Future> getCurrentUser(); + Future> refreshSession(); + Future> forgotPassword(ForgotPasswordRequest request); + Future> verifyOtp(OtpVerifyRequest request); + Future> changePassword(ChangePasswordRequest request); + Future isAuthenticated(); +} diff --git a/lib/modules/auth/presentation/screens/change_password_screen.dart b/lib/modules/auth/presentation/screens/change_password_screen.dart new file mode 100644 index 0000000..45b38bd --- /dev/null +++ b/lib/modules/auth/presentation/screens/change_password_screen.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/utils/validators.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; + +class ChangePasswordScreen extends StatefulWidget { + const ChangePasswordScreen({super.key}); + + @override + State createState() => _ChangePasswordScreenState(); +} + +class _ChangePasswordScreenState extends State { + final _formKey = GlobalKey(); + final _currentController = TextEditingController(); + final _newController = TextEditingController(); + final _confirmController = TextEditingController(); + + @override + void dispose() { + _currentController.dispose(); + _newController.dispose(); + _confirmController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Change Password')), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _currentController, + label: 'Current Password', + obscureText: true, + validator: (v) => Validators.required(v, fieldName: 'Current password'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _newController, + label: 'New Password', + obscureText: true, + validator: Validators.password, + ), + const SizedBox(height: 16), + AppTextField( + controller: _confirmController, + label: 'Confirm Password', + obscureText: true, + validator: (v) { + if (v != _newController.text) return 'Passwords do not match'; + return Validators.password(v); + }, + ), + const SizedBox(height: 24), + AppButton( + label: 'Update Password', + onPressed: () { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Password updated (API pending)')), + ); + Navigator.of(context).pop(); + } + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/auth/presentation/screens/forgot_password_screen.dart b/lib/modules/auth/presentation/screens/forgot_password_screen.dart new file mode 100644 index 0000000..c80a7d7 --- /dev/null +++ b/lib/modules/auth/presentation/screens/forgot_password_screen.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../auth/data/repositories/auth_repository_impl.dart'; +import '../../../../shared/models/user_model.dart'; + +class ForgotPasswordScreen extends ConsumerStatefulWidget { + const ForgotPasswordScreen({super.key}); + + @override + ConsumerState createState() => _ForgotPasswordScreenState(); +} + +class _ForgotPasswordScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + bool _isLoading = false; + String? _successMessage; + String? _errorMessage; + + @override + void dispose() { + _emailController.dispose(); + super.dispose(); + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + + setState(() { + _isLoading = true; + _successMessage = null; + _errorMessage = null; + }); + + final repository = ref.read(authRepositoryProvider); + final result = await repository.forgotPassword( + ForgotPasswordRequest(email: _emailController.text.trim()), + ); + + if (!mounted) return; + setState(() => _isLoading = false); + + if (result.failure != null) { + setState(() => _errorMessage = result.failure.toString()); + return; + } + + setState( + () => _successMessage = + 'Password reset instructions have been sent to your email.', + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Forgot Password')), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Enter your email address and we will send you a reset link.', + textAlign: TextAlign.center, + ), + const SizedBox(height: 24), + AppTextField( + controller: _emailController, + label: 'Email', + keyboardType: TextInputType.emailAddress, + validator: Validators.email, + ), + if (_errorMessage != null) ...[ + const SizedBox(height: 12), + Text( + _errorMessage!, + style: TextStyle(color: Theme.of(context).colorScheme.error), + textAlign: TextAlign.center, + ), + ], + if (_successMessage != null) ...[ + const SizedBox(height: 12), + Text( + _successMessage!, + style: TextStyle(color: Theme.of(context).colorScheme.primary), + textAlign: TextAlign.center, + ), + ], + const SizedBox(height: 24), + AppButton( + label: 'Send Reset Link', + isLoading: _isLoading, + onPressed: _submit, + ), + const SizedBox(height: 12), + TextButton( + onPressed: () => context.go(RouteConstants.login), + child: const Text('Back to Login'), + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/auth/presentation/screens/login_screen.dart b/lib/modules/auth/presentation/screens/login_screen.dart new file mode 100644 index 0000000..919c2bc --- /dev/null +++ b/lib/modules/auth/presentation/screens/login_screen.dart @@ -0,0 +1,368 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/config/dev_config.dart'; +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/constants/storage_keys.dart'; +import '../../../../core/theme/theme_provider.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/models/user_model.dart'; +import '../../../../shared/providers/auth_provider.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../widgets/bcpl_logo.dart'; +import '../widgets/login_colors.dart'; +import '../widgets/login_hero_panel.dart'; + +class LoginScreen extends ConsumerStatefulWidget { + const LoginScreen({super.key}); + + @override + ConsumerState createState() => _LoginScreenState(); +} + +class _LoginScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _isLoading = false; + bool _obscurePassword = true; + bool _rememberMe = false; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) => _loadRememberedEmail()); + } + + Future _loadRememberedEmail() async { + final prefs = ref.read(sharedPreferencesProvider); + final remember = prefs.getBool(StorageKeys.rememberMe) ?? false; + final email = prefs.getString(StorageKeys.rememberedEmail); + if (!mounted) return; + setState(() { + _rememberMe = remember; + if (remember && email != null) { + _emailController.text = email; + } + }); + } + + Future _persistRememberMe() async { + final prefs = ref.read(sharedPreferencesProvider); + if (_rememberMe) { + await prefs.setBool(StorageKeys.rememberMe, true); + await prefs.setString(StorageKeys.rememberedEmail, _emailController.text.trim()); + } else { + await prefs.remove(StorageKeys.rememberMe); + await prefs.remove(StorageKeys.rememberedEmail); + } + } + + @override + void dispose() { + _emailController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + Future _login() async { + if (!_formKey.currentState!.validate()) return; + + setState(() => _isLoading = true); + await _persistRememberMe(); + + final success = await ref.read(authStateProvider.notifier).login( + LoginRequest( + email: _emailController.text.trim(), + password: _passwordController.text, + ), + ); + setState(() => _isLoading = false); + + if (!mounted) return; + + if (success) { + context.go(RouteConstants.dashboard); + } else { + final error = ref.read(authStateProvider).error; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(error ?? 'Invalid credentials')), + ); + } + } + + InputDecoration _fieldDecoration(LoginColors colors, String hint) { + return InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: colors.iconMuted, fontSize: 14), + filled: true, + fillColor: colors.fieldFillColor, + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: colors.borderColor), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: colors.borderColor), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: colors.primaryAction, width: 1.5), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: colors.colorScheme.error), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide(color: colors.colorScheme.error, width: 1.5), + ), + ); + } + + Widget _buildFieldLabel(LoginColors colors, String label) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: colors.labelColor, + ), + ), + ); + } + + Widget _buildLoginCard(LoginColors colors) { + return Container( + constraints: const BoxConstraints(maxWidth: 440), + decoration: BoxDecoration( + color: colors.cardBackground, + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: colors.cardShadowColor, + blurRadius: 32, + offset: const Offset(0, 12), + ), + ], + ), + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 44), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const Center(child: BcplLogo(height: 52)), + const SizedBox(height: 28), + Text( + 'Welcome back 👋', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 22, + fontWeight: FontWeight.w700, + color: colors.headingColor, + ), + ), + const SizedBox(height: 8), + Text( + 'Sign in to continue to your account', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + color: colors.subtitleColor, + height: 1.4, + ), + ), + const SizedBox(height: 32), + _buildFieldLabel(colors, 'Email'), + TextFormField( + controller: _emailController, + keyboardType: TextInputType.emailAddress, + autofillHints: const [AutofillHints.email], + validator: Validators.email, + style: TextStyle(color: colors.headingColor), + decoration: _fieldDecoration(colors, 'Enter your email').copyWith( + prefixIcon: Icon(Icons.mail_outline, color: colors.iconMuted, size: 20), + ), + ), + const SizedBox(height: 20), + _buildFieldLabel(colors, 'Password'), + TextFormField( + controller: _passwordController, + obscureText: _obscurePassword, + autofillHints: const [AutofillHints.password], + validator: (v) => Validators.required(v, fieldName: 'Password'), + style: TextStyle(color: colors.headingColor), + decoration: _fieldDecoration(colors, 'Enter your password').copyWith( + prefixIcon: Icon(Icons.lock_outline, color: colors.iconMuted, size: 20), + suffixIcon: IconButton( + icon: Icon( + _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined, + color: colors.iconMuted, + size: 20, + ), + onPressed: () => setState(() => _obscurePassword = !_obscurePassword), + ), + ), + ), + const SizedBox(height: 8), + Row( + children: [ + SizedBox( + height: 36, + width: 36, + child: Checkbox( + value: _rememberMe, + activeColor: colors.primaryAction, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)), + onChanged: (v) => setState(() => _rememberMe = v ?? false), + ), + ), + Text( + 'Remember me', + style: TextStyle(fontSize: 13, color: colors.labelColor), + ), + const Spacer(), + TextButton( + onPressed: () => context.push(RouteConstants.forgotPassword), + style: TextButton.styleFrom( + foregroundColor: colors.linkColor, + padding: const EdgeInsets.symmetric(horizontal: 4), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: const Text( + 'Forgot password?', + style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500), + ), + ), + ], + ), + const SizedBox(height: 20), + Theme( + data: Theme.of(context).copyWith( + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: colors.primaryAction, + foregroundColor: colors.colorScheme.onPrimary, + minimumSize: const Size(double.infinity, 50), + elevation: 0, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + textStyle: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + ), + ), + child: AppButton( + label: 'Sign In', + isLoading: _isLoading, + onPressed: _login, + ), + ), + const SizedBox(height: 28), + Row( + children: [ + Expanded(child: Divider(color: colors.borderColor)), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + 'Secure access to your ERP system', + style: TextStyle( + fontSize: 12, + color: colors.subtitleColor, + ), + ), + ), + Expanded(child: Divider(color: colors.borderColor)), + ], + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.shield_outlined, size: 16, color: colors.subtitleColor), + const SizedBox(width: 6), + Text( + 'Your data is protected and secure.', + style: TextStyle( + fontSize: 12, + color: colors.subtitleColor, + ), + ), + ], + ), + if (DevConfig.screenPreviewEnabled) ...[ + const SizedBox(height: 24), + Divider(color: colors.borderColor), + const SizedBox(height: 16), + Text( + 'Login API unavailable? Browse all screens without signing in:', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + color: colors.subtitleColor, + ), + ), + const SizedBox(height: 12), + AppButton( + label: 'Explore All Screens', + isOutlined: true, + onPressed: () { + ref.read(authStateProvider.notifier).loginAsDemo(); + context.go(RouteConstants.screenGallery); + }, + ), + ], + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final isWide = context.isDesktop; + final colors = LoginColors.of(context); + + return Scaffold( + backgroundColor: colors.pageBackground, + body: isWide + ? Row( + children: [ + Expanded( + flex: 42, + child: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(32), + child: _buildLoginCard(colors), + ), + ), + ), + const Expanded( + flex: 58, + child: LoginHeroPanel(), + ), + ], + ) + : SingleChildScrollView( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 48, 24, 24), + child: Center(child: _buildLoginCard(colors)), + ), + const SizedBox( + height: 420, + child: LoginHeroPanel(compact: true), + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/auth/presentation/screens/reset_password_screen.dart b/lib/modules/auth/presentation/screens/reset_password_screen.dart new file mode 100644 index 0000000..86297b3 --- /dev/null +++ b/lib/modules/auth/presentation/screens/reset_password_screen.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; + +class ResetPasswordScreen extends StatefulWidget { + const ResetPasswordScreen({super.key}); + + @override + State createState() => _ResetPasswordScreenState(); +} + +class _ResetPasswordScreenState extends State { + final _formKey = GlobalKey(); + final _passwordController = TextEditingController(); + final _confirmController = TextEditingController(); + + @override + void dispose() { + _passwordController.dispose(); + _confirmController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Reset Password')), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _passwordController, + label: 'New Password', + obscureText: true, + validator: Validators.password, + ), + const SizedBox(height: 16), + AppTextField( + controller: _confirmController, + label: 'Confirm Password', + obscureText: true, + validator: (v) { + if (v != _passwordController.text) return 'Passwords do not match'; + return Validators.required(v, fieldName: 'Confirm password'); + }, + ), + const SizedBox(height: 24), + AppButton( + label: 'Reset Password', + onPressed: () { + if (_formKey.currentState!.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Password reset (API token flow pending)')), + ); + context.go(RouteConstants.login); + } + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/auth/presentation/screens/verify_otp_screen.dart b/lib/modules/auth/presentation/screens/verify_otp_screen.dart new file mode 100644 index 0000000..9d5593a --- /dev/null +++ b/lib/modules/auth/presentation/screens/verify_otp_screen.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; + +class VerifyOtpScreen extends StatefulWidget { + const VerifyOtpScreen({super.key, required this.email}); + + final String email; + + @override + State createState() => _VerifyOtpScreenState(); +} + +class _VerifyOtpScreenState extends State { + final _formKey = GlobalKey(); + final _otpController = TextEditingController(); + + @override + void dispose() { + _otpController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Verify OTP')), + body: Center( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 420), + child: Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Enter the OTP sent to ${widget.email}'), + const SizedBox(height: 24), + AppTextField( + controller: _otpController, + label: 'OTP', + keyboardType: TextInputType.number, + validator: (v) => Validators.minLength(v, 6, fieldName: 'OTP'), + ), + const SizedBox(height: 24), + AppButton( + label: 'Verify', + onPressed: () { + if (_formKey.currentState!.validate()) { + context.go(RouteConstants.dashboard); + } + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/auth/presentation/widgets/bcpl_logo.dart b/lib/modules/auth/presentation/widgets/bcpl_logo.dart new file mode 100644 index 0000000..0d4687c --- /dev/null +++ b/lib/modules/auth/presentation/widgets/bcpl_logo.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; + +import 'login_colors.dart'; + +/// BCPL wordmark with blue and green swooshes, matching the login design. +class BcplLogo extends StatelessWidget { + const BcplLogo({super.key, this.height = 48}); + + final double height; + + @override + Widget build(BuildContext context) { + final colors = LoginColors.of(context); + + return SizedBox( + height: height, + child: CustomPaint( + painter: _BcplLogoPainter( + primaryBlue: colors.primaryAction, + accentGreen: colors.colorScheme.secondary, + ), + child: Align( + alignment: Alignment.topCenter, + child: Padding( + padding: EdgeInsets.only(top: height * 0.08), + child: Text( + 'BCPL', + style: TextStyle( + fontSize: height * 0.58, + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + letterSpacing: -0.5, + color: colors.headingColor, + height: 1, + ), + ), + ), + ), + ), + ); + } +} + +class _BcplLogoPainter extends CustomPainter { + _BcplLogoPainter({ + required this.primaryBlue, + required this.accentGreen, + }); + + final Color primaryBlue; + final Color accentGreen; + + @override + void paint(Canvas canvas, Size size) { + final bluePaint = Paint() + ..color = primaryBlue + ..style = PaintingStyle.stroke + ..strokeWidth = size.height * 0.055 + ..strokeCap = StrokeCap.round; + + final greenPaint = Paint() + ..color = accentGreen + ..style = PaintingStyle.stroke + ..strokeWidth = size.height * 0.045 + ..strokeCap = StrokeCap.round; + + final centerX = size.width / 2; + final baseY = size.height * 0.82; + + final bluePath = Path() + ..moveTo(centerX - size.width * 0.28, baseY) + ..quadraticBezierTo( + centerX - size.width * 0.05, + baseY + size.height * 0.08, + centerX + size.width * 0.3, + baseY - size.height * 0.02, + ); + canvas.drawPath(bluePath, bluePaint); + + final greenPath = Path() + ..moveTo(centerX - size.width * 0.22, baseY + size.height * 0.06) + ..quadraticBezierTo( + centerX, + baseY + size.height * 0.14, + centerX + size.width * 0.24, + baseY + size.height * 0.04, + ); + canvas.drawPath(greenPath, greenPaint); + } + + @override + bool shouldRepaint(covariant _BcplLogoPainter oldDelegate) { + return oldDelegate.primaryBlue != primaryBlue || + oldDelegate.accentGreen != accentGreen; + } +} diff --git a/lib/modules/auth/presentation/widgets/login_colors.dart b/lib/modules/auth/presentation/widgets/login_colors.dart new file mode 100644 index 0000000..fdcb249 --- /dev/null +++ b/lib/modules/auth/presentation/widgets/login_colors.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/theme/app_colors.dart'; + +/// Theme-aware colors for the login screen and its widgets. +class LoginColors { + LoginColors.of(BuildContext context) + : theme = Theme.of(context), + colorScheme = Theme.of(context).colorScheme, + isDark = Theme.of(context).brightness == Brightness.dark; + + final ThemeData theme; + final ColorScheme colorScheme; + final bool isDark; + + Color get pageBackground => theme.scaffoldBackgroundColor; + + Color get cardBackground => AppColors.card; + + Color get headingColor => colorScheme.onSurface; + + Color get subtitleColor => colorScheme.onSurfaceVariant; + + Color get labelColor => isDark + ? colorScheme.onSurface.withValues(alpha: 0.9) + : const Color(0xFF334155); + + Color get linkColor => colorScheme.primary; + + Color get borderColor => isDark + ? colorScheme.outline.withValues(alpha: 0.35) + : const Color(0xFFE2E8F0); + + Color get fieldFillColor => + isDark ? colorScheme.surfaceContainerHighest : Colors.white; + + Color get iconMuted => colorScheme.onSurfaceVariant; + + Color get primaryAction => colorScheme.primary; + + Color get cardShadowColor => isDark + ? Colors.black.withValues(alpha: 0.35) + : Colors.black.withValues(alpha: 0.06); + + Color get moduleNodeBackground => isDark + ? colorScheme.surfaceContainerHigh + : Colors.white; + + Color get platformColor => isDark + ? colorScheme.surfaceContainerHighest + : const Color(0xFFF8FAFC); + + Color get decorCubeColor => + colorScheme.primaryContainer.withValues(alpha: isDark ? 0.35 : 0.55); + + Color get connectionLineColor => colorScheme.outline.withValues( + alpha: isDark ? 0.45 : 0.55, + ); +} diff --git a/lib/modules/auth/presentation/widgets/login_hero_illustration.dart b/lib/modules/auth/presentation/widgets/login_hero_illustration.dart new file mode 100644 index 0000000..aae9ee3 --- /dev/null +++ b/lib/modules/auth/presentation/widgets/login_hero_illustration.dart @@ -0,0 +1,357 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +import 'login_colors.dart'; + +/// Isometric ERP module diagram for the login hero panel. +class LoginHeroIllustration extends StatelessWidget { + const LoginHeroIllustration({super.key}); + + @override + Widget build(BuildContext context) { + final colors = LoginColors.of(context); + + return LayoutBuilder( + builder: (context, constraints) { + final size = math.min(constraints.maxWidth, constraints.maxHeight); + return Center( + child: SizedBox( + width: size, + height: size * 0.85, + child: Stack( + clipBehavior: Clip.none, + children: [ + Positioned( + top: size * 0.02, + left: size * 0.08, + child: _DecorCube( + size: size * 0.06, + opacity: 0.35, + color: colors.decorCubeColor, + ), + ), + Positioned( + top: size * 0.12, + right: size * 0.06, + child: _DecorCube( + size: size * 0.05, + opacity: 0.25, + color: colors.decorCubeColor, + ), + ), + Positioned( + bottom: size * 0.08, + left: size * 0.04, + child: _DecorCube( + size: size * 0.04, + opacity: 0.3, + color: colors.decorCubeColor, + ), + ), + Positioned( + bottom: size * 0.18, + right: size * 0.1, + child: _DecorCube( + size: size * 0.07, + opacity: 0.2, + color: colors.decorCubeColor, + ), + ), + ..._modulePositions(size).map( + (module) => Positioned( + left: module.dx, + top: module.dy, + child: _ModuleNode( + icon: module.icon, + label: module.label, + size: size, + colors: colors, + ), + ), + ), + Positioned( + left: size * 0.32, + top: size * 0.28, + child: CustomPaint( + size: Size(size * 0.36, size * 0.36), + painter: _ConnectionLinesPainter( + modules: _modulePositions(size), + center: Offset(size * 0.18, size * 0.18), + lineColor: colors.connectionLineColor, + ), + ), + ), + Positioned( + left: size * 0.34, + top: size * 0.3, + child: _ErpCore(size: size * 0.32, colors: colors), + ), + ], + ), + ), + ); + }, + ); + } + + static List<_ModuleData> _modulePositions(double size) { + return [ + _ModuleData( + dx: size * 0.02, + dy: size * 0.08, + icon: Icons.inventory_2_outlined, + label: 'Inventory', + ), + _ModuleData( + dx: size * 0.68, + dy: size * 0.02, + icon: Icons.bar_chart_rounded, + label: 'Sales', + ), + _ModuleData( + dx: size * 0.74, + dy: size * 0.38, + icon: Icons.receipt_long_outlined, + label: 'Accounts', + ), + _ModuleData( + dx: size * 0.58, + dy: size * 0.62, + icon: Icons.pie_chart_outline_rounded, + label: 'Reports', + ), + _ModuleData( + dx: size * 0.02, + dy: size * 0.52, + icon: Icons.shopping_cart_outlined, + label: 'Purchases', + ), + ]; + } +} + +class _ModuleData { + const _ModuleData({ + required this.dx, + required this.dy, + required this.icon, + required this.label, + }); + + final double dx; + final double dy; + final IconData icon; + final String label; +} + +class _ErpCore extends StatelessWidget { + const _ErpCore({required this.size, required this.colors}); + + final double size; + final LoginColors colors; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: size, + height: size * 0.55, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + colors.primaryAction, + Color.lerp(colors.primaryAction, Colors.black, 0.25)!, + ], + ), + borderRadius: BorderRadius.circular(size * 0.08), + boxShadow: [ + BoxShadow( + color: colors.primaryAction.withValues(alpha: 0.35), + blurRadius: size * 0.12, + offset: Offset(0, size * 0.06), + ), + ], + ), + alignment: Alignment.center, + child: Text( + 'ERP', + style: TextStyle( + color: colors.colorScheme.onPrimary, + fontSize: size * 0.22, + fontWeight: FontWeight.w800, + letterSpacing: 1, + ), + ), + ), + Container( + width: size * 1.15, + height: size * 0.14, + margin: EdgeInsets.only(top: size * 0.04), + decoration: BoxDecoration( + color: colors.platformColor, + borderRadius: BorderRadius.circular(size * 0.04), + boxShadow: [ + BoxShadow( + color: colors.cardShadowColor, + blurRadius: size * 0.06, + offset: Offset(0, size * 0.02), + ), + ], + ), + ), + Container( + width: size * 1.35, + height: size * 0.1, + margin: EdgeInsets.only(top: size * 0.02), + decoration: BoxDecoration( + color: colors.moduleNodeBackground, + borderRadius: BorderRadius.circular(size * 0.03), + boxShadow: [ + BoxShadow( + color: colors.cardShadowColor, + blurRadius: size * 0.04, + ), + ], + ), + ), + ], + ); + } +} + +class _ModuleNode extends StatelessWidget { + const _ModuleNode({ + required this.icon, + required this.label, + required this.size, + required this.colors, + }); + + final IconData icon; + final String label; + final double size; + final LoginColors colors; + + @override + Widget build(BuildContext context) { + final nodeSize = size * 0.14; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: nodeSize, + height: nodeSize, + decoration: BoxDecoration( + color: colors.moduleNodeBackground, + borderRadius: BorderRadius.circular(nodeSize * 0.22), + boxShadow: [ + BoxShadow( + color: colors.cardShadowColor, + blurRadius: nodeSize * 0.15, + offset: Offset(0, nodeSize * 0.06), + ), + ], + ), + child: Icon( + icon, + color: colors.primaryAction, + size: nodeSize * 0.45, + ), + ), + SizedBox(height: nodeSize * 0.12), + Text( + label, + style: TextStyle( + fontSize: nodeSize * 0.28, + fontWeight: FontWeight.w500, + color: colors.subtitleColor, + ), + ), + ], + ); + } +} + +class _DecorCube extends StatelessWidget { + const _DecorCube({ + required this.size, + required this.opacity, + required this.color, + }); + + final double size; + final double opacity; + final Color color; + + @override + Widget build(BuildContext context) { + return Transform.rotate( + angle: math.pi / 6, + child: Container( + width: size, + height: size, + decoration: BoxDecoration( + color: color.withValues(alpha: opacity), + borderRadius: BorderRadius.circular(size * 0.15), + ), + ), + ); + } +} + +class _ConnectionLinesPainter extends CustomPainter { + _ConnectionLinesPainter({ + required this.modules, + required this.center, + required this.lineColor, + }); + + final List<_ModuleData> modules; + final Offset center; + final Color lineColor; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = lineColor + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + for (final module in modules) { + final moduleCenter = Offset( + module.dx + size.width * 0.07 - (size.width * 0.32), + module.dy + size.height * 0.05 - (size.height * 0.28), + ); + _drawDashedLine(canvas, center, moduleCenter, paint); + } + } + + void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) { + const dashWidth = 5.0; + const dashSpace = 4.0; + final distance = (end - start).distance; + if (distance == 0) return; + + final direction = (end - start) / distance; + var drawn = 0.0; + while (drawn < distance) { + final dashEnd = drawn + dashWidth > distance ? distance : drawn + dashWidth; + canvas.drawLine( + start + direction * drawn, + start + direction * dashEnd, + paint, + ); + drawn += dashWidth + dashSpace; + } + } + + @override + bool shouldRepaint(covariant _ConnectionLinesPainter oldDelegate) { + return oldDelegate.lineColor != lineColor; + } +} diff --git a/lib/modules/auth/presentation/widgets/login_hero_panel.dart b/lib/modules/auth/presentation/widgets/login_hero_panel.dart new file mode 100644 index 0000000..a003d80 --- /dev/null +++ b/lib/modules/auth/presentation/widgets/login_hero_panel.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import 'login_colors.dart'; +import 'login_hero_illustration.dart'; + +/// Marketing hero section shown on the right side of the login screen. +class LoginHeroPanel extends StatelessWidget { + const LoginHeroPanel({super.key, this.compact = false}); + + final bool compact; + + @override + Widget build(BuildContext context) { + final colors = LoginColors.of(context); + + return Padding( + padding: EdgeInsets.symmetric( + horizontal: compact ? 24 : 48, + vertical: compact ? 32 : 48, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: compact ? MainAxisAlignment.start : MainAxisAlignment.center, + children: [ + Text( + 'Smart. Integrated. Efficient.', + style: TextStyle( + fontSize: compact ? 28 : 40, + fontWeight: FontWeight.w700, + color: colors.headingColor, + height: 1.2, + letterSpacing: -0.5, + ), + ), + SizedBox(height: compact ? 12 : 16), + Text( + 'Manage your business operations seamlessly with BCPL ERP.', + style: TextStyle( + fontSize: compact ? 15 : 17, + fontWeight: FontWeight.w400, + color: colors.subtitleColor, + height: 1.5, + ), + ), + SizedBox(height: compact ? 24 : 40), + if (compact) + const SizedBox( + height: 260, + width: double.infinity, + child: LoginHeroIllustration(), + ) + else + const Expanded(child: LoginHeroIllustration()), + ], + ), + ); + } +} diff --git a/lib/modules/branch/presentation/screens/branch_form_screen.dart b/lib/modules/branch/presentation/screens/branch_form_screen.dart new file mode 100644 index 0000000..0ab86a4 --- /dev/null +++ b/lib/modules/branch/presentation/screens/branch_form_screen.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/utils/validators.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; + +class BranchFormScreen extends StatefulWidget { + const BranchFormScreen({super.key, this.branchId}); + + final String? branchId; + + @override + State createState() => _BranchFormScreenState(); +} + +class _BranchFormScreenState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _codeController = TextEditingController(); + final _locationController = TextEditingController(); + final _managerController = TextEditingController(); + + bool get isEditing => widget.branchId != null; + + @override + void dispose() { + _nameController.dispose(); + _codeController.dispose(); + _locationController.dispose(); + _managerController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(isEditing ? 'Edit Branch' : 'Add Branch')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _nameController, + label: 'Branch Name', + validator: (v) => Validators.required(v, fieldName: 'Branch name'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _codeController, + label: 'Branch Code', + validator: (v) => Validators.required(v, fieldName: 'Branch code'), + ), + const SizedBox(height: 16), + AppTextField(controller: _locationController, label: 'Location'), + const SizedBox(height: 16), + AppTextField(controller: _managerController, label: 'Manager'), + const SizedBox(height: 24), + AppButton( + label: isEditing ? 'Update Branch' : 'Create Branch', + onPressed: () { + if (_formKey.currentState!.validate()) Navigator.of(context).pop(); + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/branch/presentation/screens/branch_list_screen.dart b/lib/modules/branch/presentation/screens/branch_list_screen.dart new file mode 100644 index 0000000..7576860 --- /dev/null +++ b/lib/modules/branch/presentation/screens/branch_list_screen.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class BranchListScreen extends StatelessWidget { + const BranchListScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Branches', + subtitle: 'Manage company branches', + actions: [ + ElevatedButton.icon( + onPressed: () => context.push('${RouteConstants.branches}/add'), + icon: const Icon(Icons.add), + label: const Text('Add Branch'), + ), + ], + ), + const Expanded( + child: EmptyStateView( + title: 'No branches yet', + description: 'Add branches to organize assets by location.', + icon: Icons.account_tree_outlined, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/company/data/repositories/company_repository_impl.dart b/lib/modules/company/data/repositories/company_repository_impl.dart new file mode 100644 index 0000000..09b9f0c --- /dev/null +++ b/lib/modules/company/data/repositories/company_repository_impl.dart @@ -0,0 +1,67 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/network/api_handler.dart'; +import '../../../../core/network/dio_client.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/company_model.dart'; +import '../../domain/repositories/company_repository.dart'; + +final companyRepositoryProvider = Provider((ref) { + return CompanyRepositoryImpl(dio: ref.watch(dioProvider)); +}); + +class CompanyRepositoryImpl implements CompanyRepository { + CompanyRepositoryImpl({required this.dio}); + + final Dio dio; + + @override + Future>> getCompanies( + PaginationParams params, + ) async { + return safeApiCall(() async { + final response = await dio.get('/companies', queryParameters: { + 'page': params.page, + 'limit': params.limit, + if (params.search != null) 'search': params.search, + }); + return PaginatedResponse.fromJson( + response.data['data'] as Map, + (json) => CompanyModel.fromJson(json! as Map), + ); + }); + } + + @override + Future> getCompanyById(String id) async { + return safeApiCall(() async { + final response = await dio.get('/companies/$id'); + return CompanyModel.fromJson(response.data['data'] as Map); + }); + } + + @override + Future> createCompany(Map data) async { + return safeApiCall(() async { + final response = await dio.post('/companies', data: data); + return CompanyModel.fromJson(response.data['data'] as Map); + }); + } + + @override + Future> updateCompany(String id, Map data) async { + return safeApiCall(() async { + final response = await dio.put('/companies/$id', data: data); + return CompanyModel.fromJson(response.data['data'] as Map); + }); + } + + @override + Future> getCompanySettings(String id) async { + return safeApiCall(() async { + final response = await dio.get('/companies/$id/settings'); + return CompanySettingsModel.fromJson(response.data['data'] as Map); + }); + } +} diff --git a/lib/modules/company/domain/repositories/company_repository.dart b/lib/modules/company/domain/repositories/company_repository.dart new file mode 100644 index 0000000..bdf35b9 --- /dev/null +++ b/lib/modules/company/domain/repositories/company_repository.dart @@ -0,0 +1,12 @@ +import '../../../../core/errors/failure.dart'; +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/company_model.dart'; + +abstract class CompanyRepository { + Future>> getCompanies(PaginationParams params); + Future> getCompanyById(String id); + Future> createCompany(Map data); + Future> updateCompany(String id, Map data); + Future> getCompanySettings(String id); +} diff --git a/lib/modules/company/presentation/screens/company_form_screen.dart b/lib/modules/company/presentation/screens/company_form_screen.dart new file mode 100644 index 0000000..6b7c9de --- /dev/null +++ b/lib/modules/company/presentation/screens/company_form_screen.dart @@ -0,0 +1,105 @@ +import 'package:flutter/material.dart'; + +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../core/utils/validators.dart'; + +class CompanyFormScreen extends StatefulWidget { + const CompanyFormScreen({super.key, this.companyId}); + + final String? companyId; + + @override + State createState() => _CompanyFormScreenState(); +} + +class _CompanyFormScreenState extends State { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _codeController = TextEditingController(); + final _gstController = TextEditingController(); + final _addressController = TextEditingController(); + final _emailController = TextEditingController(); + final _phoneController = TextEditingController(); + + bool get isEditing => widget.companyId != null; + + @override + void dispose() { + _nameController.dispose(); + _codeController.dispose(); + _gstController.dispose(); + _addressController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(isEditing ? 'Edit Company' : 'Add Company')), + body: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _nameController, + label: 'Company Name', + validator: (v) => Validators.required(v, fieldName: 'Company name'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _codeController, + label: 'Company Code', + validator: (v) => Validators.required(v, fieldName: 'Company code'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _gstController, + label: 'GST Number', + validator: Validators.gstNumber, + ), + const SizedBox(height: 16), + AppTextField( + controller: _addressController, + label: 'Address', + maxLines: 3, + ), + const SizedBox(height: 16), + AppTextField( + controller: _emailController, + label: 'Email', + keyboardType: TextInputType.emailAddress, + validator: Validators.email, + ), + const SizedBox(height: 16), + AppTextField( + controller: _phoneController, + label: 'Phone', + keyboardType: TextInputType.phone, + validator: Validators.phone, + ), + const SizedBox(height: 24), + AppButton( + label: isEditing ? 'Update Company' : 'Create Company', + onPressed: () { + if (_formKey.currentState!.validate()) { + Navigator.of(context).pop(); + } + }, + ), + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/modules/company/presentation/screens/company_list_screen.dart b/lib/modules/company/presentation/screens/company_list_screen.dart new file mode 100644 index 0000000..4218751 --- /dev/null +++ b/lib/modules/company/presentation/screens/company_list_screen.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class CompanyListScreen extends StatelessWidget { + const CompanyListScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Companies', + subtitle: 'Manage organizations', + actions: [ + ElevatedButton.icon( + onPressed: () => context.push('${RouteConstants.companies}/add'), + icon: const Icon(Icons.add), + label: const Text('Add Company'), + ), + ], + ), + const Expanded( + child: EmptyStateView( + title: 'No companies yet', + description: 'Connect to the API to load companies, or add your first company.', + icon: Icons.business_outlined, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/dashboard/presentation/screens/dashboard_screen.dart b/lib/modules/dashboard/presentation/screens/dashboard_screen.dart new file mode 100644 index 0000000..448b000 --- /dev/null +++ b/lib/modules/dashboard/presentation/screens/dashboard_screen.dart @@ -0,0 +1,101 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/widgets/kpi_card.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class DashboardScreen extends StatelessWidget { + const DashboardScreen({super.key}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: context.contentMaxWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PageHeader( + title: 'Dashboard', + subtitle: 'Asset management overview', + ), + LayoutBuilder( + builder: (context, constraints) { + final crossAxisCount = constraints.maxWidth > 900 ? 3 : (constraints.maxWidth > 600 ? 2 : 1); + return GridView.count( + crossAxisCount: crossAxisCount, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 16, + crossAxisSpacing: 16, + childAspectRatio: 1.8, + children: [ + KpiCard( + title: 'Total Assets', + value: '—', + icon: Icons.inventory_2_outlined, + onTap: () => context.go(RouteConstants.assets), + ), + KpiCard( + title: 'Allocated', + value: '—', + icon: Icons.assignment_ind_outlined, + color: Colors.blue, + ), + KpiCard( + title: 'Available', + value: '—', + icon: Icons.check_circle_outline, + color: Colors.green, + ), + KpiCard( + title: 'Under Maintenance', + value: '—', + icon: Icons.build_outlined, + color: Colors.orange, + ), + KpiCard( + title: 'Disposed', + value: '—', + icon: Icons.delete_outline, + color: Colors.red, + ), + KpiCard( + title: 'Warranty Expiring', + value: '—', + icon: Icons.warning_amber_outlined, + color: Colors.amber, + ), + ], + ); + }, + ), + const SizedBox(height: 32), + Text('Charts', style: Theme.of(context).textTheme.titleLarge), + const SizedBox(height: 16), + AppCard( + child: Padding( + padding: const EdgeInsets.all(48), + child: Center( + child: Text( + 'Charts will load from API\n(Assets by Category, Branch, Allocation Trend)', + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/dev/presentation/screens/screen_gallery_screen.dart b/lib/modules/dev/presentation/screens/screen_gallery_screen.dart new file mode 100644 index 0000000..6368355 --- /dev/null +++ b/lib/modules/dev/presentation/screens/screen_gallery_screen.dart @@ -0,0 +1,185 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../shared/providers/auth_provider.dart'; +import '../../../../shared/widgets/page_header.dart'; + +class _GalleryEntry { + const _GalleryEntry({ + required this.title, + required this.route, + required this.group, + this.preview = true, + }); + + final String title; + final String route; + final String group; + final bool preview; +} + +String _route(String path, {bool preview = true}) { + if (!preview || path.contains('preview=true')) return path; + return path.contains('?') ? '$path&preview=true' : '$path?preview=true'; +} + +const _entries = [ + // Auth + _GalleryEntry( + title: 'Login', + route: '${RouteConstants.login}?preview=true', + group: 'Auth', + preview: false, + ), + _GalleryEntry( + title: 'Forgot Password', + route: '${RouteConstants.forgotPassword}?preview=true', + group: 'Auth', + preview: false, + ), + _GalleryEntry( + title: 'Reset Password', + route: '${RouteConstants.resetPassword}?preview=true', + group: 'Auth', + preview: false, + ), + _GalleryEntry( + title: 'Verify OTP', + route: '${RouteConstants.verifyOtp}?email=demo@bharaterp.com&preview=true', + group: 'Auth', + preview: false, + ), + _GalleryEntry(title: 'Change Password', route: RouteConstants.changePassword, group: 'Auth'), + // Main + _GalleryEntry(title: 'Dashboard', route: RouteConstants.dashboard, group: 'Main'), + // Organization + _GalleryEntry(title: 'Companies', route: RouteConstants.companies, group: 'Organization'), + _GalleryEntry(title: 'Add Company', route: RouteConstants.companyAdd, group: 'Organization'), + _GalleryEntry(title: 'Edit Company', route: '/companies/demo-co/edit', group: 'Organization'), + _GalleryEntry(title: 'Branches', route: RouteConstants.branches, group: 'Organization'), + _GalleryEntry(title: 'Add Branch', route: RouteConstants.branchAdd, group: 'Organization'), + _GalleryEntry(title: 'Edit Branch', route: '/branches/demo-br/edit', group: 'Organization'), + // People + _GalleryEntry(title: 'Users & Role Management', route: RouteConstants.usersRoleManagement, group: 'People'), + _GalleryEntry(title: 'Users List', route: RouteConstants.users, group: 'People'), + _GalleryEntry(title: 'Add User', route: RouteConstants.userAdd, group: 'People'), + _GalleryEntry(title: 'Edit User', route: '/users/demo-user/edit', group: 'People'), + _GalleryEntry(title: 'User Detail', route: '/users/demo-user', group: 'People'), + _GalleryEntry(title: 'User Profile', route: RouteConstants.profile, group: 'People'), + _GalleryEntry(title: 'Roles List', route: RouteConstants.roles, group: 'People'), + _GalleryEntry( + title: 'Permission Matrix', + route: '/roles/demo-role/permissions', + group: 'People', + ), + // Assets + _GalleryEntry(title: 'Asset List', route: RouteConstants.assets, group: 'Assets'), + _GalleryEntry(title: 'Add Asset', route: RouteConstants.assetAdd, group: 'Assets'), + _GalleryEntry(title: 'Edit Asset', route: '/assets/demo-asset/edit', group: 'Assets'), + _GalleryEntry(title: 'Asset Detail', route: '/assets/demo-asset', group: 'Assets'), + _GalleryEntry(title: 'Asset Categories', route: RouteConstants.assetCategories, group: 'Assets'), + _GalleryEntry(title: 'Allocations', route: RouteConstants.assetAllocations, group: 'Assets'), + _GalleryEntry(title: 'Maintenance', route: RouteConstants.assetMaintenance, group: 'Assets'), + _GalleryEntry(title: 'Disposal', route: RouteConstants.assetDisposal, group: 'Assets'), + _GalleryEntry(title: 'QR Scan', route: RouteConstants.assetQrScan, group: 'Assets'), + _GalleryEntry(title: 'QR Generate', route: RouteConstants.assetQrGenerate, group: 'Assets'), + // Master data + _GalleryEntry(title: 'Departments', route: RouteConstants.departments, group: 'Master Data'), + _GalleryEntry(title: 'Locations', route: RouteConstants.locations, group: 'Master Data'), + _GalleryEntry(title: 'UOM', route: RouteConstants.uom, group: 'Master Data'), + // Settings + _GalleryEntry(title: 'Settings Hub', route: RouteConstants.settings, group: 'Settings'), + _GalleryEntry(title: 'General Settings', route: RouteConstants.settingsGeneral, group: 'Settings'), + _GalleryEntry( + title: 'Company Profile', + route: RouteConstants.settingsCompanyProfile, + group: 'Settings', + ), + _GalleryEntry(title: 'Appearance', route: RouteConstants.settingsAppearance, group: 'Settings'), + _GalleryEntry( + title: 'Roles & Permissions', + route: RouteConstants.settingsRoles, + group: 'Settings', + ), + _GalleryEntry(title: 'Asset Settings', route: RouteConstants.settingsAsset, group: 'Settings'), + _GalleryEntry( + title: 'Notifications', + route: RouteConstants.settingsNotifications, + group: 'Settings', + ), + _GalleryEntry(title: 'Email Config', route: RouteConstants.settingsEmail, group: 'Settings'), + _GalleryEntry(title: 'Security', route: RouteConstants.settingsSecurity, group: 'Settings'), + // Other + _GalleryEntry(title: 'Reports', route: RouteConstants.reports, group: 'Other'), + _GalleryEntry(title: 'Audit Logs', route: RouteConstants.auditLogs, group: 'Other'), +]; + +class ScreenGalleryScreen extends ConsumerStatefulWidget { + const ScreenGalleryScreen({super.key}); + + @override + ConsumerState createState() => _ScreenGalleryScreenState(); +} + +class _ScreenGalleryScreenState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (ref.read(authStateProvider).status != AuthStatus.authenticated) { + ref.read(authStateProvider.notifier).loginAsDemo(); + } + }); + } + + @override + Widget build(BuildContext context) { + final groups = _entries.map((e) => e.group).toSet().toList(); + + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PageHeader( + title: 'Screen Gallery', + subtitle: 'Browse all app screens while the login API is unavailable', + ), + const SizedBox(height: 8), + Text( + 'Demo user session is active. Tap any chip to open that screen.', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(height: 16), + ...groups.map((group) { + final items = _entries.where((e) => e.group == group).toList(); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 12), + child: Text(group, style: Theme.of(context).textTheme.titleMedium), + ), + Wrap( + spacing: 12, + runSpacing: 12, + children: items.map((entry) { + final route = entry.preview ? _route(entry.route) : entry.route; + return ActionChip( + avatar: const Icon(Icons.open_in_new, size: 18), + label: Text(entry.title), + onPressed: () => context.push(route), + ); + }).toList(), + ), + const SizedBox(height: 16), + ], + ); + }), + ], + ), + ); + } +} diff --git a/lib/modules/master_data/presentation/screens/departments_screen.dart b/lib/modules/master_data/presentation/screens/departments_screen.dart new file mode 100644 index 0000000..2a19e9e --- /dev/null +++ b/lib/modules/master_data/presentation/screens/departments_screen.dart @@ -0,0 +1,6 @@ +import '../../../../shared/widgets/placeholder_screen.dart'; + +class DepartmentsScreen extends PlaceholderScreen { + const DepartmentsScreen({super.key}) + : super(title: 'Departments', description: 'Master data — departments'); +} diff --git a/lib/modules/master_data/presentation/screens/locations_screen.dart b/lib/modules/master_data/presentation/screens/locations_screen.dart new file mode 100644 index 0000000..a152d13 --- /dev/null +++ b/lib/modules/master_data/presentation/screens/locations_screen.dart @@ -0,0 +1,6 @@ +import '../../../../shared/widgets/placeholder_screen.dart'; + +class LocationsScreen extends PlaceholderScreen { + const LocationsScreen({super.key}) + : super(title: 'Locations', description: 'Master data — locations'); +} diff --git a/lib/modules/master_data/presentation/screens/uom_screen.dart b/lib/modules/master_data/presentation/screens/uom_screen.dart new file mode 100644 index 0000000..6b9dd29 --- /dev/null +++ b/lib/modules/master_data/presentation/screens/uom_screen.dart @@ -0,0 +1,6 @@ +import '../../../../shared/widgets/placeholder_screen.dart'; + +class UomScreen extends PlaceholderScreen { + const UomScreen({super.key}) + : super(title: 'Units of Measure', description: 'Master data — UOM'); +} diff --git a/lib/modules/masters/data/datasources/master_remote_data_source.dart b/lib/modules/masters/data/datasources/master_remote_data_source.dart new file mode 100644 index 0000000..2e459b2 --- /dev/null +++ b/lib/modules/masters/data/datasources/master_remote_data_source.dart @@ -0,0 +1,57 @@ +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../../../core/network/dio_client.dart'; +import '../../../../shared/models/user_management_models.dart'; + +final masterRemoteDataSourceProvider = Provider((ref) { + return MasterRemoteDataSource(dio: ref.watch(dioProvider)); +}); + +/// Fetches master-data options (departments, plants, designations) for dropdowns. +class MasterRemoteDataSource { + MasterRemoteDataSource({required this.dio}); + + final Dio dio; + + Future> listDepartments() => + _listOptions(ApiEndpoints.departments); + + Future> listPlants() => + _listOptions(ApiEndpoints.plants); + + Future> listDesignations() => + _listOptions(ApiEndpoints.designations); + + Future> _listOptions(String endpoint) async { + final response = await dio.get( + endpoint, + queryParameters: const {'limit': 100, 'is_active': true}, + ); + final body = response.data as Map; + final raw = body['data']; + + final list = raw is List + ? raw + : raw is Map + ? raw['items'] as List? ?? const [] + : const []; + + return list + .whereType>() + .where((item) => item['is_active'] != false) + .map( + (item) => FilterOptionModel( + id: item['id']?.toString() ?? '', + name: item['name'] as String? ?? '', + ), + ) + .where((item) => item.id.isNotEmpty && item.name.isNotEmpty) + .toList(); + } +} + +/// Users whose role name is exactly "Manager" (for reporting_to dropdown). +bool isReportingManagerRole(String? roleName) => + roleName?.trim().toLowerCase() == 'manager'; diff --git a/lib/modules/rbac/domain/entities/rbac_entities.dart b/lib/modules/rbac/domain/entities/rbac_entities.dart new file mode 100644 index 0000000..5be44bf --- /dev/null +++ b/lib/modules/rbac/domain/entities/rbac_entities.dart @@ -0,0 +1,404 @@ +import 'package:flutter/material.dart'; + +/// RBAC permission actions shown in the permission matrix. +enum RbacAction { + read('read', 'VIEW'), + create('create', 'CREATE'), + update('update', 'EDIT'), + delete('delete', 'DELETE'), + approve('approve', 'APPROVE'), + export('export', 'EXPORT'); + + const RbacAction(this.value, this.label); + final String value; + final String label; +} + +/// Modules available in the permission matrix. +class RbacModule { + const RbacModule({ + required this.key, + required this.label, + required this.icon, + required this.color, + }); + + final String key; + final String label; + final IconData icon; + final Color color; +} + +const rbacModules = [ + RbacModule( + key: 'users', + label: 'User Management', + icon: Icons.people_outline, + color: Color(0xFF2563EB), + ), + RbacModule( + key: 'roles', + label: 'Roles & Permissions', + icon: Icons.shield_outlined, + color: Color(0xFF16A34A), + ), + RbacModule( + key: 'masters', + label: 'Master Data', + icon: Icons.dataset_outlined, + color: Color(0xFFEA580C), + ), + RbacModule( + key: 'vendors', + label: 'Vendor Management', + icon: Icons.storefront_outlined, + color: Color(0xFF7C3AED), + ), + RbacModule( + key: 'purchase_orders', + label: 'Purchase Orders', + icon: Icons.receipt_long_outlined, + color: Color(0xFFCA8A04), + ), + RbacModule( + key: 'grn', + label: 'GRN / PO Receipt', + icon: Icons.inventory_outlined, + color: Color(0xFF0891B2), + ), + RbacModule( + key: 'assets', + label: 'Asset Management', + icon: Icons.inventory_2_outlined, + color: Color(0xFFDC2626), + ), +]; + +class ManagedUser { + const ManagedUser({ + required this.id, + required this.name, + required this.email, + required this.employeeCode, + required this.roleId, + required this.roleName, + required this.department, + required this.plant, + required this.lastLogin, + required this.status, + }); + + final String id; + final String name; + final String email; + final String employeeCode; + final String roleId; + final String roleName; + final String department; + final String plant; + final String lastLogin; + final UserAccountStatus status; + + String get initials { + final parts = name.trim().split(RegExp(r'\s+')); + if (parts.length >= 2) { + return '${parts.first[0]}${parts[1][0]}'.toUpperCase(); + } + return name.isNotEmpty ? name[0].toUpperCase() : 'U'; + } + + ManagedUser copyWith({ + String? id, + String? name, + String? email, + String? employeeCode, + String? roleId, + String? roleName, + String? department, + String? plant, + String? lastLogin, + UserAccountStatus? status, + }) { + return ManagedUser( + id: id ?? this.id, + name: name ?? this.name, + email: email ?? this.email, + employeeCode: employeeCode ?? this.employeeCode, + roleId: roleId ?? this.roleId, + roleName: roleName ?? this.roleName, + department: department ?? this.department, + plant: plant ?? this.plant, + lastLogin: lastLogin ?? this.lastLogin, + status: status ?? this.status, + ); + } +} + +enum UserAccountStatus { + active('Active', Color(0xFF16A34A)), + inactive('Inactive', Color(0xFF6B7280)), + locked('Locked', Color(0xFFDC2626)); + + const UserAccountStatus(this.label, this.color); + final String label; + final Color color; +} + +UserAccountStatus userStatusFromApi(String status) => switch (status.toLowerCase()) { + 'active' => UserAccountStatus.active, + 'inactive' => UserAccountStatus.inactive, + 'locked' => UserAccountStatus.locked, + _ => UserAccountStatus.inactive, + }; + +class ManagedRole { + const ManagedRole({ + required this.id, + required this.name, + required this.description, + required this.icon, + required this.color, + required this.userCount, + required this.permissions, + }); + + final String id; + final String name; + final String description; + final IconData icon; + final Color color; + final int userCount; + final Set permissions; + + int get permissionCount => permissions.length; + + ManagedRole copyWith({ + String? id, + String? name, + String? description, + IconData? icon, + Color? color, + int? userCount, + Set? permissions, + }) { + return ManagedRole( + id: id ?? this.id, + name: name ?? this.name, + description: description ?? this.description, + icon: icon ?? this.icon, + color: color ?? this.color, + userCount: userCount ?? this.userCount, + permissions: permissions ?? this.permissions, + ); + } + + bool hasPermission(String module, RbacAction action) => + permissions.contains('$module.${action.value}'); +} + +String permissionKey(String module, RbacAction action) => + '$module.${action.value}'; + +Set allPermissionsForRole(String roleId) { + final all = {}; + for (final module in rbacModules) { + for (final action in RbacAction.values) { + all.add(permissionKey(module.key, action)); + } + } + + Set modulePerms( + String module, { + bool create = false, + bool edit = false, + bool delete = false, + bool approve = false, + bool export = false, + bool view = true, + }) { + final perms = {}; + if (view) perms.add(permissionKey(module, RbacAction.read)); + if (create) perms.add(permissionKey(module, RbacAction.create)); + if (edit) perms.add(permissionKey(module, RbacAction.update)); + if (delete) perms.add(permissionKey(module, RbacAction.delete)); + if (approve) perms.add(permissionKey(module, RbacAction.approve)); + if (export) perms.add(permissionKey(module, RbacAction.export)); + return perms; + } + + return switch (roleId) { + 'super_admin' => all, + 'admin' => all.where((p) => !p.startsWith('settings.')).toSet(), + 'purchase_manager' => { + ...modulePerms('users', view: true), + ...modulePerms('masters', view: true, create: true, edit: true), + ...modulePerms('vendors', view: true, create: true, edit: true), + ...modulePerms('purchase_orders', + view: true, create: true, edit: true, approve: true, export: true), + ...modulePerms('grn', view: true), + ...modulePerms('assets', view: true), + }, + 'store_manager' => { + ...modulePerms('masters', view: true), + ...modulePerms('grn', view: true, create: true, edit: true), + ...modulePerms('assets', view: true), + }, + 'accounts' => { + ...modulePerms('vendors', view: true), + ...modulePerms('purchase_orders', view: true, export: true), + ...modulePerms('grn', view: true), + }, + 'asset_manager' => { + ...modulePerms('assets', + view: true, create: true, edit: true, delete: true, export: true), + ...modulePerms('masters', view: true), + }, + _ => {permissionKey('assets', RbacAction.read)}, + }; +} + +List defaultRoles = [ + ManagedRole( + id: 'super_admin', + name: 'Super Admin', + description: 'Full access to all modules', + icon: Icons.admin_panel_settings_outlined, + color: const Color(0xFF2563EB), + userCount: 1, + permissions: allPermissionsForRole('super_admin'), + ), + ManagedRole( + id: 'admin', + name: 'Admin', + description: 'Administrative access, no system config', + icon: Icons.shield_outlined, + color: const Color(0xFF16A34A), + userCount: 2, + permissions: allPermissionsForRole('admin'), + ), + ManagedRole( + id: 'purchase_manager', + name: 'Purchase Manager', + description: 'Full PO lifecycle and vendor management', + icon: Icons.receipt_long_outlined, + color: const Color(0xFFCA8A04), + userCount: 8, + permissions: allPermissionsForRole('purchase_manager'), + ), + ManagedRole( + id: 'store_manager', + name: 'Store Manager', + description: 'GRN, warehouse receipts, stock view', + icon: Icons.warehouse_outlined, + color: const Color(0xFF0891B2), + userCount: 12, + permissions: allPermissionsForRole('store_manager'), + ), + ManagedRole( + id: 'accounts', + name: 'Accounts', + description: 'View PO, GRN, vendor financial details', + icon: Icons.account_balance_wallet_outlined, + color: const Color(0xFF7C3AED), + userCount: 8, + permissions: allPermissionsForRole('accounts'), + ), + ManagedRole( + id: 'asset_manager', + name: 'Asset Manager', + description: 'Full asset register and transfer mgmt', + icon: Icons.inventory_2_outlined, + color: const Color(0xFFEA580C), + userCount: 4, + permissions: allPermissionsForRole('asset_manager'), + ), +]; + +List buildDefaultUsers() { + const seeds = [ + ('Gowtham S', 'gowtham@bharaterp.com', 'super_admin', 'Super Admin', + 'Administration', 'Unit 1 - Soap', 'Today 10:24 AM', UserAccountStatus.active), + ('Ravi Kumar', 'ravi@bharaterp.com', 'purchase_manager', 'Purchase Manager', + 'Procurement', 'Unit 1 - Soap', 'Yesterday', UserAccountStatus.active), + ('Priya Sharma', 'priya@bharaterp.com', 'store_manager', 'Store Manager', + 'Stores / Warehouse', 'Unit 2 - Powder', '2 days ago', UserAccountStatus.active), + ('Anil Mehta', 'anil@bharaterp.com', 'accounts', 'Accounts', + 'Finance', 'Unit 1 - Soap', '1 week ago', UserAccountStatus.inactive), + ('Deepak Singh', 'deepak@bharaterp.com', 'asset_manager', 'Asset Manager', + 'Asset Management', 'Unit 1 - Soap', '3 days ago', UserAccountStatus.locked), + ]; + + final users = []; + final roleIds = [ + 'purchase_manager', + 'store_manager', + 'accounts', + 'asset_manager', + 'admin', + ]; + final roleNames = { + 'purchase_manager': 'Purchase Manager', + 'store_manager': 'Store Manager', + 'accounts': 'Accounts', + 'asset_manager': 'Asset Manager', + 'admin': 'Admin', + }; + final departments = [ + 'Procurement', + 'Stores / Warehouse', + 'Finance', + 'Asset Management', + 'Administration', + 'Production', + ]; + final plants = ['Unit 1 - Soap', 'Unit 2 - Powder', 'Head Office']; + final logins = ['Today 09:15 AM', 'Yesterday', '2 days ago', '3 days ago', '1 week ago']; + final statuses = [ + UserAccountStatus.active, + UserAccountStatus.active, + UserAccountStatus.active, + UserAccountStatus.inactive, + UserAccountStatus.locked, + ]; + + for (var i = 0; i < 38; i++) { + if (i < seeds.length) { + final s = seeds[i]; + users.add( + ManagedUser( + id: '${i + 1}', + name: s.$1, + email: s.$2, + employeeCode: 'EMP-${(i + 1).toString().padLeft(3, '0')}', + roleId: s.$3, + roleName: s.$4, + department: s.$5, + plant: s.$6, + lastLogin: s.$7, + status: s.$8, + ), + ); + continue; + } + + final roleId = roleIds[i % roleIds.length]; + users.add( + ManagedUser( + id: '${i + 1}', + name: 'User ${i + 1}', + email: 'user${i + 1}@bharaterp.com', + employeeCode: 'EMP-${(i + 1).toString().padLeft(3, '0')}', + roleId: roleId, + roleName: roleNames[roleId]!, + department: departments[i % departments.length], + plant: plants[i % plants.length], + lastLogin: logins[i % logins.length], + status: statuses[i % statuses.length], + ), + ); + } + + return users; +} + +List get defaultUsers => buildDefaultUsers(); diff --git a/lib/modules/rbac/presentation/providers/add_user_form_provider.dart b/lib/modules/rbac/presentation/providers/add_user_form_provider.dart new file mode 100644 index 0000000..19f07ef --- /dev/null +++ b/lib/modules/rbac/presentation/providers/add_user_form_provider.dart @@ -0,0 +1,140 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/models/user_management_models.dart'; +import '../../../masters/data/datasources/master_remote_data_source.dart'; +import '../../../roles/data/repositories/role_repository_impl.dart'; +import '../../../users/presentation/providers/users_provider.dart'; + +class AddUserFormState { + const AddUserFormState({ + this.roles = const [], + this.departments = const [], + this.designations = const [], + this.plants = const [], + this.managers = const [], + this.editingUser, + this.isSubmitting = false, + this.errorMessage, + }); + + final List roles; + final List departments; + final List designations; + final List plants; + final List managers; + final ManagedUserModel? editingUser; + final bool isSubmitting; + final String? errorMessage; + + AddUserFormState copyWith({ + List? roles, + List? departments, + List? designations, + List? plants, + List? managers, + ManagedUserModel? editingUser, + bool? isSubmitting, + String? errorMessage, + bool clearError = false, + }) { + return AddUserFormState( + roles: roles ?? this.roles, + departments: departments ?? this.departments, + designations: designations ?? this.designations, + plants: plants ?? this.plants, + managers: managers ?? this.managers, + editingUser: editingUser ?? this.editingUser, + isSubmitting: isSubmitting ?? this.isSubmitting, + errorMessage: clearError ? null : errorMessage ?? this.errorMessage, + ); + } +} + +final addUserFormProvider = AsyncNotifierProvider.family< + AddUserFormNotifier, AddUserFormState, String?>(AddUserFormNotifier.new); + +class AddUserFormNotifier extends FamilyAsyncNotifier { + @override + Future build(String? userId) async { + final masterRemote = ref.read(masterRemoteDataSourceProvider); + final roleRepository = ref.read(roleRepositoryProvider); + final getUsers = ref.read(getUsersUseCaseProvider); + + final rolesResult = await roleRepository.listRoleOptions(); + if (rolesResult.failure != null) throw rolesResult.failure!; + + final departments = await masterRemote.listDepartments(); + final plants = await masterRemote.listPlants(); + final designations = await masterRemote.listDesignations(); + + final usersResult = await getUsers(const UserListQuery(limit: 100)); + if (usersResult.failure != null) throw usersResult.failure!; + + final managers = usersResult.data!.items + .where((user) => isReportingManagerRole(user.roleName)) + .map((user) => FilterOptionModel(id: user.id, name: user.fullName)) + .toList(); + + ManagedUserModel? editingUser; + if (userId != null) { + final userResult = await ref.read(getUserByIdUseCaseProvider)(userId); + if (userResult.failure != null) throw userResult.failure!; + editingUser = userResult.data; + } + + return AddUserFormState( + roles: rolesResult.data ?? const [], + departments: departments, + designations: designations, + plants: plants, + managers: managers, + editingUser: editingUser, + ); + } + + Future submitCreate(CreateUserRequest request) async { + final current = state.valueOrNull ?? const AddUserFormState(); + state = AsyncData(current.copyWith(isSubmitting: true, clearError: true)); + + final result = await ref.read(createUserUseCaseProvider)(request); + + if (result.failure != null) { + state = AsyncData( + current.copyWith( + isSubmitting: false, + errorMessage: result.failure!.message, + ), + ); + return false; + } + + state = AsyncData(current.copyWith(isSubmitting: false, clearError: true)); + return true; + } + + Future submitUpdate(String userId, UpdateUserRequest request) async { + final current = state.valueOrNull ?? const AddUserFormState(); + state = AsyncData(current.copyWith(isSubmitting: true, clearError: true)); + + final result = await ref.read(updateUserUseCaseProvider)(userId, request); + + if (result.failure != null) { + state = AsyncData( + current.copyWith( + isSubmitting: false, + errorMessage: result.failure!.message, + ), + ); + return false; + } + + state = AsyncData( + current.copyWith( + isSubmitting: false, + clearError: true, + editingUser: result.data, + ), + ); + return true; + } +} diff --git a/lib/modules/rbac/presentation/providers/rbac_provider.dart b/lib/modules/rbac/presentation/providers/rbac_provider.dart new file mode 100644 index 0000000..181b945 --- /dev/null +++ b/lib/modules/rbac/presentation/providers/rbac_provider.dart @@ -0,0 +1,184 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../domain/entities/rbac_entities.dart'; + +enum RbacTab { users, roles, permissions } + +class RbacState { + const RbacState({ + this.users = const [], + this.roles = const [], + this.selectedTab = RbacTab.users, + this.selectedRoleId = 'super_admin', + this.searchQuery = '', + this.roleFilter = 'All roles', + this.departmentFilter = 'All departments', + this.statusFilter = 'All statuses', + this.currentPage = 0, + this.pageSize = 10, + }); + + final List users; + final List roles; + final RbacTab selectedTab; + final String selectedRoleId; + final String searchQuery; + final String roleFilter; + final String departmentFilter; + final String statusFilter; + final int currentPage; + final int pageSize; + + int get totalUsers => users.length; + int get activeUsers => + users.where((u) => u.status == UserAccountStatus.active).length; + int get inactiveUsers => + users.where((u) => u.status == UserAccountStatus.inactive).length; + int get lockedUsers => + users.where((u) => u.status == UserAccountStatus.locked).length; + + List get filteredUsers { + return users.where((user) { + final q = searchQuery.toLowerCase(); + final matchesSearch = q.isEmpty || + user.name.toLowerCase().contains(q) || + user.email.toLowerCase().contains(q) || + user.employeeCode.toLowerCase().contains(q); + final matchesRole = + roleFilter == 'All roles' || user.roleName == roleFilter; + final matchesDept = departmentFilter == 'All departments' || + user.department == departmentFilter; + final matchesStatus = statusFilter == 'All statuses' || + user.status.label == statusFilter; + return matchesSearch && matchesRole && matchesDept && matchesStatus; + }).toList(); + } + + List get pagedUsers { + final filtered = filteredUsers; + final start = currentPage * pageSize; + if (start >= filtered.length) return []; + final end = (start + pageSize).clamp(0, filtered.length); + return filtered.sublist(start, end); + } + + int get totalPages => (filteredUsers.length / pageSize).ceil().clamp(1, 999); + + ManagedRole? get selectedRole { + for (final role in roles) { + if (role.id == selectedRoleId) return role; + } + return roles.isNotEmpty ? roles.first : null; + } + + RbacState copyWith({ + List? users, + List? roles, + RbacTab? selectedTab, + String? selectedRoleId, + String? searchQuery, + String? roleFilter, + String? departmentFilter, + String? statusFilter, + int? currentPage, + int? pageSize, + }) { + return RbacState( + users: users ?? this.users, + roles: roles ?? this.roles, + selectedTab: selectedTab ?? this.selectedTab, + selectedRoleId: selectedRoleId ?? this.selectedRoleId, + searchQuery: searchQuery ?? this.searchQuery, + roleFilter: roleFilter ?? this.roleFilter, + departmentFilter: departmentFilter ?? this.departmentFilter, + statusFilter: statusFilter ?? this.statusFilter, + currentPage: currentPage ?? this.currentPage, + pageSize: pageSize ?? this.pageSize, + ); + } +} + +final rbacProvider = StateNotifierProvider((ref) { + return RbacNotifier(); +}); + +class RbacNotifier extends StateNotifier { + RbacNotifier() + : super(RbacState( + users: List.of(defaultUsers), + roles: List.of(defaultRoles), + )) { + _recountRoleUsers(); + } + + void setTab(RbacTab tab) => state = state.copyWith(selectedTab: tab, currentPage: 0); + + void setSearch(String query) => + state = state.copyWith(searchQuery: query, currentPage: 0); + + void setRoleFilter(String filter) => + state = state.copyWith(roleFilter: filter, currentPage: 0); + + void setDepartmentFilter(String filter) => + state = state.copyWith(departmentFilter: filter, currentPage: 0); + + void setStatusFilter(String filter) => + state = state.copyWith(statusFilter: filter, currentPage: 0); + + void setPage(int page) => state = state.copyWith(currentPage: page); + + void selectRole(String roleId) => state = state.copyWith(selectedRoleId: roleId); + + void togglePermission(String module, RbacAction action) { + final role = state.selectedRole; + if (role == null) return; + + final key = permissionKey(module, action); + final updated = Set.from(role.permissions); + if (updated.contains(key)) { + updated.remove(key); + } else { + updated.add(key); + } + + final roles = state.roles.map((r) { + if (r.id == role.id) return r.copyWith(permissions: updated); + return r; + }).toList(); + + state = state.copyWith(roles: roles); + } + + void savePermissions() { + // Permissions already in state; would sync to API here. + } + + void addUser(ManagedUser user) { + state = state.copyWith(users: [...state.users, user]); + _recountRoleUsers(); + } + + void addRole(ManagedRole role) { + state = state.copyWith( + roles: [...state.roles, role], + selectedRoleId: role.id, + ); + } + + void deleteRole(String roleId) { + if (roleId == 'super_admin') return; + state = state.copyWith( + roles: state.roles.where((r) => r.id != roleId).toList(), + ); + _recountRoleUsers(); + } + + void _recountRoleUsers() { + final roles = state.roles.map((role) { + final count = + state.users.where((u) => u.roleId == role.id).length; + return role.copyWith(userCount: count); + }).toList(); + state = state.copyWith(roles: roles); + } +} diff --git a/lib/modules/rbac/presentation/providers/role_form_provider.dart b/lib/modules/rbac/presentation/providers/role_form_provider.dart new file mode 100644 index 0000000..c89cdff --- /dev/null +++ b/lib/modules/rbac/presentation/providers/role_form_provider.dart @@ -0,0 +1,106 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/models/user_management_models.dart'; +import '../../../roles/data/repositories/role_repository_impl.dart'; +import '../../../roles/domain/usecases/role_usecases.dart'; +import '../../../roles/presentation/providers/roles_provider.dart'; + +class RoleFormState { + const RoleFormState({ + this.editingRole, + this.isSubmitting = false, + this.errorMessage, + }); + + final RoleCardModel? editingRole; + final bool isSubmitting; + final String? errorMessage; + + RoleFormState copyWith({ + RoleCardModel? editingRole, + bool? isSubmitting, + String? errorMessage, + bool clearError = false, + }) { + return RoleFormState( + editingRole: editingRole ?? this.editingRole, + isSubmitting: isSubmitting ?? this.isSubmitting, + errorMessage: clearError ? null : errorMessage ?? this.errorMessage, + ); + } +} + +final createRoleUseCaseProvider = Provider( + (ref) => CreateRoleUseCase(ref.watch(roleRepositoryProvider)), +); + +final updateRoleUseCaseProvider = Provider( + (ref) => UpdateRoleUseCase(ref.watch(roleRepositoryProvider)), +); + +final roleFormProvider = + AsyncNotifierProvider.family( + RoleFormNotifier.new, +); + +class RoleFormNotifier extends FamilyAsyncNotifier { + @override + Future build(String? roleId) async { + if (roleId == null) return const RoleFormState(); + + final cached = ref.read(rolesListProvider).valueOrNull?.roles; + var editingRole = cached?.where((role) => role.id == roleId).firstOrNull; + + if (editingRole == null) { + final result = await ref.read(getRoleCardsUseCaseProvider)(); + if (result.failure != null) throw result.failure!; + editingRole = result.data?.where((role) => role.id == roleId).firstOrNull; + } + + if (editingRole == null) { + throw StateError('Role not found'); + } + + return RoleFormState(editingRole: editingRole); + } + + Future submitCreate(CreateRoleRequest request) async { + final current = state.valueOrNull ?? const RoleFormState(); + state = AsyncData(current.copyWith(isSubmitting: true, clearError: true)); + + final result = await ref.read(createRoleUseCaseProvider)(request); + + if (result.failure != null) { + state = AsyncData( + current.copyWith( + isSubmitting: false, + errorMessage: result.failure!.message, + ), + ); + return false; + } + + state = AsyncData(current.copyWith(isSubmitting: false, clearError: true)); + return true; + } + + Future submitUpdate(String roleId, UpdateRoleRequest request) async { + final current = state.valueOrNull ?? const RoleFormState(); + state = AsyncData(current.copyWith(isSubmitting: true, clearError: true)); + + final result = await ref.read(updateRoleUseCaseProvider)(roleId, request); + + if (result.failure != null) { + state = AsyncData( + current.copyWith( + isSubmitting: false, + errorMessage: result.failure!.message, + ), + ); + return false; + } + + state = AsyncData(current.copyWith(isSubmitting: false, clearError: true)); + return true; + } +} diff --git a/lib/modules/rbac/presentation/screens/users_role_management_screen.dart b/lib/modules/rbac/presentation/screens/users_role_management_screen.dart new file mode 100644 index 0000000..5f5e383 --- /dev/null +++ b/lib/modules/rbac/presentation/screens/users_role_management_screen.dart @@ -0,0 +1,1335 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:intl/intl.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_confirmation_dialog.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_card.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../users/presentation/providers/users_provider.dart'; +import '../../domain/entities/rbac_entities.dart'; +import '../providers/add_user_form_provider.dart'; +import '../providers/role_form_provider.dart'; +import '../providers/rbac_provider.dart'; +import '../widgets/add_user_panel.dart'; +import '../widgets/create_role_panel.dart'; +import '../../../roles/presentation/providers/roles_provider.dart'; +import '../../../../shared/widgets/app_hover_effect.dart'; +import '../widgets/rbac_widgets.dart'; + +RbacTab rbacTabFromLocation(String location) { + if (location.startsWith(RouteConstants.roles)) return RbacTab.roles; + if (location.contains('tab=permissions')) return RbacTab.permissions; + return RbacTab.users; +} + +class UsersRoleManagementScreen extends ConsumerStatefulWidget { + const UsersRoleManagementScreen({super.key, this.initialTab}); + + final RbacTab? initialTab; + + @override + ConsumerState createState() => + _UsersRoleManagementScreenState(); +} + +class _UsersRoleManagementScreenState + extends ConsumerState { + @override + void initState() { + super.initState(); + final tab = widget.initialTab; + if (tab != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + ref.read(rbacProvider.notifier).setTab(tab); + }); + } + } + + Future _openUserPanel({String? userId}) async { + ref.invalidate(addUserFormProvider(userId)); + final saved = await showSidePanel( + context, + AddUserPanel(userId: userId), + ); + if (saved == true && mounted) { + ref.invalidate(usersListProvider); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + userId == null ? 'User added successfully' : 'User updated successfully', + ), + ), + ); + } + } + + Future _openAddUser() => _openUserPanel(); + + Future _openRolePanel({String? roleId}) async { + ref.invalidate(roleFormProvider(roleId)); + final saved = await showSidePanel( + context, + RoleFormPanel(roleId: roleId), + ); + if (saved == true && mounted) { + ref.invalidate(rolesListProvider); + ref.invalidate(usersListProvider); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + roleId == null ? 'Role created successfully' : 'Role updated successfully', + ), + ), + ); + } + } + + Future _openCreateRole() => _openRolePanel(); + + Future _editRole(RoleCardModel role) => _openRolePanel(roleId: role.id); + + Future _deleteRole(RoleCardModel role) async { + if (isProtectedRole(role)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Super Admin role cannot be deleted')), + ); + return; + } + + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Delete role', + message: 'Delete "${role.name}"? This is a soft delete.', + confirmLabel: 'Delete', + isDestructive: true, + ); + if (confirmed != true || !mounted) return; + + final success = await ref.read(rolesListProvider.notifier).deleteRole(role.id); + if (!mounted) return; + + if (success) { + ref.invalidate(usersListProvider); + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success ? 'Role deleted successfully' : 'Failed to delete role'), + ), + ); + } + + @override + Widget build(BuildContext context) { + final state = ref.watch(rbacProvider); + final usersAsync = ref.watch(usersListProvider); + final rolesAsync = ref.watch(rolesListProvider); + final usersState = usersAsync.valueOrNull; + final summary = usersState?.summary; + final roleCount = rolesAsync.valueOrNull?.roles.length ?? + summary?.rolesCount ?? + state.roles.length; + final isCompact = context.isMobile || MediaQuery.sizeOf(context).width < 900; + + return Padding( + padding: EdgeInsets.all(isCompact ? 16 : 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _Header( + compact: isCompact, + onNewRole: _openCreateRole, + onAddUser: _openAddUser, + ), + const SizedBox(height: 20), + LayoutBuilder( + builder: (context, constraints) { + final cardWidth = isCompact + ? constraints.maxWidth + : (constraints.maxWidth - 48) / 5; + return Wrap( + spacing: 12, + runSpacing: 12, + children: [ + SizedBox( + width: cardWidth.clamp(160, constraints.maxWidth), + child: RbacStatCard( + label: 'Total users', + value: '${summary?.totalUsers ?? usersState?.total ?? 0}', + icon: Icons.people_outline, + color: Color(0xFF2563EB), + ), + ), + SizedBox( + width: cardWidth.clamp(160, constraints.maxWidth), + child: RbacStatCard( + label: 'Active', + value: '${summary?.activeUsers ?? 0}', + icon: Icons.person_outline, + color: const Color(0xFF16A34A), + ), + ), + SizedBox( + width: cardWidth.clamp(160, constraints.maxWidth), + child: RbacStatCard( + label: 'Inactive', + value: '${summary?.inactiveUsers ?? 0}', + icon: Icons.person_off_outlined, + color: const Color(0xFFEA580C), + ), + ), + SizedBox( + width: cardWidth.clamp(160, constraints.maxWidth), + child: RbacStatCard( + label: 'Locked', + value: '${summary?.lockedUsers ?? 0}', + icon: Icons.lock_outline, + color: const Color(0xFFDC2626), + ), + ), + SizedBox( + width: cardWidth.clamp(160, constraints.maxWidth), + child: RbacStatCard( + label: 'Roles defined', + value: '${summary?.rolesCount ?? state.roles.length}', + icon: Icons.shield_outlined, + color: const Color(0xFF16A34A), + ), + ), + ], + ); + }, + ), + const SizedBox(height: 20), + _TabBar( + state: state, + userCount: summary?.totalUsers ?? usersState?.total ?? state.totalUsers, + roleCount: roleCount, + ), + const SizedBox(height: 16), + Expanded( + child: switch (state.selectedTab) { + RbacTab.users => _UsersTab( + onAddUser: _openAddUser, + onEditUser: (user) => _openUserPanel(userId: user.id), + ), + RbacTab.roles => _RolesTab( + onNewRole: _openCreateRole, + onEditRole: _editRole, + onDeleteRole: _deleteRole, + ), + RbacTab.permissions => const _PermissionMatrixTab(), + }, + ), + ], + ), + ); + } +} + +class _Header extends StatelessWidget { + const _Header({ + required this.compact, + required this.onNewRole, + required this.onAddUser, + }); + + final bool compact; + final VoidCallback onNewRole; + final VoidCallback onAddUser; + + @override + Widget build(BuildContext context) { + final title = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Users & Role Management', + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + 'Manage system users, assign roles, and configure module permissions.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ); + + final actions = Wrap( + spacing: 12, + runSpacing: 8, + children: [ + OutlinedButton.icon( + onPressed: onNewRole, + icon: const Icon(Icons.add_circle_outline, size: 18), + label: const Text('New Role'), + ), + ElevatedButton.icon( + onPressed: onAddUser, + icon: const Icon(Icons.person_add_outlined, size: 18), + label: const Text('Add User'), + ), + ], + ); + + if (compact) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [title, const SizedBox(height: 12), actions], + ); + } + + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded(child: title), + actions, + ], + ); + } +} + +class _TabBar extends ConsumerWidget { + const _TabBar({ + required this.state, + required this.userCount, + required this.roleCount, + }); + + final RbacState state; + final int userCount; + final int roleCount; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + _TabButton( + label: 'Users ($userCount)', + icon: Icons.people_outline, + selected: state.selectedTab == RbacTab.users, + onTap: () => ref.read(rbacProvider.notifier).setTab(RbacTab.users), + ), + _TabButton( + label: 'Roles ($roleCount)', + icon: Icons.shield_outlined, + selected: state.selectedTab == RbacTab.roles, + onTap: () => ref.read(rbacProvider.notifier).setTab(RbacTab.roles), + ), + _TabButton( + label: 'Permission Matrix', + icon: Icons.vpn_key_outlined, + selected: state.selectedTab == RbacTab.permissions, + onTap: () => + ref.read(rbacProvider.notifier).setTab(RbacTab.permissions), + ), + ], + ), + ); + } +} + +class _TabButton extends StatelessWidget { + const _TabButton({ + required this.label, + required this.icon, + required this.selected, + required this.onTap, + }); + + final String label; + final IconData icon; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final primary = Theme.of(context).colorScheme.primary; + return InkWell( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: selected ? primary : Colors.transparent, + width: 2, + ), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 18, + color: selected ? primary : Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 8), + Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: selected + ? primary + : Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + ), + ), + ], + ), + ), + ); + } +} + +class _UsersTab extends ConsumerStatefulWidget { + const _UsersTab({ + required this.onAddUser, + required this.onEditUser, + }); + + final VoidCallback onAddUser; + final void Function(ManagedUserModel user) onEditUser; + + @override + ConsumerState<_UsersTab> createState() => _UsersTabState(); +} + +class _UsersTabState extends ConsumerState<_UsersTab> { + static const _tableMinWidth = 1040.0; + + String? _selectedRoleName; + String? _selectedDepartmentName; + String? _selectedStatusLabel; + + String _formatLastLogin(DateTime? value) { + if (value == null) return '—'; + return DateFormat('MMM d, yyyy h:mm a').format(value.toLocal()); + } + + int? _roleIdForFilter(UserFiltersModel? filters) { + if (_selectedRoleName == null || filters == null) return null; + for (final role in filters.roles) { + if (role.name == _selectedRoleName) { + return int.tryParse(role.id); + } + } + return null; + } + + int? _departmentIdForFilter(UserFiltersModel? filters) { + if (_selectedDepartmentName == null || filters == null) return null; + for (final department in filters.departments) { + if (department.name == _selectedDepartmentName) { + return int.tryParse(department.id); + } + } + return null; + } + + String? _statusValueForFilter(UserFiltersModel? filters) { + if (_selectedStatusLabel == null || filters == null) return null; + for (final status in filters.statuses) { + if (status.name == _selectedStatusLabel) { + return status.id; + } + } + return null; + } + + void _applyFilters(UserFiltersModel? filters) { + final current = ref.read(usersListProvider).valueOrNull; + ref.read(usersListProvider.notifier).applyQuery( + (current?.query ?? const UserListQuery(limit: 10)).copyWith( + page: 1, + roleId: _roleIdForFilter(filters), + departmentId: _departmentIdForFilter(filters), + status: _statusValueForFilter(filters), + ), + ); + } + + void _editUser(ManagedUserModel user) { + widget.onEditUser(user); + } + + Future _resetPassword(ManagedUserModel user) async { + final controller = TextEditingController(); + final password = await showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: const Text('Reset password'), + content: TextField( + controller: controller, + obscureText: true, + autofocus: true, + decoration: const InputDecoration( + labelText: 'New temporary password', + hintText: 'Min. 8 characters', + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + final value = controller.text.trim(); + if (value.length < 8) return; + Navigator.of(dialogContext).pop(value); + }, + child: const Text('Reset'), + ), + ], + ), + ); + controller.dispose(); + if (password == null || password.isEmpty || !mounted) return; + + final result = await ref.read(updateUserUseCaseProvider)( + user.id, + UpdateUserRequest(password: password), + ); + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + result.failure == null + ? 'Password reset for ${user.fullName}' + : result.failure!.message, + ), + ), + ); + } + + Future _deactivateUser(ManagedUserModel user) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Deactivate user', + message: 'Deactivate ${user.fullName}? This is a soft delete.', + confirmLabel: 'Deactivate', + isDestructive: true, + ); + if (confirmed != true || !mounted) return; + + final success = await ref.read(usersListProvider.notifier).deactivateUser(user.id); + if (!mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success ? 'User deactivated' : 'Failed to deactivate user'), + ), + ); + } + + @override + Widget build(BuildContext context) { + final usersAsync = ref.watch(usersListProvider); + + return usersAsync.when( + loading: () => const AppCard( + enableHover: false, + child: AppLoadingView(message: 'Loading users...'), + ), + error: (error, _) => AppCard( + enableHover: false, + child: ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(usersListProvider), + ), + ), + data: (usersState) { + final filters = usersState.filters; + final roles = ['All roles', ...?filters?.roles.map((r) => r.name)]; + final departments = [ + 'All departments', + ...?filters?.departments.map((d) => d.name), + ]; + final statuses = [ + 'All statuses', + ...?filters?.statuses.map((s) => s.name), + ]; + final roleFilter = _selectedRoleName ?? 'All roles'; + final departmentFilter = _selectedDepartmentName ?? 'All departments'; + final statusFilter = _selectedStatusLabel ?? 'All statuses'; + final page = usersState.query.page; + final pageSize = usersState.query.limit; + final total = usersState.total; + final start = total == 0 ? 0 : ((page - 1) * pageSize) + 1; + final end = (page * pageSize).clamp(0, total); + + return AppCard( + enableHover: false, + clipBehavior: Clip.antiAlias, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.12), + ), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: LayoutBuilder( + builder: (context, constraints) { + final useWrappedFilters = constraints.maxWidth < 1000; + return _UsersFilterBar( + wrapped: useWrappedFilters, + roleFilter: roleFilter, + departmentFilter: departmentFilter, + statusFilter: statusFilter, + roles: roles, + departments: departments, + statuses: statuses, + onSearch: ref.read(usersListProvider.notifier).setSearch, + onRoleChanged: (value) { + setState(() { + _selectedRoleName = value == 'All roles' ? null : value; + }); + _applyFilters(filters); + }, + onDepartmentChanged: (value) { + setState(() { + _selectedDepartmentName = + value == 'All departments' ? null : value; + }); + _applyFilters(filters); + }, + onStatusChanged: (value) { + setState(() { + _selectedStatusLabel = + value == 'All statuses' ? null : value; + }); + _applyFilters(filters); + }, + ); + }, + ), + ), + const Divider(height: 1), + if (usersState.users.isEmpty) + Expanded( + child: Center( + child: Text( + 'No users found', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ) + else + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { + final tableWidth = constraints.maxWidth < _tableMinWidth + ? _tableMinWidth + : constraints.maxWidth; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: BoxConstraints(minWidth: tableWidth), + child: DataTable( + headingRowColor: WidgetStateProperty.all( + Theme.of(context) + .colorScheme + .surfaceContainerHighest + .withValues(alpha: 0.4), + ), + columns: const [ + DataColumn(label: Text('USER')), + DataColumn(label: Text('EMPLOYEE CODE')), + DataColumn(label: Text('ROLE')), + DataColumn(label: Text('DEPARTMENT')), + DataColumn(label: Text('PLANT')), + DataColumn(label: Text('LAST LOGIN')), + DataColumn(label: Text('STATUS')), + DataColumn(label: Text('')), + ], + rows: usersState.users.map((user) { + final status = userStatusFromApi(user.status); + return DataRow( + cells: [ + DataCell( + Row( + children: [ + UserAvatarChip( + name: user.fullName, + initials: user.initialsDisplay, + ), + const SizedBox(width: 10), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + user.fullName, + style: const TextStyle( + fontWeight: FontWeight.w600, + ), + ), + Text( + user.email, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ], + ), + ), + DataCell(Text(user.employeeCode)), + DataCell(RoleBadge(label: user.roleLabel)), + DataCell(Text(user.departmentLabel)), + DataCell(Text(user.plantLabel)), + DataCell(Text(_formatLastLogin(user.lastLoginAt))), + DataCell( + StatusBadge( + label: status.label, + color: status.color, + ), + ), + DataCell( + UserTableActions( + user: user, + onEdit: () => _editUser(user), + onResetPassword: () => _resetPassword(user), + onDeactivate: () => _deactivateUser(user), + ), + ), + ], + ); + }).toList(), + ), + ), + ); + }, + ), + ), + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Text( + 'Showing $start–$end of $total users', + style: Theme.of(context).textTheme.bodySmall, + ), + const Spacer(), + TextButton( + onPressed: page > 1 + ? () => ref.read(usersListProvider.notifier).setPage(page - 1) + : null, + child: const Text('Previous'), + ), + ...List.generate(usersState.totalPages.clamp(0, 4), (i) { + final pageIndex = i + 1; + final selected = page == pageIndex; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: Material( + color: selected + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => ref + .read(usersListProvider.notifier) + .setPage(pageIndex), + child: SizedBox( + width: 36, + height: 36, + child: Center( + child: Text( + '$pageIndex', + style: TextStyle( + color: selected + ? Theme.of(context).colorScheme.onPrimary + : null, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ), + ), + ); + }), + TextButton( + onPressed: page < usersState.totalPages + ? () => ref.read(usersListProvider.notifier).setPage(page + 1) + : null, + child: const Text('Next'), + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} + +class _UsersFilterBar extends StatelessWidget { + const _UsersFilterBar({ + required this.wrapped, + required this.roleFilter, + required this.departmentFilter, + required this.statusFilter, + required this.roles, + required this.departments, + required this.statuses, + required this.onSearch, + required this.onRoleChanged, + required this.onDepartmentChanged, + required this.onStatusChanged, + }); + + final bool wrapped; + final String roleFilter; + final String departmentFilter; + final String statusFilter; + final List roles; + final List departments; + final List statuses; + final ValueChanged onSearch; + final ValueChanged onRoleChanged; + final ValueChanged onDepartmentChanged; + final ValueChanged onStatusChanged; + + @override + Widget build(BuildContext context) { + final searchField = TextField( + decoration: const InputDecoration( + hintText: 'Search by name, email, employee code...', + prefixIcon: Icon(Icons.search, size: 20), + isDense: true, + ), + onChanged: onSearch, + ); + + final filters = [ + _FilterDropdown( + value: roleFilter, + label: 'Role', + items: roles, + onChanged: onRoleChanged, + ), + _FilterDropdown( + value: departmentFilter, + label: 'Department', + items: departments, + onChanged: onDepartmentChanged, + ), + _FilterDropdown( + value: statusFilter, + label: 'Status', + items: statuses, + onChanged: onStatusChanged, + ), + OutlinedButton.icon( + onPressed: () {}, + icon: const Icon(Icons.download_outlined, size: 18), + label: const Text('Export'), + ), + ]; + + if (wrapped) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + searchField, + const SizedBox(height: 12), + Wrap(spacing: 12, runSpacing: 12, children: filters), + ], + ); + } + + return Row( + children: [ + Expanded(flex: 3, child: searchField), + const SizedBox(width: 12), + Expanded(flex: 2, child: filters[0]), + const SizedBox(width: 12), + Expanded(flex: 2, child: filters[1]), + const SizedBox(width: 12), + Expanded(child: filters[2]), + const SizedBox(width: 12), + filters[3], + ], + ); + } +} + +class _FilterDropdown extends StatelessWidget { + const _FilterDropdown({ + required this.value, + required this.label, + required this.items, + required this.onChanged, + }); + + final String value; + final String label; + final List items; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return AppSearchableDropdown( + label: label, + value: value, + isDense: true, + searchHint: 'Search $label...', + options: items + .map((item) => AppDropdownOption(value: item, label: item)) + .toList(), + onChanged: (v) { + if (v != null) onChanged(v); + }, + ); + } +} + +class _RolesTab extends ConsumerWidget { + const _RolesTab({ + required this.onNewRole, + required this.onEditRole, + required this.onDeleteRole, + }); + + final VoidCallback onNewRole; + final void Function(RoleCardModel role) onEditRole; + final Future Function(RoleCardModel role) onDeleteRole; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final rolesAsync = ref.watch(rolesListProvider); + + return rolesAsync.when( + loading: () => const AppCard( + enableHover: false, + child: AppLoadingView(message: 'Loading roles...'), + ), + error: (error, _) => AppCard( + enableHover: false, + child: ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(rolesListProvider), + ), + ), + data: (rolesState) { + final roles = rolesState.roles; + + return GridView.builder( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 320, + mainAxisExtent: 190, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + ), + itemCount: roles.length + 1, + itemBuilder: (context, index) { + if (index == roles.length) { + return AppHoverEffect( + onTap: onNewRole, + showHoverBorder: false, + child: CustomPaint( + painter: _DashedBorderPainter( + color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.5), + radius: 12, + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.add, + size: 32, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(height: 8), + Text( + 'New role', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } + + final role = roles[index]; + final appearance = roleCardAppearance(index); + + return AppCard( + elevation: 0, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: appearance.color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(appearance.icon, color: appearance.color, size: 20), + ), + const Spacer(), + IconButton( + icon: const Icon(Icons.edit_outlined, size: 18), + onPressed: () => onEditRole(role), + visualDensity: VisualDensity.compact, + ), + if (!isProtectedRole(role)) + IconButton( + icon: const Icon(Icons.delete_outline, size: 18), + onPressed: () => onDeleteRole(role), + visualDensity: VisualDensity.compact, + ), + ], + ), + const SizedBox(height: 8), + Text( + role.name, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Expanded( + child: Text( + role.description ?? '—', + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Row( + children: [ + Icon( + Icons.people_outline, + size: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + '${role.userCount} users', + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(width: 16), + Icon( + Icons.vpn_key_outlined, + size: 14, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + '${role.permissionCount} permissions', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ], + ), + ), + ); + }, + ); + }, + ); + } +} + +class _PermissionMatrixTab extends ConsumerStatefulWidget { + const _PermissionMatrixTab(); + + @override + ConsumerState<_PermissionMatrixTab> createState() => + _PermissionMatrixTabState(); +} + +class _PermissionMatrixTabState extends ConsumerState<_PermissionMatrixTab> { + @override + Widget build(BuildContext context) { + final state = ref.watch(rbacProvider); + final role = state.selectedRole; + + return AppCard( + enableHover: false, + clipBehavior: Clip.antiAlias, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.12), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(20, 20, 20, 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Role permission matrix', + style: + Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + Text( + 'Select a role below and toggle module permissions.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ], + ), + ), + ElevatedButton.icon( + onPressed: role == null + ? null + : () { + ref.read(rbacProvider.notifier).savePermissions(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.check_circle, + color: Colors.white, size: 18), + const SizedBox(width: 8), + Text( + 'Permissions saved for ${role.name}', + ), + ], + ), + backgroundColor: const Color(0xFF16A34A), + behavior: SnackBarBehavior.floating, + ), + ); + }, + icon: const Icon(Icons.save_outlined, size: 18), + label: const Text('Save changes'), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: state.roles.map((r) { + return Padding( + padding: const EdgeInsets.only(right: 8), + child: RolePill( + label: r.name, + selected: r.id == state.selectedRoleId, + onTap: () => + ref.read(rbacProvider.notifier).selectRole(r.id), + ), + ); + }).toList(), + ), + ), + ), + const Divider(height: 1), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: role == null + ? const SizedBox.shrink() + : Table( + columnWidths: const { + 0: FlexColumnWidth(2.5), + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + 3: FlexColumnWidth(1), + 4: FlexColumnWidth(1), + 5: FlexColumnWidth(1), + 6: FlexColumnWidth(1), + }, + border: TableBorder( + horizontalInside: BorderSide( + color: Theme.of(context) + .colorScheme + .outline + .withValues(alpha: 0.12), + ), + ), + children: [ + TableRow( + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .surfaceContainerHighest + .withValues(alpha: 0.4), + ), + children: [ + const _MatrixHeader('MODULE'), + ...RbacAction.values + .map((a) => _MatrixHeader(a.label)), + ], + ), + ...rbacModules.map((module) { + return TableRow( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + Container( + width: 28, + height: 28, + decoration: BoxDecoration( + color: module.color + .withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(6), + ), + child: Icon( + module.icon, + size: 16, + color: module.color, + ), + ), + const SizedBox(width: 10), + Text( + module.label, + style: const TextStyle( + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ...RbacAction.values.map((action) { + final checked = + role.hasPermission(module.key, action); + return Padding( + padding: + const EdgeInsets.symmetric(vertical: 4), + child: Center( + child: Checkbox( + value: checked, + onChanged: (_) => ref + .read(rbacProvider.notifier) + .togglePermission( + module.key, + action, + ), + ), + ), + ); + }), + ], + ); + }), + ], + ), + ), + ), + ], + ), + ); + } +} + +class _MatrixHeader extends StatelessWidget { + const _MatrixHeader(this.label); + + final String label; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4), + child: Text( + label, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + ); + } +} + +class _DashedBorderPainter extends CustomPainter { + _DashedBorderPainter({required this.color, required this.radius}); + + final Color color; + final double radius; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + final path = Path() + ..addRRect( + RRect.fromRectAndRadius( + Rect.fromLTWH(0, 0, size.width, size.height), + Radius.circular(radius), + ), + ); + + const dashWidth = 6.0; + const dashSpace = 4.0; + for (final metric in path.computeMetrics()) { + var distance = 0.0; + while (distance < metric.length) { + final end = distance + dashWidth; + canvas.drawPath( + metric.extractPath(distance, end.clamp(0, metric.length)), + paint, + ); + distance += dashWidth + dashSpace; + } + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/modules/rbac/presentation/widgets/add_user_panel.dart b/lib/modules/rbac/presentation/widgets/add_user_panel.dart new file mode 100644 index 0000000..464d607 --- /dev/null +++ b/lib/modules/rbac/presentation/widgets/add_user_panel.dart @@ -0,0 +1,359 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../providers/add_user_form_provider.dart'; +import 'rbac_widgets.dart'; + +class AddUserPanel extends ConsumerStatefulWidget { + const AddUserPanel({super.key, this.userId}); + + final String? userId; + + bool get isEditing => userId != null; + + @override + ConsumerState createState() => _AddUserPanelState(); +} + +class _AddUserPanelState extends ConsumerState { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _employeeCodeController = TextEditingController(); + final _emailController = TextEditingController(); + final _mobileController = TextEditingController(); + final _passwordController = TextEditingController(); + String? _selectedRoleId; + String _selectedStatus = 'Active'; + String? _selectedDepartmentId; + String? _selectedPlantId; + String? _selectedDesignationId; + String? _selectedReportingToId; + bool _prefilled = false; + + static const _statusOptions = [ + AppDropdownOption(value: 'Active', label: 'Active'), + AppDropdownOption(value: 'Inactive', label: 'Inactive'), + AppDropdownOption(value: 'Locked', label: 'Locked'), + ]; + + @override + void dispose() { + _nameController.dispose(); + _employeeCodeController.dispose(); + _emailController.dispose(); + _mobileController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + String _statusToApi(String label) => switch (label) { + 'Active' => 'active', + 'Inactive' => 'inactive', + 'Locked' => 'locked', + _ => 'active', + }; + + String _statusFromApi(String status) => switch (status.toLowerCase()) { + 'active' => 'Active', + 'inactive' => 'Inactive', + 'locked' => 'Locked', + _ => 'Active', + }; + + void _prefillFromUser(ManagedUserModel user) { + if (_prefilled) return; + _prefilled = true; + _nameController.text = user.fullName; + _employeeCodeController.text = user.employeeCode; + _emailController.text = user.email; + _mobileController.text = user.mobile; + _selectedRoleId = user.roleId; + _selectedDepartmentId = user.departmentId; + _selectedDesignationId = user.designationId; + _selectedPlantId = user.plantId; + _selectedReportingToId = user.reportingTo; + _selectedStatus = _statusFromApi(user.status); + } + + List> _toOptions(List items) { + return items + .map((item) => AppDropdownOption(value: item.id, label: item.name)) + .toList(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + final roleId = int.tryParse(_selectedRoleId ?? ''); + if (roleId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please select a role')), + ); + return; + } + + final notifier = ref.read(addUserFormProvider(widget.userId).notifier); + final bool success; + + if (widget.isEditing) { + success = await notifier.submitUpdate( + widget.userId!, + UpdateUserRequest( + employeeCode: _employeeCodeController.text.trim(), + fullName: _nameController.text.trim(), + email: _emailController.text.trim(), + password: _passwordController.text.isEmpty ? null : _passwordController.text, + mobile: _mobileController.text.trim().isEmpty + ? null + : _mobileController.text.trim(), + roleId: roleId, + departmentId: int.tryParse(_selectedDepartmentId ?? ''), + designationId: int.tryParse(_selectedDesignationId ?? ''), + plantId: int.tryParse(_selectedPlantId ?? ''), + reportingTo: int.tryParse(_selectedReportingToId ?? ''), + status: _statusToApi(_selectedStatus), + isActive: _selectedStatus == 'Active', + ), + ); + } else { + success = await notifier.submitCreate( + CreateUserRequest( + employeeCode: _employeeCodeController.text.trim(), + fullName: _nameController.text.trim(), + email: _emailController.text.trim(), + password: _passwordController.text, + mobile: _mobileController.text.trim().isEmpty + ? null + : _mobileController.text.trim(), + roleId: roleId, + departmentId: int.tryParse(_selectedDepartmentId ?? ''), + designationId: int.tryParse(_selectedDesignationId ?? ''), + plantId: int.tryParse(_selectedPlantId ?? ''), + reportingTo: int.tryParse(_selectedReportingToId ?? ''), + status: _statusToApi(_selectedStatus), + isActive: _selectedStatus == 'Active', + ), + ); + } + + if (!mounted) return; + + if (success) { + Navigator.of(context, rootNavigator: true).pop(true); + return; + } + + final error = ref.read(addUserFormProvider(widget.userId)).valueOrNull?.errorMessage; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + error ?? (widget.isEditing ? 'Failed to update user' : 'Failed to create user'), + ), + ), + ); + } + + Widget _buildDropdown({ + required String label, + required String? value, + required List options, + required ValueChanged onChanged, + String? hint, + bool required = false, + }) { + return AppSearchableDropdown( + label: required ? '$label *' : label, + value: value, + hint: hint ?? 'Select ${label.toLowerCase()}', + searchHint: 'Search $label...', + enabled: options.isNotEmpty, + options: _toOptions(options), + onChanged: onChanged, + validator: required ? (v) => v == null ? 'Please select $label' : null : null, + ); + } + + @override + Widget build(BuildContext context) { + final formAsync = ref.watch(addUserFormProvider(widget.userId)); + final isSubmitting = formAsync.valueOrNull?.isSubmitting ?? false; + + return SidePanelScaffold( + title: widget.isEditing ? 'Edit user' : 'Add new user', + footer: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: isSubmitting + ? null + : () => Navigator.of(context, rootNavigator: true).pop(), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: AppButton( + label: widget.isEditing ? 'Update user' : 'Save user', + expand: true, + isLoading: isSubmitting, + onPressed: isSubmitting ? null : _save, + ), + ), + ], + ), + child: formAsync.when( + loading: () => AppLoadingView( + message: widget.isEditing ? 'Loading user...' : 'Loading form options...', + ), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(addUserFormProvider(widget.userId)), + ), + data: (formState) { + if (formState.editingUser != null) { + if (!_prefilled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _prefilled) return; + setState(() => _prefillFromUser(formState.editingUser!)); + }); + } + } else { + _selectedRoleId ??= + formState.roles.isNotEmpty ? formState.roles.first.id : null; + } + + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SidePanelSection( + title: 'PERSONAL DETAILS', + children: [ + AppTextField( + controller: _nameController, + label: 'Full name *', + hint: 'e.g. Ravi Kumar', + validator: (v) => Validators.required(v, fieldName: 'Name'), + ), + const SizedBox(height: 12), + AppTextField( + controller: _employeeCodeController, + label: 'Employee code *', + hint: 'e.g. EMP002', + validator: (v) => + Validators.required(v, fieldName: 'Employee code'), + ), + const SizedBox(height: 12), + AppTextField( + controller: _emailController, + label: 'Email *', + hint: 'ravi@company.com', + keyboardType: TextInputType.emailAddress, + validator: Validators.email, + ), + const SizedBox(height: 12), + AppTextField( + controller: _mobileController, + label: 'Mobile', + hint: '9XXXXXXXXX', + keyboardType: TextInputType.phone, + ), + ], + ), + SidePanelSection( + title: 'ROLE & ACCESS', + children: [ + _buildDropdown( + label: 'Role', + value: _selectedRoleId, + options: formState.roles, + required: true, + onChanged: (v) => setState(() => _selectedRoleId = v), + ), + const SizedBox(height: 12), + AppSearchableDropdown( + label: 'Status', + value: _selectedStatus, + options: _statusOptions, + searchHint: 'Search status...', + onChanged: (v) => setState(() => _selectedStatus = v ?? 'Active'), + ), + ], + ), + SidePanelSection( + title: 'ORGANISATION', + children: [ + _buildDropdown( + label: 'Department', + value: _selectedDepartmentId, + options: formState.departments, + onChanged: (v) => setState(() => _selectedDepartmentId = v), + ), + const SizedBox(height: 12), + _buildDropdown( + label: 'Designation', + value: _selectedDesignationId, + options: formState.designations, + onChanged: (v) => setState(() => _selectedDesignationId = v), + ), + const SizedBox(height: 12), + _buildDropdown( + label: 'Plant / Unit', + value: _selectedPlantId, + options: formState.plants, + onChanged: (v) => setState(() => _selectedPlantId = v), + ), + const SizedBox(height: 12), + _buildDropdown( + label: 'Reporting to', + value: _selectedReportingToId, + options: formState.managers, + hint: formState.managers.isEmpty + ? 'No managers available' + : 'Select manager', + onChanged: (v) => setState(() => _selectedReportingToId = v), + ), + ], + ), + SidePanelSection( + title: 'PASSWORD', + children: [ + AppTextField( + controller: _passwordController, + label: widget.isEditing + ? 'New password' + : 'Temporary password *', + hint: 'Min. 8 characters', + obscureText: true, + validator: widget.isEditing + ? null + : (v) => Validators.required(v, fieldName: 'Password'), + ), + Text( + widget.isEditing + ? 'Leave blank to keep the current password.' + : 'User will be asked to change this on first login.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ], + ), + ); + }, + ), + ); + } +} diff --git a/lib/modules/rbac/presentation/widgets/create_role_panel.dart b/lib/modules/rbac/presentation/widgets/create_role_panel.dart new file mode 100644 index 0000000..7d73dcb --- /dev/null +++ b/lib/modules/rbac/presentation/widgets/create_role_panel.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../domain/entities/rbac_entities.dart'; +import '../providers/role_form_provider.dart'; +import 'rbac_widgets.dart'; + +class RoleFormPanel extends ConsumerStatefulWidget { + const RoleFormPanel({super.key, this.roleId}); + + final String? roleId; + + bool get isEditing => roleId != null; + + @override + ConsumerState createState() => _RoleFormPanelState(); +} + +class _RoleFormPanelState extends ConsumerState { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _descriptionController = TextEditingController(); + final Map _viewPermissions = { + for (final module in rbacModules) module.key: false, + }; + bool _prefilled = false; + + @override + void dispose() { + _nameController.dispose(); + _descriptionController.dispose(); + super.dispose(); + } + + void _prefillFromRole(RoleCardModel role) { + if (_prefilled) return; + _prefilled = true; + _nameController.text = role.name; + _descriptionController.text = role.description ?? ''; + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + final notifier = ref.read(roleFormProvider(widget.roleId).notifier); + final bool success; + + if (widget.isEditing) { + success = await notifier.submitUpdate( + widget.roleId!, + UpdateRoleRequest( + name: _nameController.text.trim(), + description: _descriptionController.text.trim(), + ), + ); + } else { + success = await notifier.submitCreate( + CreateRoleRequest( + name: _nameController.text.trim(), + description: _descriptionController.text.trim(), + ), + ); + } + + if (!mounted) return; + + if (success) { + Navigator.of(context, rootNavigator: true).pop(true); + return; + } + + final error = ref.read(roleFormProvider(widget.roleId)).valueOrNull?.errorMessage; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + error ?? + (widget.isEditing ? 'Failed to update role' : 'Failed to create role'), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final formAsync = ref.watch(roleFormProvider(widget.roleId)); + final isSubmitting = formAsync.valueOrNull?.isSubmitting ?? false; + + return SidePanelScaffold( + title: widget.isEditing ? 'Edit role' : 'Create new role', + footer: Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: isSubmitting + ? null + : () => Navigator.of(context, rootNavigator: true).pop(), + child: const Text('Cancel'), + ), + ), + const SizedBox(width: 12), + Expanded( + child: AppButton( + label: widget.isEditing ? 'Update role' : 'Create role', + expand: true, + isLoading: isSubmitting, + onPressed: isSubmitting ? null : _save, + ), + ), + ], + ), + child: formAsync.when( + loading: () => AppLoadingView( + message: widget.isEditing ? 'Loading role...' : 'Loading form...', + ), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(roleFormProvider(widget.roleId)), + ), + data: (formState) { + if (formState.editingRole != null && !_prefilled) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _prefilled) return; + setState(() => _prefillFromRole(formState.editingRole!)); + }); + } + + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppTextField( + controller: _nameController, + label: 'Role name', + hint: 'e.g. QC Manager', + validator: (v) => Validators.required(v, fieldName: 'Role name'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _descriptionController, + label: 'Description', + hint: 'What does this role do?', + maxLines: 3, + ), + if (!widget.isEditing) ...[ + const SizedBox(height: 24), + Text( + 'INITIAL PERMISSIONS', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.8, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + Text( + 'You can fine-tune permissions in the Permission Matrix tab after creating the role.', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 16), + ...rbacModules.map((module) { + return ModulePermissionRow( + icon: module.icon, + color: module.color, + label: module.label, + value: _viewPermissions[module.key] ?? false, + onChanged: (v) => + setState(() => _viewPermissions[module.key] = v), + ); + }), + ], + ], + ), + ); + }, + ), + ); + } +} + +/// Backwards-compatible alias. +typedef CreateRolePanel = RoleFormPanel; diff --git a/lib/modules/rbac/presentation/widgets/rbac_widgets.dart b/lib/modules/rbac/presentation/widgets/rbac_widgets.dart new file mode 100644 index 0000000..1671b48 --- /dev/null +++ b/lib/modules/rbac/presentation/widgets/rbac_widgets.dart @@ -0,0 +1,457 @@ +import 'package:flutter/material.dart'; + +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_card.dart'; + +bool isProtectedRole(RoleCardModel role) => + role.id == '1' || role.name.toLowerCase() == 'super admin'; + +({Color color, IconData icon}) roleCardAppearance(int index) { + const styles = [ + (color: Color(0xFF2563EB), icon: Icons.shield_outlined), + (color: Color(0xFF16A34A), icon: Icons.admin_panel_settings_outlined), + (color: Color(0xFFCA8A04), icon: Icons.receipt_long_outlined), + (color: Color(0xFF0891B2), icon: Icons.inventory_outlined), + (color: Color(0xFF7C3AED), icon: Icons.account_balance_wallet_outlined), + (color: Color(0xFFEA580C), icon: Icons.inventory_2_outlined), + ]; + return styles[index % styles.length]; +} + +class RbacStatCard extends StatelessWidget { + const RbacStatCard({ + super.key, + required this.label, + required this.value, + required this.icon, + required this.color, + }); + + final String label; + final String value; + final IconData icon; + final Color color; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AppCard( + elevation: 0, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon(icon, size: 20, color: color), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + value, + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + Text( + label, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} + +class StatusBadge extends StatelessWidget { + const StatusBadge({super.key, required this.label, required this.color}); + + final String label; + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Text( + label, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} + +class RoleBadge extends StatelessWidget { + const RoleBadge({super.key, required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + final primary = Theme.of(context).colorScheme.primary; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: primary.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration(color: primary, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Text( + label, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: primary, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ); + } +} + +class UserAvatarChip extends StatelessWidget { + const UserAvatarChip({super.key, required this.name, this.initials}); + + final String name; + final String? initials; + + @override + Widget build(BuildContext context) { + final colors = [ + const Color(0xFF2563EB), + const Color(0xFF0891B2), + const Color(0xFF7C3AED), + const Color(0xFFEA580C), + const Color(0xFF16A34A), + ]; + final color = colors[name.hashCode.abs() % colors.length]; + final display = initials ?? + (name.isNotEmpty ? name.trim()[0].toUpperCase() : 'U'); + + return CircleAvatar( + radius: 20, + backgroundColor: color.withValues(alpha: 0.12), + child: Text( + display.length > 2 ? display.substring(0, 2) : display, + style: TextStyle( + color: color, + fontWeight: FontWeight.w700, + fontSize: 12, + ), + ), + ); + } +} + +class UserTableActions extends StatelessWidget { + const UserTableActions({ + super.key, + required this.user, + required this.onEdit, + required this.onResetPassword, + required this.onDeactivate, + }); + + final ManagedUserModel user; + final VoidCallback onEdit; + final VoidCallback onResetPassword; + final VoidCallback onDeactivate; + + @override + Widget build(BuildContext context) { + final muted = Theme.of(context).colorScheme.onSurfaceVariant; + + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + tooltip: 'Edit user', + icon: Icon(Icons.edit_outlined, size: 18, color: muted), + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + onPressed: onEdit, + ), + IconButton( + tooltip: 'Reset password', + icon: Icon(Icons.vpn_key_outlined, size: 18, color: muted), + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + onPressed: onResetPassword, + ), + IconButton( + tooltip: 'Deactivate user', + icon: Icon(Icons.person_off_outlined, size: 18, color: muted), + visualDensity: VisualDensity.compact, + padding: EdgeInsets.zero, + constraints: const BoxConstraints(minWidth: 32, minHeight: 32), + onPressed: onDeactivate, + ), + ], + ); + } +} + +class RolePill extends StatelessWidget { + const RolePill({ + super.key, + required this.label, + required this.selected, + required this.onTap, + }); + + final String label; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final primary = Theme.of(context).colorScheme.primary; + return Material( + color: selected ? primary : Theme.of(context).colorScheme.surface, + borderRadius: BorderRadius.circular(24), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(24), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: selected + ? primary + : Theme.of(context).colorScheme.outline.withValues(alpha: 0.35), + ), + ), + child: Text( + label, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: selected + ? Theme.of(context).colorScheme.onPrimary + : Theme.of(context).colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + } +} + +Future showSidePanel(BuildContext context, Widget panel) { + final width = MediaQuery.sizeOf(context).width; + final panelWidth = width > 1200 ? 480.0 : (width * 0.38).clamp(360.0, 480.0); + + return showGeneralDialog( + context: context, + useRootNavigator: true, + barrierDismissible: true, + barrierLabel: 'Dismiss', + barrierColor: Colors.black.withValues(alpha: 0.35), + transitionDuration: const Duration(milliseconds: 280), + pageBuilder: (context, _, __) { + final theme = Theme.of(context); + return Align( + alignment: Alignment.centerRight, + child: Material( + elevation: 16, + color: theme.colorScheme.surface, + borderRadius: const BorderRadius.horizontal(left: Radius.circular(16)), + clipBehavior: Clip.antiAlias, + child: SizedBox( + width: panelWidth, + height: MediaQuery.sizeOf(context).height, + child: panel, + ), + ), + ); + }, + transitionBuilder: (context, anim, _, child) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(CurvedAnimation(parent: anim, curve: Curves.easeOutCubic)), + child: child, + ); + }, + ); +} + +class SidePanelScaffold extends StatelessWidget { + const SidePanelScaffold({ + super.key, + required this.title, + required this.child, + this.footer, + this.onClose, + }); + + final String title; + final Widget child; + final Widget? footer; + final VoidCallback? onClose; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(24, 20, 12, 16), + child: Row( + children: [ + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: onClose ?? () => Navigator.of(context).pop(), + ), + ], + ), + ), + const Divider(height: 1), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: child, + ), + ), + if (footer != null) ...[ + const Divider(height: 1), + Padding(padding: const EdgeInsets.all(24), child: footer), + ], + ], + ); + } +} + +class SidePanelSection extends StatelessWidget { + const SidePanelSection({ + super.key, + required this.title, + required this.children, + }); + + final String title; + final List children; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.8, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 12), + ...children, + const SizedBox(height: 24), + ], + ); + } +} + +class ModulePermissionRow extends StatelessWidget { + const ModulePermissionRow({ + super.key, + required this.icon, + required this.color, + required this.label, + required this.value, + required this.onChanged, + }); + + final IconData icon; + final Color color; + final String label; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 6), + child: Row( + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, size: 16, color: color), + ), + const SizedBox(width: 12), + Expanded(child: Text(label)), + Text( + 'View only', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 8), + Checkbox( + value: value, + onChanged: (v) => onChanged(v ?? false), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ], + ), + ); + } +} diff --git a/lib/modules/reports/presentation/screens/reports_screen.dart b/lib/modules/reports/presentation/screens/reports_screen.dart new file mode 100644 index 0000000..176c4e0 --- /dev/null +++ b/lib/modules/reports/presentation/screens/reports_screen.dart @@ -0,0 +1,6 @@ +import '../../../../shared/widgets/placeholder_screen.dart'; + +class ReportsScreen extends PlaceholderScreen { + const ReportsScreen({super.key}) + : super(title: 'Reports', description: 'Asset and operational reports'); +} diff --git a/lib/modules/roles/data/datasources/role_remote_data_source.dart b/lib/modules/roles/data/datasources/role_remote_data_source.dart new file mode 100644 index 0000000..b99b91d --- /dev/null +++ b/lib/modules/roles/data/datasources/role_remote_data_source.dart @@ -0,0 +1,124 @@ +import 'package:dio/dio.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../../../shared/models/role_model.dart'; +import '../../../../shared/models/user_management_models.dart'; + +class RoleRemoteDataSource { + RoleRemoteDataSource({required this.dio}); + + final Dio dio; + + Future> getRoleCards({String? search}) async { + final response = await dio.get( + ApiEndpoints.rolesCards, + queryParameters: {if (search != null && search.isNotEmpty) 'search': search}, + ); + return _parseList(response.data['data'], RoleCardModel.fromJson); + } + + Future> getRoles({String? search}) async { + final response = await dio.get( + ApiEndpoints.roles, + queryParameters: { + 'page': 1, + 'limit': 100, + 'is_active': true, + if (search != null && search.isNotEmpty) 'search': search, + }, + ); + return _parseList(response.data['data'], RoleModel.fromJson); + } + + /// Active roles as dropdown options for user forms. + Future> listRoleOptions({String? search}) async { + final response = await dio.get( + ApiEndpoints.roles, + queryParameters: { + 'page': 1, + 'limit': 100, + 'is_active': true, + if (search != null && search.isNotEmpty) 'search': search, + }, + ); + return _parseList( + response.data['data'], + (json) => FilterOptionModel( + id: json['id']?.toString() ?? '', + name: json['name'] as String? ?? '', + ), + ).where((role) => role.id.isNotEmpty && role.name.isNotEmpty).toList(); + } + + Future getRoleById(String id) async { + final response = await dio.get(ApiEndpoints.roleById(id)); + return RoleModel.fromJson(response.data['data'] as Map); + } + + Future createRole(CreateRoleRequest request) async { + final response = await dio.post(ApiEndpoints.roles, data: request.toJson()); + return _parseRole(response.data); + } + + Future updateRole(String id, UpdateRoleRequest request) async { + final response = await dio.put( + ApiEndpoints.roleById(id), + data: request.toJson()..removeWhere((_, v) => v == null), + ); + return _parseRole(response.data); + } + + Future deleteRole(String id) async { + await dio.delete(ApiEndpoints.roleById(id)); + } + + Future> getPermissionCatalog() async { + final response = await dio.get(ApiEndpoints.rolesPermissions); + return _parseList(response.data['data'], PermissionCatalogModel.fromJson); + } + + Future getPermissionMatrix(String roleId) async { + final response = await dio.get(ApiEndpoints.rolePermissionMatrix(roleId)); + return PermissionMatrixModel.fromJson(response.data['data'] as Map); + } + + Future savePermissionMatrix( + String roleId, + PermissionMatrixSaveRequest request, + ) async { + await dio.put( + ApiEndpoints.rolePermissionMatrix(roleId), + data: request.toApiJson(), + ); + } + + List _parseList( + dynamic rawData, + T Function(Map) fromJson, + ) { + final list = rawData is List + ? rawData + : (rawData is Map ? rawData['items'] as List? : null) ?? + []; + return list.map((e) => fromJson(e as Map)).toList(); + } + + RoleModel _parseRole(dynamic body) { + if (body is! Map) { + throw FormatException('Unexpected role response'); + } + final data = body['data']; + final map = data is Map ? data : body; + return RoleModel( + id: map['id']?.toString() ?? '', + name: map['name'] as String? ?? '', + slug: map['slug'] as String? ?? + (map['name'] as String? ?? '').toLowerCase().replaceAll(' ', '_'), + description: map['description'] as String?, + permissions: (map['permissions'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + const [], + ); + } +} diff --git a/lib/modules/roles/data/repositories/role_repository_impl.dart b/lib/modules/roles/data/repositories/role_repository_impl.dart new file mode 100644 index 0000000..1f41b9f --- /dev/null +++ b/lib/modules/roles/data/repositories/role_repository_impl.dart @@ -0,0 +1,65 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/network/api_handler.dart'; +import '../../../../core/network/dio_client.dart'; +import '../../../../shared/models/role_model.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../domain/repositories/role_repository.dart'; +import '../datasources/role_remote_data_source.dart'; + +final roleRemoteDataSourceProvider = Provider((ref) { + return RoleRemoteDataSource(dio: ref.watch(dioProvider)); +}); + +final roleRepositoryProvider = Provider((ref) { + return RoleRepositoryImpl(remote: ref.watch(roleRemoteDataSourceProvider)); +}); + +class RoleRepositoryImpl implements RoleRepository { + RoleRepositoryImpl({required this.remote}); + + final RoleRemoteDataSource remote; + + @override + Future>> getRoleCards({String? search}) => + safeApiCall(() => remote.getRoleCards(search: search)); + + @override + Future>> getRoles({String? search}) => + safeApiCall(() => remote.getRoles(search: search)); + + @override + Future>> listRoleOptions({String? search}) => + safeApiCall(() => remote.listRoleOptions(search: search)); + + @override + Future> getRoleById(String id) => + safeApiCall(() => remote.getRoleById(id)); + + @override + Future> createRole(CreateRoleRequest request) => + safeApiCall(() => remote.createRole(request)); + + @override + Future> updateRole(String id, UpdateRoleRequest request) => + safeApiCall(() => remote.updateRole(id, request)); + + @override + Future> deleteRole(String id) => + safeApiCall(() => remote.deleteRole(id)); + + @override + Future>> getPermissionCatalog() => + safeApiCall(remote.getPermissionCatalog); + + @override + Future> getPermissionMatrix(String roleId) => + safeApiCall(() => remote.getPermissionMatrix(roleId)); + + @override + Future> savePermissionMatrix( + String roleId, + PermissionMatrixSaveRequest request, + ) => + safeApiCall(() => remote.savePermissionMatrix(roleId, request)); +} diff --git a/lib/modules/roles/domain/repositories/role_repository.dart b/lib/modules/roles/domain/repositories/role_repository.dart new file mode 100644 index 0000000..d61e185 --- /dev/null +++ b/lib/modules/roles/domain/repositories/role_repository.dart @@ -0,0 +1,19 @@ +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/role_model.dart'; +import '../../../../shared/models/user_management_models.dart'; + +abstract class RoleRepository { + Future>> getRoleCards({String? search}); + Future>> getRoles({String? search}); + Future>> listRoleOptions({String? search}); + Future> getRoleById(String id); + Future> createRole(CreateRoleRequest request); + Future> updateRole(String id, UpdateRoleRequest request); + Future> deleteRole(String id); + Future>> getPermissionCatalog(); + Future> getPermissionMatrix(String roleId); + Future> savePermissionMatrix( + String roleId, + PermissionMatrixSaveRequest request, + ); +} diff --git a/lib/modules/roles/domain/usecases/role_usecases.dart b/lib/modules/roles/domain/usecases/role_usecases.dart new file mode 100644 index 0000000..36f039f --- /dev/null +++ b/lib/modules/roles/domain/usecases/role_usecases.dart @@ -0,0 +1,74 @@ +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/role_model.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../repositories/role_repository.dart'; + +class GetRoleCardsUseCase { + GetRoleCardsUseCase(this._repository); + + final RoleRepository _repository; + + Future>> call({String? search}) => + _repository.getRoleCards(search: search); +} + +class GetRoleByIdUseCase { + GetRoleByIdUseCase(this._repository); + + final RoleRepository _repository; + + Future> call(String id) => _repository.getRoleById(id); +} + +class GetPermissionMatrixUseCase { + GetPermissionMatrixUseCase(this._repository); + + final RoleRepository _repository; + + Future> call(String roleId) => + _repository.getPermissionMatrix(roleId); +} + +class SavePermissionMatrixUseCase { + SavePermissionMatrixUseCase(this._repository); + + final RoleRepository _repository; + + Future> call(String roleId, PermissionMatrixSaveRequest request) => + _repository.savePermissionMatrix(roleId, request); +} + +class CreateRoleUseCase { + CreateRoleUseCase(this._repository); + + final RoleRepository _repository; + + Future> call(CreateRoleRequest request) => + _repository.createRole(request); +} + +class UpdateRoleUseCase { + UpdateRoleUseCase(this._repository); + + final RoleRepository _repository; + + Future> call(String id, UpdateRoleRequest request) => + _repository.updateRole(id, request); +} + +class DeleteRoleUseCase { + DeleteRoleUseCase(this._repository); + + final RoleRepository _repository; + + Future> call(String id) => _repository.deleteRole(id); +} + +class GetPermissionCatalogUseCase { + GetPermissionCatalogUseCase(this._repository); + + final RoleRepository _repository; + + Future>> call() => + _repository.getPermissionCatalog(); +} diff --git a/lib/modules/roles/presentation/providers/roles_provider.dart b/lib/modules/roles/presentation/providers/roles_provider.dart new file mode 100644 index 0000000..d75f5ec --- /dev/null +++ b/lib/modules/roles/presentation/providers/roles_provider.dart @@ -0,0 +1,149 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/models/user_management_models.dart'; +import '../../data/repositories/role_repository_impl.dart'; +import '../../domain/usecases/role_usecases.dart'; + +class RolesListState { + const RolesListState({ + this.roles = const [], + this.search = '', + }); + + final List roles; + final String search; + + List get filteredRoles { + if (search.isEmpty) return roles; + final q = search.toLowerCase(); + return roles + .where( + (role) => + role.name.toLowerCase().contains(q) || + (role.description?.toLowerCase().contains(q) ?? false), + ) + .toList(); + } + + RolesListState copyWith({ + List? roles, + String? search, + }) { + return RolesListState( + roles: roles ?? this.roles, + search: search ?? this.search, + ); + } +} + +final getRoleCardsUseCaseProvider = Provider((ref) { + return GetRoleCardsUseCase(ref.watch(roleRepositoryProvider)); +}); + +final deleteRoleUseCaseProvider = Provider((ref) { + return DeleteRoleUseCase(ref.watch(roleRepositoryProvider)); +}); + +final getPermissionMatrixUseCaseProvider = Provider((ref) { + return GetPermissionMatrixUseCase(ref.watch(roleRepositoryProvider)); +}); + +final savePermissionMatrixUseCaseProvider = Provider((ref) { + return SavePermissionMatrixUseCase(ref.watch(roleRepositoryProvider)); +}); + +final rolesListProvider = + AsyncNotifierProvider(RolesListNotifier.new); + +class RolesListNotifier extends AsyncNotifier { + @override + Future build() async { + return _load(); + } + + Future _load({String? search}) async { + final result = await ref.read(getRoleCardsUseCaseProvider)(search: search); + if (result.failure != null) throw result.failure!; + return RolesListState(roles: result.data ?? [], search: search ?? ''); + } + + Future refresh() async { + final current = state.valueOrNull; + state = const AsyncLoading(); + try { + state = AsyncData(await _load(search: current?.search)); + } catch (e, st) { + state = AsyncError(e, st); + } + } + + Future deleteRole(String id) async { + final result = await ref.read(deleteRoleUseCaseProvider)(id); + if (result.failure != null) return false; + await refresh(); + return true; + } + + void setSearch(String search) { + final current = state.valueOrNull; + if (current == null) return; + state = AsyncData(current.copyWith(search: search)); + } +} + +final permissionMatrixProvider = AsyncNotifierProvider.family< + PermissionMatrixNotifier, + PermissionMatrixModel, + String>(PermissionMatrixNotifier.new); + +class PermissionMatrixNotifier extends FamilyAsyncNotifier { + @override + Future build(String arg) async { + final useCase = ref.read(getPermissionMatrixUseCaseProvider); + final result = await useCase(arg); + if (result.failure != null) throw result.failure!; + return result.data!; + } + + void toggleAction(String moduleId, String action, bool value) { + final current = state.valueOrNull; + if (current == null) return; + + final updated = current.matrix.map((row) { + if (row.moduleId != moduleId) return row; + final actions = row.actions; + final next = switch (action) { + 'view' => actions.copyWith(view: value), + 'edit' => actions.copyWith(edit: value), + 'approve' => actions.copyWith(approve: value), + 'export' => actions.copyWith(export: value), + _ => actions, + }; + return row.copyWith(actions: next); + }).toList(); + + state = AsyncData(current.copyWith(matrix: updated)); + } + + Future save() async { + final current = state.valueOrNull; + if (current == null) return false; + + final useCase = ref.read(savePermissionMatrixUseCaseProvider); + final request = PermissionMatrixSaveRequest( + matrix: current.matrix + .map( + (row) => PermissionMatrixSaveRow( + moduleId: row.moduleId, + actions: row.actions, + ), + ) + .toList(), + ); + final result = await useCase(arg, request); + if (result.failure != null) return false; + ref.invalidateSelf(); + await future; + return true; + } +} diff --git a/lib/modules/roles/presentation/screens/permission_matrix_screen.dart b/lib/modules/roles/presentation/screens/permission_matrix_screen.dart new file mode 100644 index 0000000..e615380 --- /dev/null +++ b/lib/modules/roles/presentation/screens/permission_matrix_screen.dart @@ -0,0 +1,204 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/roles_provider.dart'; + +class PermissionMatrixScreen extends ConsumerStatefulWidget { + const PermissionMatrixScreen({super.key, required this.roleId}); + + final String roleId; + + @override + ConsumerState createState() => _PermissionMatrixScreenState(); +} + +class _PermissionMatrixScreenState extends ConsumerState { + bool _isSaving = false; + + Future _save() async { + setState(() => _isSaving = true); + final success = + await ref.read(permissionMatrixProvider(widget.roleId).notifier).save(); + if (!mounted) return; + setState(() => _isSaving = false); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success ? 'Permissions saved' : 'Failed to save permissions'), + ), + ); + } + + @override + Widget build(BuildContext context) { + final matrixAsync = ref.watch(permissionMatrixProvider(widget.roleId)); + + return Padding( + padding: const EdgeInsets.all(24), + child: matrixAsync.when( + loading: () => const AppLoadingView(message: 'Loading permission matrix...'), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(permissionMatrixProvider(widget.roleId)), + ), + data: (matrix) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Permission Matrix', + subtitle: matrix.roleName, + actions: [ + AppButton( + label: 'Save Changes', + expand: false, + isLoading: _isSaving, + onPressed: _save, + ), + ], + ), + const SizedBox(height: 16), + Expanded( + child: context.isMobile + ? _MatrixCardList(roleId: widget.roleId, matrix: matrix) + : _MatrixGrid(roleId: widget.roleId, matrix: matrix), + ), + ], + ), + ), + ); + } +} + +class _MatrixGrid extends ConsumerWidget { + const _MatrixGrid({required this.roleId, required this.matrix}); + + final String roleId; + final PermissionMatrixModel matrix; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AppCard( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + columns: const [ + DataColumn(label: Text('Module')), + DataColumn(label: Text('View')), + DataColumn(label: Text('Edit')), + DataColumn(label: Text('Approve')), + DataColumn(label: Text('Export')), + ], + rows: matrix.matrix + .map( + (row) => DataRow( + cells: [ + DataCell(Text(row.module)), + DataCell(_actionToggle(roleId, row, 'view', row.actions.view, ref)), + DataCell(_actionToggle(roleId, row, 'edit', row.actions.edit, ref)), + DataCell(_actionToggle(roleId, row, 'approve', row.actions.approve, ref)), + DataCell(_actionToggle(roleId, row, 'export', row.actions.export, ref)), + ], + ), + ) + .toList(), + ), + ), + ); + } + + Widget _actionToggle( + String roleId, + PermissionMatrixRow row, + String action, + bool value, + WidgetRef ref, + ) { + return Checkbox( + value: value, + onChanged: (checked) => ref + .read(permissionMatrixProvider(roleId).notifier) + .toggleAction(row.moduleId, action, checked ?? false), + ); + } +} + +class _MatrixCardList extends ConsumerWidget { + const _MatrixCardList({required this.roleId, required this.matrix}); + + final String roleId; + final PermissionMatrixModel matrix; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView.separated( + itemCount: matrix.matrix.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final row = matrix.matrix[index]; + return AppCard( + child: ExpansionTile( + title: Text(row.module), + children: [ + _PermissionSwitch( + label: 'View', + value: row.actions.view, + onChanged: (v) => ref + .read(permissionMatrixProvider(roleId).notifier) + .toggleAction(row.moduleId, 'view', v), + ), + _PermissionSwitch( + label: 'Edit (Add & Edit)', + value: row.actions.edit, + onChanged: (v) => ref + .read(permissionMatrixProvider(roleId).notifier) + .toggleAction(row.moduleId, 'edit', v), + ), + _PermissionSwitch( + label: 'Approve', + value: row.actions.approve, + onChanged: (v) => ref + .read(permissionMatrixProvider(roleId).notifier) + .toggleAction(row.moduleId, 'approve', v), + ), + _PermissionSwitch( + label: 'Export', + value: row.actions.export, + onChanged: (v) => ref + .read(permissionMatrixProvider(roleId).notifier) + .toggleAction(row.moduleId, 'export', v), + ), + ], + ), + ); + }, + ); + } +} + +class _PermissionSwitch extends StatelessWidget { + const _PermissionSwitch({ + required this.label, + required this.value, + required this.onChanged, + }); + + final String label; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return SwitchListTile( + title: Text(label), + value: value, + onChanged: onChanged, + ); + } +} diff --git a/lib/modules/roles/presentation/screens/role_list_screen.dart b/lib/modules/roles/presentation/screens/role_list_screen.dart new file mode 100644 index 0000000..c546951 --- /dev/null +++ b/lib/modules/roles/presentation/screens/role_list_screen.dart @@ -0,0 +1,152 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_data_table.dart'; +import '../../../../shared/widgets/app_empty_state.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_search_field.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/roles_provider.dart'; + +class RoleListScreen extends ConsumerStatefulWidget { + const RoleListScreen({super.key}); + + @override + ConsumerState createState() => _RoleListScreenState(); +} + +class _RoleListScreenState extends ConsumerState { + final _searchController = TextEditingController(); + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final rolesAsync = ref.watch(rolesListProvider); + + return Padding( + padding: const EdgeInsets.all(24), + child: rolesAsync.when( + loading: () => const AppLoadingView(message: 'Loading roles...'), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(rolesListProvider), + ), + data: (state) { + final roles = state.filteredRoles; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PageHeader( + title: 'Roles', + subtitle: 'Manage roles and permission assignments', + ), + const SizedBox(height: 16), + SizedBox( + width: context.isMobile ? double.infinity : 320, + child: AppSearchField( + controller: _searchController, + hint: 'Search roles...', + onChanged: ref.read(rolesListProvider.notifier).setSearch, + ), + ), + const SizedBox(height: 16), + Expanded( + child: RefreshIndicator( + onRefresh: () => ref.read(rolesListProvider.notifier).refresh(), + child: roles.isEmpty + ? ListView( + children: const [ + AppEmptyState( + title: 'No roles found', + description: 'Roles from the API will appear here.', + icon: Icons.security_outlined, + ), + ], + ) + : context.isMobile + ? _RoleCardList(roles: roles, onOpen: _openRole) + : _RoleDataTable(roles: roles, onOpen: _openRole), + ), + ), + ], + ); + }, + ), + ); + } + + void _openRole(RoleCardModel role) { + context.push('/roles/${role.id}/permissions'); + } +} + +class _RoleDataTable extends StatelessWidget { + const _RoleDataTable({required this.roles, required this.onOpen}); + + final List roles; + final void Function(RoleCardModel role) onOpen; + + @override + Widget build(BuildContext context) { + return AppDataTable( + columns: [ + AppDataColumn(label: 'Role Name', cellBuilder: (_, r) => Text(r.name)), + AppDataColumn( + label: 'Description', + cellBuilder: (_, r) => Text(r.description ?? '—'), + ), + AppDataColumn( + label: 'Users Count', + cellBuilder: (_, r) => Text('${r.userCount}'), + ), + AppDataColumn( + label: 'Actions', + cellBuilder: (_, r) => TextButton( + onPressed: () => onOpen(r), + child: const Text('View Matrix'), + ), + ), + ], + rows: roles, + ); + } +} + +class _RoleCardList extends StatelessWidget { + const _RoleCardList({required this.roles, required this.onOpen}); + + final List roles; + final void Function(RoleCardModel role) onOpen; + + @override + Widget build(BuildContext context) { + return ListView.separated( + itemCount: roles.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final role = roles[index]; + return AppCard( + child: ListTile( + title: Text(role.name), + subtitle: Text( + '${role.description ?? 'No description'} · ${role.userCount} users · ${role.permissionCount} permissions', + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => onOpen(role), + ), + ); + }, + ); + } +} diff --git a/lib/modules/settings/data/datasources/settings_local_data_source.dart b/lib/modules/settings/data/datasources/settings_local_data_source.dart new file mode 100644 index 0000000..d81909c --- /dev/null +++ b/lib/modules/settings/data/datasources/settings_local_data_source.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../../../core/constants/storage_keys.dart'; +import '../../domain/entities/app_settings.dart'; + +class SettingsLocalDataSource { + SettingsLocalDataSource(this._prefs); + + final SharedPreferences _prefs; + + Future read() async { + final raw = _prefs.getString(StorageKeys.appSettings); + if (raw == null || raw.isEmpty) return null; + return AppSettings.fromJson(jsonDecode(raw) as Map); + } + + Future write(AppSettings settings) async { + await _prefs.setString( + StorageKeys.appSettings, + jsonEncode(settings.toJson()), + ); + } +} diff --git a/lib/modules/settings/data/datasources/settings_remote_data_source.dart b/lib/modules/settings/data/datasources/settings_remote_data_source.dart new file mode 100644 index 0000000..12db4a7 --- /dev/null +++ b/lib/modules/settings/data/datasources/settings_remote_data_source.dart @@ -0,0 +1,26 @@ +import 'package:dio/dio.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../domain/entities/app_settings.dart'; + +class SettingsRemoteDataSource { + SettingsRemoteDataSource(this._dio); + + final Dio _dio; + + Future fetch() async { + final response = await _dio.get(ApiEndpoints.settings); + final data = response.data['data']; + if (data is! Map) return null; + return AppSettings.fromJson(data); + } + + Future save(AppSettings settings) async { + final response = await _dio.put(ApiEndpoints.settings, data: settings.toJson()); + final data = response.data['data']; + if (data is Map) { + return AppSettings.fromJson(data); + } + return settings; + } +} diff --git a/lib/modules/settings/data/repositories/settings_repository_impl.dart b/lib/modules/settings/data/repositories/settings_repository_impl.dart new file mode 100644 index 0000000..586dee2 --- /dev/null +++ b/lib/modules/settings/data/repositories/settings_repository_impl.dart @@ -0,0 +1,45 @@ +import '../../../../core/network/api_handler.dart'; +import '../../domain/entities/app_settings.dart'; +import '../../domain/repositories/settings_repository.dart'; +import '../datasources/settings_local_data_source.dart'; +import '../datasources/settings_remote_data_source.dart'; + +class SettingsRepositoryImpl implements SettingsRepository { + SettingsRepositoryImpl({ + required this.local, + required this.remote, + }); + + final SettingsLocalDataSource local; + final SettingsRemoteDataSource remote; + + @override + Future> getSettings() async { + return safeApiCall(() async { + try { + final remoteSettings = await remote.fetch(); + if (remoteSettings != null) { + await local.write(remoteSettings); + return remoteSettings; + } + } catch (_) { + // Fall back to local cache when API is unavailable. + } + return await local.read() ?? const AppSettings(); + }); + } + + @override + Future> saveSettings(AppSettings settings) async { + return safeApiCall(() async { + try { + final saved = await remote.save(settings); + await local.write(saved); + return saved; + } catch (_) { + await local.write(settings); + return settings; + } + }); + } +} diff --git a/lib/modules/settings/domain/entities/app_settings.dart b/lib/modules/settings/domain/entities/app_settings.dart new file mode 100644 index 0000000..8c6fc8b --- /dev/null +++ b/lib/modules/settings/domain/entities/app_settings.dart @@ -0,0 +1,723 @@ +import 'package:flutter/material.dart'; + +class GeneralSettings { + const GeneralSettings({ + this.defaultBranch = '', + this.branchCodeFormat = 'BR-{000}', + this.timeZone = 'Asia/Kolkata', + this.language = 'en', + this.currency = 'INR', + this.dateFormat = 'dd/MM/yyyy', + this.timeFormat = 'HH:mm', + this.numberFormat = '1,234.56', + }); + + final String defaultBranch; + final String branchCodeFormat; + final String timeZone; + final String language; + final String currency; + final String dateFormat; + final String timeFormat; + final String numberFormat; + + GeneralSettings copyWith({ + String? defaultBranch, + String? branchCodeFormat, + String? timeZone, + String? language, + String? currency, + String? dateFormat, + String? timeFormat, + String? numberFormat, + }) { + return GeneralSettings( + defaultBranch: defaultBranch ?? this.defaultBranch, + branchCodeFormat: branchCodeFormat ?? this.branchCodeFormat, + timeZone: timeZone ?? this.timeZone, + language: language ?? this.language, + currency: currency ?? this.currency, + dateFormat: dateFormat ?? this.dateFormat, + timeFormat: timeFormat ?? this.timeFormat, + numberFormat: numberFormat ?? this.numberFormat, + ); + } + + Map toJson() => { + 'defaultBranch': defaultBranch, + 'branchCodeFormat': branchCodeFormat, + 'timeZone': timeZone, + 'language': language, + 'currency': currency, + 'dateFormat': dateFormat, + 'timeFormat': timeFormat, + 'numberFormat': numberFormat, + }; + + factory GeneralSettings.fromJson(Map json) => GeneralSettings( + defaultBranch: json['defaultBranch'] as String? ?? '', + branchCodeFormat: json['branchCodeFormat'] as String? ?? 'BR-{000}', + timeZone: json['timeZone'] as String? ?? 'Asia/Kolkata', + language: json['language'] as String? ?? 'en', + currency: json['currency'] as String? ?? 'INR', + dateFormat: json['dateFormat'] as String? ?? 'dd/MM/yyyy', + timeFormat: json['timeFormat'] as String? ?? 'HH:mm', + numberFormat: json['numberFormat'] as String? ?? '1,234.56', + ); +} + +class CompanyProfileSettings { + const CompanyProfileSettings({ + this.companyName = '', + this.companyCode = '', + this.registrationNumber = '', + this.gstNumber = '', + this.address = '', + this.email = '', + this.phone = '', + this.website = '', + this.logoUrl = '', + }); + + final String companyName; + final String companyCode; + final String registrationNumber; + final String gstNumber; + final String address; + final String email; + final String phone; + final String website; + final String logoUrl; + + CompanyProfileSettings copyWith({ + String? companyName, + String? companyCode, + String? registrationNumber, + String? gstNumber, + String? address, + String? email, + String? phone, + String? website, + String? logoUrl, + }) { + return CompanyProfileSettings( + companyName: companyName ?? this.companyName, + companyCode: companyCode ?? this.companyCode, + registrationNumber: registrationNumber ?? this.registrationNumber, + gstNumber: gstNumber ?? this.gstNumber, + address: address ?? this.address, + email: email ?? this.email, + phone: phone ?? this.phone, + website: website ?? this.website, + logoUrl: logoUrl ?? this.logoUrl, + ); + } + + Map toJson() => { + 'companyName': companyName, + 'companyCode': companyCode, + 'registrationNumber': registrationNumber, + 'gstNumber': gstNumber, + 'address': address, + 'email': email, + 'phone': phone, + 'website': website, + 'logoUrl': logoUrl, + }; + + factory CompanyProfileSettings.fromJson(Map json) => + CompanyProfileSettings( + companyName: json['companyName'] as String? ?? '', + companyCode: json['companyCode'] as String? ?? '', + registrationNumber: json['registrationNumber'] as String? ?? '', + gstNumber: json['gstNumber'] as String? ?? '', + address: json['address'] as String? ?? '', + email: json['email'] as String? ?? '', + phone: json['phone'] as String? ?? '', + website: json['website'] as String? ?? '', + logoUrl: json['logoUrl'] as String? ?? '', + ); +} + +class UiPreferencesSettings { + const UiPreferencesSettings({ + this.sidebarExpanded = true, + this.navigationLayout = 'sidebar', + this.dashboardLayout = 'default', + this.tableDensity = 'comfortable', + this.paginationSize = 20, + }); + + final bool sidebarExpanded; + final String navigationLayout; + final String dashboardLayout; + final String tableDensity; + final int paginationSize; + + UiPreferencesSettings copyWith({ + bool? sidebarExpanded, + String? navigationLayout, + String? dashboardLayout, + String? tableDensity, + int? paginationSize, + }) { + return UiPreferencesSettings( + sidebarExpanded: sidebarExpanded ?? this.sidebarExpanded, + navigationLayout: navigationLayout ?? this.navigationLayout, + dashboardLayout: dashboardLayout ?? this.dashboardLayout, + tableDensity: tableDensity ?? this.tableDensity, + paginationSize: paginationSize ?? this.paginationSize, + ); + } + + Map toJson() => { + 'sidebarExpanded': sidebarExpanded, + 'navigationLayout': navigationLayout, + 'dashboardLayout': dashboardLayout, + 'tableDensity': tableDensity, + 'paginationSize': paginationSize, + }; + + factory UiPreferencesSettings.fromJson(Map json) => + UiPreferencesSettings( + sidebarExpanded: json['sidebarExpanded'] as bool? ?? true, + navigationLayout: json['navigationLayout'] as String? ?? 'sidebar', + dashboardLayout: json['dashboardLayout'] as String? ?? 'default', + tableDensity: json['tableDensity'] as String? ?? 'comfortable', + paginationSize: json['paginationSize'] as int? ?? 20, + ); +} + +class AssetSettingsConfig { + const AssetSettingsConfig({ + this.codePrefix = 'AST', + this.runningNumberLength = 6, + this.autoGenerateCode = true, + this.warrantyReminderDays = 30, + this.expiryAlertDays = 7, + this.qrEnabled = true, + this.qrFormat = 'asset_id', + this.labelSize = '50x25mm', + this.enabledStatuses = const [ + 'available', + 'allocated', + 'maintenance', + 'lost', + 'disposed', + 'retired', + ], + }); + + final String codePrefix; + final int runningNumberLength; + final bool autoGenerateCode; + final int warrantyReminderDays; + final int expiryAlertDays; + final bool qrEnabled; + final String qrFormat; + final String labelSize; + final List enabledStatuses; + + AssetSettingsConfig copyWith({ + String? codePrefix, + int? runningNumberLength, + bool? autoGenerateCode, + int? warrantyReminderDays, + int? expiryAlertDays, + bool? qrEnabled, + String? qrFormat, + String? labelSize, + List? enabledStatuses, + }) { + return AssetSettingsConfig( + codePrefix: codePrefix ?? this.codePrefix, + runningNumberLength: runningNumberLength ?? this.runningNumberLength, + autoGenerateCode: autoGenerateCode ?? this.autoGenerateCode, + warrantyReminderDays: warrantyReminderDays ?? this.warrantyReminderDays, + expiryAlertDays: expiryAlertDays ?? this.expiryAlertDays, + qrEnabled: qrEnabled ?? this.qrEnabled, + qrFormat: qrFormat ?? this.qrFormat, + labelSize: labelSize ?? this.labelSize, + enabledStatuses: enabledStatuses ?? this.enabledStatuses, + ); + } + + Map toJson() => { + 'codePrefix': codePrefix, + 'runningNumberLength': runningNumberLength, + 'autoGenerateCode': autoGenerateCode, + 'warrantyReminderDays': warrantyReminderDays, + 'expiryAlertDays': expiryAlertDays, + 'qrEnabled': qrEnabled, + 'qrFormat': qrFormat, + 'labelSize': labelSize, + 'enabledStatuses': enabledStatuses, + }; + + factory AssetSettingsConfig.fromJson(Map json) => + AssetSettingsConfig( + codePrefix: json['codePrefix'] as String? ?? 'AST', + runningNumberLength: json['runningNumberLength'] as int? ?? 6, + autoGenerateCode: json['autoGenerateCode'] as bool? ?? true, + warrantyReminderDays: json['warrantyReminderDays'] as int? ?? 30, + expiryAlertDays: json['expiryAlertDays'] as int? ?? 7, + qrEnabled: json['qrEnabled'] as bool? ?? true, + qrFormat: json['qrFormat'] as String? ?? 'asset_id', + labelSize: json['labelSize'] as String? ?? '50x25mm', + enabledStatuses: (json['enabledStatuses'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + const [ + 'available', + 'allocated', + 'maintenance', + 'lost', + 'disposed', + 'retired', + ], + ); +} + +class NotificationSettingsConfig { + const NotificationSettingsConfig({ + this.emailAssetAllocation = true, + this.emailAssetReturn = true, + this.emailMaintenanceRequest = true, + this.emailWarrantyExpiry = true, + this.smsEnabled = false, + this.pushEnabled = true, + this.inAppEnabled = true, + }); + + final bool emailAssetAllocation; + final bool emailAssetReturn; + final bool emailMaintenanceRequest; + final bool emailWarrantyExpiry; + final bool smsEnabled; + final bool pushEnabled; + final bool inAppEnabled; + + NotificationSettingsConfig copyWith({ + bool? emailAssetAllocation, + bool? emailAssetReturn, + bool? emailMaintenanceRequest, + bool? emailWarrantyExpiry, + bool? smsEnabled, + bool? pushEnabled, + bool? inAppEnabled, + }) { + return NotificationSettingsConfig( + emailAssetAllocation: emailAssetAllocation ?? this.emailAssetAllocation, + emailAssetReturn: emailAssetReturn ?? this.emailAssetReturn, + emailMaintenanceRequest: + emailMaintenanceRequest ?? this.emailMaintenanceRequest, + emailWarrantyExpiry: emailWarrantyExpiry ?? this.emailWarrantyExpiry, + smsEnabled: smsEnabled ?? this.smsEnabled, + pushEnabled: pushEnabled ?? this.pushEnabled, + inAppEnabled: inAppEnabled ?? this.inAppEnabled, + ); + } + + Map toJson() => { + 'emailAssetAllocation': emailAssetAllocation, + 'emailAssetReturn': emailAssetReturn, + 'emailMaintenanceRequest': emailMaintenanceRequest, + 'emailWarrantyExpiry': emailWarrantyExpiry, + 'smsEnabled': smsEnabled, + 'pushEnabled': pushEnabled, + 'inAppEnabled': inAppEnabled, + }; + + factory NotificationSettingsConfig.fromJson(Map json) => + NotificationSettingsConfig( + emailAssetAllocation: json['emailAssetAllocation'] as bool? ?? true, + emailAssetReturn: json['emailAssetReturn'] as bool? ?? true, + emailMaintenanceRequest: + json['emailMaintenanceRequest'] as bool? ?? true, + emailWarrantyExpiry: json['emailWarrantyExpiry'] as bool? ?? true, + smsEnabled: json['smsEnabled'] as bool? ?? false, + pushEnabled: json['pushEnabled'] as bool? ?? true, + inAppEnabled: json['inAppEnabled'] as bool? ?? true, + ); +} + +class EmailConfigurationSettings { + const EmailConfigurationSettings({ + this.smtpHost = '', + this.smtpPort = 587, + this.smtpUsername = '', + this.smtpPassword = '', + this.senderEmail = '', + this.senderName = '', + this.allocationTemplate = 'Your asset {{asset_name}} has been allocated.', + this.returnTemplate = 'Your asset {{asset_name}} has been returned.', + this.maintenanceTemplate = 'Maintenance request created for {{asset_name}}.', + this.warrantyTemplate = 'Warranty for {{asset_name}} expires on {{date}}.', + }); + + final String smtpHost; + final int smtpPort; + final String smtpUsername; + final String smtpPassword; + final String senderEmail; + final String senderName; + final String allocationTemplate; + final String returnTemplate; + final String maintenanceTemplate; + final String warrantyTemplate; + + EmailConfigurationSettings copyWith({ + String? smtpHost, + int? smtpPort, + String? smtpUsername, + String? smtpPassword, + String? senderEmail, + String? senderName, + String? allocationTemplate, + String? returnTemplate, + String? maintenanceTemplate, + String? warrantyTemplate, + }) { + return EmailConfigurationSettings( + smtpHost: smtpHost ?? this.smtpHost, + smtpPort: smtpPort ?? this.smtpPort, + smtpUsername: smtpUsername ?? this.smtpUsername, + smtpPassword: smtpPassword ?? this.smtpPassword, + senderEmail: senderEmail ?? this.senderEmail, + senderName: senderName ?? this.senderName, + allocationTemplate: allocationTemplate ?? this.allocationTemplate, + returnTemplate: returnTemplate ?? this.returnTemplate, + maintenanceTemplate: maintenanceTemplate ?? this.maintenanceTemplate, + warrantyTemplate: warrantyTemplate ?? this.warrantyTemplate, + ); + } + + Map toJson() => { + 'smtpHost': smtpHost, + 'smtpPort': smtpPort, + 'smtpUsername': smtpUsername, + 'smtpPassword': smtpPassword, + 'senderEmail': senderEmail, + 'senderName': senderName, + 'allocationTemplate': allocationTemplate, + 'returnTemplate': returnTemplate, + 'maintenanceTemplate': maintenanceTemplate, + 'warrantyTemplate': warrantyTemplate, + }; + + factory EmailConfigurationSettings.fromJson(Map json) => + EmailConfigurationSettings( + smtpHost: json['smtpHost'] as String? ?? '', + smtpPort: json['smtpPort'] as int? ?? 587, + smtpUsername: json['smtpUsername'] as String? ?? '', + smtpPassword: json['smtpPassword'] as String? ?? '', + senderEmail: json['senderEmail'] as String? ?? '', + senderName: json['senderName'] as String? ?? '', + allocationTemplate: json['allocationTemplate'] as String? ?? + 'Your asset {{asset_name}} has been allocated.', + returnTemplate: json['returnTemplate'] as String? ?? + 'Your asset {{asset_name}} has been returned.', + maintenanceTemplate: json['maintenanceTemplate'] as String? ?? + 'Maintenance request created for {{asset_name}}.', + warrantyTemplate: json['warrantyTemplate'] as String? ?? + 'Warranty for {{asset_name}} expires on {{date}}.', + ); +} + +class SecuritySettingsConfig { + const SecuritySettingsConfig({ + this.minPasswordLength = 8, + this.passwordExpiryDays = 90, + this.otpEnabled = true, + this.mfaEnabled = false, + this.sessionTimeoutMinutes = 30, + this.autoLogout = true, + this.concurrentLoginControl = false, + this.loginAttemptLimit = 5, + this.accountLockDurationMinutes = 15, + this.auditLoggingEnabled = true, + this.ipWhitelist = '', + }); + + final int minPasswordLength; + final int passwordExpiryDays; + final bool otpEnabled; + final bool mfaEnabled; + final int sessionTimeoutMinutes; + final bool autoLogout; + final bool concurrentLoginControl; + final int loginAttemptLimit; + final int accountLockDurationMinutes; + final bool auditLoggingEnabled; + final String ipWhitelist; + + SecuritySettingsConfig copyWith({ + int? minPasswordLength, + int? passwordExpiryDays, + bool? otpEnabled, + bool? mfaEnabled, + int? sessionTimeoutMinutes, + bool? autoLogout, + bool? concurrentLoginControl, + int? loginAttemptLimit, + int? accountLockDurationMinutes, + bool? auditLoggingEnabled, + String? ipWhitelist, + }) { + return SecuritySettingsConfig( + minPasswordLength: minPasswordLength ?? this.minPasswordLength, + passwordExpiryDays: passwordExpiryDays ?? this.passwordExpiryDays, + otpEnabled: otpEnabled ?? this.otpEnabled, + mfaEnabled: mfaEnabled ?? this.mfaEnabled, + sessionTimeoutMinutes: sessionTimeoutMinutes ?? this.sessionTimeoutMinutes, + autoLogout: autoLogout ?? this.autoLogout, + concurrentLoginControl: + concurrentLoginControl ?? this.concurrentLoginControl, + loginAttemptLimit: loginAttemptLimit ?? this.loginAttemptLimit, + accountLockDurationMinutes: + accountLockDurationMinutes ?? this.accountLockDurationMinutes, + auditLoggingEnabled: auditLoggingEnabled ?? this.auditLoggingEnabled, + ipWhitelist: ipWhitelist ?? this.ipWhitelist, + ); + } + + Map toJson() => { + 'minPasswordLength': minPasswordLength, + 'passwordExpiryDays': passwordExpiryDays, + 'otpEnabled': otpEnabled, + 'mfaEnabled': mfaEnabled, + 'sessionTimeoutMinutes': sessionTimeoutMinutes, + 'autoLogout': autoLogout, + 'concurrentLoginControl': concurrentLoginControl, + 'loginAttemptLimit': loginAttemptLimit, + 'accountLockDurationMinutes': accountLockDurationMinutes, + 'auditLoggingEnabled': auditLoggingEnabled, + 'ipWhitelist': ipWhitelist, + }; + + factory SecuritySettingsConfig.fromJson(Map json) => + SecuritySettingsConfig( + minPasswordLength: json['minPasswordLength'] as int? ?? 8, + passwordExpiryDays: json['passwordExpiryDays'] as int? ?? 90, + otpEnabled: json['otpEnabled'] as bool? ?? true, + mfaEnabled: json['mfaEnabled'] as bool? ?? false, + sessionTimeoutMinutes: json['sessionTimeoutMinutes'] as int? ?? 30, + autoLogout: json['autoLogout'] as bool? ?? true, + concurrentLoginControl: json['concurrentLoginControl'] as bool? ?? false, + loginAttemptLimit: json['loginAttemptLimit'] as int? ?? 5, + accountLockDurationMinutes: + json['accountLockDurationMinutes'] as int? ?? 15, + auditLoggingEnabled: json['auditLoggingEnabled'] as bool? ?? true, + ipWhitelist: json['ipWhitelist'] as String? ?? '', + ); +} + +class AppSettings { + const AppSettings({ + this.general = const GeneralSettings(), + this.companyProfile = const CompanyProfileSettings(), + this.uiPreferences = const UiPreferencesSettings(), + this.asset = const AssetSettingsConfig(), + this.notifications = const NotificationSettingsConfig(), + this.email = const EmailConfigurationSettings(), + this.security = const SecuritySettingsConfig(), + }); + + final GeneralSettings general; + final CompanyProfileSettings companyProfile; + final UiPreferencesSettings uiPreferences; + final AssetSettingsConfig asset; + final NotificationSettingsConfig notifications; + final EmailConfigurationSettings email; + final SecuritySettingsConfig security; + + AppSettings copyWith({ + GeneralSettings? general, + CompanyProfileSettings? companyProfile, + UiPreferencesSettings? uiPreferences, + AssetSettingsConfig? asset, + NotificationSettingsConfig? notifications, + EmailConfigurationSettings? email, + SecuritySettingsConfig? security, + }) { + return AppSettings( + general: general ?? this.general, + companyProfile: companyProfile ?? this.companyProfile, + uiPreferences: uiPreferences ?? this.uiPreferences, + asset: asset ?? this.asset, + notifications: notifications ?? this.notifications, + email: email ?? this.email, + security: security ?? this.security, + ); + } + + Map toJson() => { + 'general': general.toJson(), + 'companyProfile': companyProfile.toJson(), + 'uiPreferences': uiPreferences.toJson(), + 'asset': asset.toJson(), + 'notifications': notifications.toJson(), + 'email': email.toJson(), + 'security': security.toJson(), + }; + + factory AppSettings.fromJson(Map json) => AppSettings( + general: GeneralSettings.fromJson( + json['general'] as Map? ?? {}, + ), + companyProfile: CompanyProfileSettings.fromJson( + json['companyProfile'] as Map? ?? {}, + ), + uiPreferences: UiPreferencesSettings.fromJson( + json['uiPreferences'] as Map? ?? {}, + ), + asset: AssetSettingsConfig.fromJson( + json['asset'] as Map? ?? {}, + ), + notifications: NotificationSettingsConfig.fromJson( + json['notifications'] as Map? ?? {}, + ), + email: EmailConfigurationSettings.fromJson( + json['email'] as Map? ?? {}, + ), + security: SecuritySettingsConfig.fromJson( + json['security'] as Map? ?? {}, + ), + ); +} + +class SettingsSection { + const SettingsSection({ + required this.id, + required this.title, + required this.subtitle, + required this.icon, + required this.route, + this.phase = 1, + }); + + final String id; + final String title; + final String subtitle; + final IconData icon; + final String route; + final int phase; +} + +const phase1SettingsSections = [ + SettingsSection( + id: 'general', + title: 'General', + subtitle: 'Regional, branch, and default preferences', + icon: Icons.tune_outlined, + route: '/settings/general', + ), + SettingsSection( + id: 'company-profile', + title: 'Company Profile', + subtitle: 'Company information and logo', + icon: Icons.business_outlined, + route: '/settings/company-profile', + ), + SettingsSection( + id: 'appearance', + title: 'Appearance', + subtitle: 'Theme, branding, and UI preferences', + icon: Icons.palette_outlined, + route: '/settings/appearance', + ), + SettingsSection( + id: 'roles', + title: 'Roles & Permissions', + subtitle: 'Manage roles, permissions, and menu access', + icon: Icons.security_outlined, + route: '/settings/roles', + ), + SettingsSection( + id: 'asset', + title: 'Asset Settings', + subtitle: 'Asset codes, statuses, warranty, and QR', + icon: Icons.inventory_2_outlined, + route: '/settings/asset', + ), + SettingsSection( + id: 'notifications', + title: 'Notifications', + subtitle: 'Email, SMS, push, and in-app alerts', + icon: Icons.notifications_outlined, + route: '/settings/notifications', + ), + SettingsSection( + id: 'email', + title: 'Email Configuration', + subtitle: 'SMTP server and email templates', + icon: Icons.email_outlined, + route: '/settings/email', + ), + SettingsSection( + id: 'security', + title: 'Security', + subtitle: 'Authentication, session, and audit policies', + icon: Icons.lock_outline, + route: '/settings/security', + ), +]; + +const phase2SettingsSections = [ + SettingsSection( + id: 'workflow', + title: 'Workflow Settings', + subtitle: 'Approval workflows and approvers', + icon: Icons.account_tree_outlined, + route: '/settings/workflow', + phase: 2, + ), + SettingsSection( + id: 'dashboard', + title: 'Dashboard', + subtitle: 'KPI visibility and widget layout', + icon: Icons.dashboard_outlined, + route: '/settings/dashboard', + phase: 2, + ), + SettingsSection( + id: 'reports', + title: 'Reports', + subtitle: 'Export options and scheduled reports', + icon: Icons.assessment_outlined, + route: '/settings/reports', + phase: 2, + ), + SettingsSection( + id: 'storage', + title: 'Storage', + subtitle: 'File upload limits and storage provider', + icon: Icons.cloud_upload_outlined, + route: '/settings/storage', + phase: 2, + ), + SettingsSection( + id: 'audit', + title: 'Audit Logs', + subtitle: 'Log retention and tracked events', + icon: Icons.history_outlined, + route: '/settings/audit', + phase: 2, + ), + SettingsSection( + id: 'mobile', + title: 'Mobile Settings', + subtitle: 'Mobile login and sync preferences', + icon: Icons.phone_android_outlined, + route: '/settings/mobile', + phase: 2, + ), + SettingsSection( + id: 'integrations', + title: 'Integrations', + subtitle: 'SSO, SMS gateway, and cloud storage', + icon: Icons.extension_outlined, + route: '/settings/integrations', + phase: 2, + ), +]; diff --git a/lib/modules/settings/domain/repositories/settings_repository.dart b/lib/modules/settings/domain/repositories/settings_repository.dart new file mode 100644 index 0000000..3980145 --- /dev/null +++ b/lib/modules/settings/domain/repositories/settings_repository.dart @@ -0,0 +1,7 @@ +import '../../../../core/network/api_handler.dart'; +import '../entities/app_settings.dart'; + +abstract class SettingsRepository { + Future> getSettings(); + Future> saveSettings(AppSettings settings); +} diff --git a/lib/modules/settings/domain/usecases/get_settings_use_case.dart b/lib/modules/settings/domain/usecases/get_settings_use_case.dart new file mode 100644 index 0000000..c274b02 --- /dev/null +++ b/lib/modules/settings/domain/usecases/get_settings_use_case.dart @@ -0,0 +1,11 @@ +import '../../../../core/network/api_handler.dart'; +import '../entities/app_settings.dart'; +import '../repositories/settings_repository.dart'; + +class GetSettingsUseCase { + GetSettingsUseCase(this._repository); + + final SettingsRepository _repository; + + Future> call() => _repository.getSettings(); +} diff --git a/lib/modules/settings/domain/usecases/save_settings_use_case.dart b/lib/modules/settings/domain/usecases/save_settings_use_case.dart new file mode 100644 index 0000000..98b42ce --- /dev/null +++ b/lib/modules/settings/domain/usecases/save_settings_use_case.dart @@ -0,0 +1,12 @@ +import '../../../../core/network/api_handler.dart'; +import '../entities/app_settings.dart'; +import '../repositories/settings_repository.dart'; + +class SaveSettingsUseCase { + SaveSettingsUseCase(this._repository); + + final SettingsRepository _repository; + + Future> call(AppSettings settings) => + _repository.saveSettings(settings); +} diff --git a/lib/modules/settings/presentation/providers/settings_provider.dart b/lib/modules/settings/presentation/providers/settings_provider.dart new file mode 100644 index 0000000..88e3f35 --- /dev/null +++ b/lib/modules/settings/presentation/providers/settings_provider.dart @@ -0,0 +1,100 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/network/dio_client.dart'; +import '../../../../core/theme/theme_provider.dart'; +import '../../data/datasources/settings_local_data_source.dart'; +import '../../data/datasources/settings_remote_data_source.dart'; +import '../../data/repositories/settings_repository_impl.dart'; +import '../../domain/entities/app_settings.dart'; +import '../../domain/repositories/settings_repository.dart'; +import '../../domain/usecases/get_settings_use_case.dart'; +import '../../domain/usecases/save_settings_use_case.dart'; + +final settingsLocalDataSourceProvider = Provider((ref) { + return SettingsLocalDataSource(ref.watch(sharedPreferencesProvider)); +}); + +final settingsRemoteDataSourceProvider = + Provider((ref) { + return SettingsRemoteDataSource(ref.watch(dioProvider)); +}); + +final settingsRepositoryProvider = Provider((ref) { + return SettingsRepositoryImpl( + local: ref.watch(settingsLocalDataSourceProvider), + remote: ref.watch(settingsRemoteDataSourceProvider), + ); +}); + +final getSettingsUseCaseProvider = Provider((ref) { + return GetSettingsUseCase(ref.watch(settingsRepositoryProvider)); +}); + +final saveSettingsUseCaseProvider = Provider((ref) { + return SaveSettingsUseCase(ref.watch(settingsRepositoryProvider)); +}); + +final appSettingsProvider = + StateNotifierProvider((ref) { + return AppSettingsNotifier( + getSettings: ref.watch(getSettingsUseCaseProvider), + saveSettings: ref.watch(saveSettingsUseCaseProvider), + ); +}); + +class AppSettingsNotifier extends StateNotifier { + AppSettingsNotifier({ + required GetSettingsUseCase getSettings, + required SaveSettingsUseCase saveSettings, + }) : _getSettings = getSettings, + _saveSettings = saveSettings, + super(const AppSettings()) { + _load(); + } + + final GetSettingsUseCase _getSettings; + final SaveSettingsUseCase _saveSettings; + + Future _load() async { + final result = await _getSettings(); + state = result.data ?? const AppSettings(); + } + + Future _persist(AppSettings settings) async { + state = settings; + final result = await _saveSettings(settings); + state = result.data ?? settings; + } + + Future updateGeneral(GeneralSettings general) async { + await _persist(state.copyWith(general: general)); + } + + Future updateCompanyProfile(CompanyProfileSettings profile) async { + await _persist(state.copyWith(companyProfile: profile)); + } + + Future updateUiPreferences(UiPreferencesSettings prefs) async { + await _persist(state.copyWith(uiPreferences: prefs)); + } + + Future updateAsset(AssetSettingsConfig asset) async { + await _persist(state.copyWith(asset: asset)); + } + + Future updateNotifications(NotificationSettingsConfig notifications) async { + await _persist(state.copyWith(notifications: notifications)); + } + + Future updateEmail(EmailConfigurationSettings email) async { + await _persist(state.copyWith(email: email)); + } + + Future updateSecurity(SecuritySettingsConfig security) async { + await _persist(state.copyWith(security: security)); + } + + Future resetToDefaults() async { + await _persist(const AppSettings()); + } +} diff --git a/lib/modules/settings/presentation/screens/appearance_settings_screen.dart b/lib/modules/settings/presentation/screens/appearance_settings_screen.dart new file mode 100644 index 0000000..04fb70f --- /dev/null +++ b/lib/modules/settings/presentation/screens/appearance_settings_screen.dart @@ -0,0 +1,240 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../core/theme/branding_config.dart'; +import '../../../../core/theme/theme_provider.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../providers/settings_provider.dart'; +import '../widgets/settings_widgets.dart'; + +class AppearanceSettingsScreen extends ConsumerWidget { + const AppearanceSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeMode = ref.watch(themeModeProvider); + final branding = ref.watch(brandingProvider); + final uiPrefs = ref.watch(appSettingsProvider).uiPreferences; + + return SettingsPageLayout( + title: 'Appearance', + subtitle: 'Theme, branding, and UI preferences', + child: Column( + children: [ + SettingsFormCard( + title: 'Theme', + children: [ + ...ThemeModeOption.values.map( + (mode) => RadioListTile( + title: Text(_themeLabel(mode)), + value: mode, + groupValue: themeMode, + onChanged: (v) { + if (v != null) { + ref.read(themeModeProvider.notifier).setThemeMode(v); + } + }, + ), + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Branding', + subtitle: 'Primary and secondary colors for the application', + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar(backgroundColor: branding.primaryColor), + title: const Text('Primary Color'), + subtitle: Text( + '#${branding.primaryColorValue.toRadixString(16).padLeft(8, '0').substring(2)}', + ), + ), + ListTile( + contentPadding: EdgeInsets.zero, + leading: CircleAvatar(backgroundColor: branding.secondaryColor), + title: const Text('Secondary Color'), + subtitle: Text( + '#${branding.secondaryColorValue.toRadixString(16).padLeft(8, '0').substring(2)}', + ), + ), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _ColorPreset( + label: 'Blue', + primary: 0xFF1565C0, + secondary: 0xFF00897B, + branding: branding, + ref: ref, + ), + _ColorPreset( + label: 'Purple', + primary: 0xFF6A1B9A, + secondary: 0xFF00838F, + branding: branding, + ref: ref, + ), + _ColorPreset( + label: 'Green', + primary: 0xFF2E7D32, + secondary: 0xFF558B2F, + branding: branding, + ref: ref, + ), + _ColorPreset( + label: 'Orange', + primary: 0xFFE65100, + secondary: 0xFFF57C00, + branding: branding, + ref: ref, + ), + ], + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Navigation Layout', + subtitle: 'Choose how the main menu is displayed', + children: [ + ...NavigationLayout.values.map( + (layout) => RadioListTile( + title: Text(layout.label), + subtitle: Text( + layout == NavigationLayout.sidebar + ? 'Vertical sidebar on the left (default)' + : 'Horizontal menu bar at the top', + ), + value: layout, + groupValue: NavigationLayout.fromValue(uiPrefs.navigationLayout), + onChanged: (v) { + if (v != null) { + ref.read(appSettingsProvider.notifier).updateUiPreferences( + uiPrefs.copyWith(navigationLayout: v.value), + ); + } + }, + ), + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'UI Preferences', + children: [ + SettingsSwitchTile( + title: 'Sidebar Expanded by Default', + subtitle: 'Show full sidebar labels on login', + value: uiPrefs.sidebarExpanded, + onChanged: (v) => ref + .read(appSettingsProvider.notifier) + .updateUiPreferences(uiPrefs.copyWith(sidebarExpanded: v)), + ), + SettingsDropdownField( + label: 'Dashboard Layout', + value: uiPrefs.dashboardLayout, + items: const ['default', 'compact', 'analytics'], + itemLabel: (v) => v[0].toUpperCase() + v.substring(1), + onChanged: (v) { + if (v != null) { + ref.read(appSettingsProvider.notifier).updateUiPreferences( + uiPrefs.copyWith(dashboardLayout: v), + ); + } + }, + ), + const SizedBox(height: 16), + SettingsDropdownField( + label: 'Table Density', + value: uiPrefs.tableDensity, + items: const ['compact', 'comfortable', 'spacious'], + itemLabel: (v) => v[0].toUpperCase() + v.substring(1), + onChanged: (v) { + if (v != null) { + ref.read(appSettingsProvider.notifier).updateUiPreferences( + uiPrefs.copyWith(tableDensity: v), + ); + } + }, + ), + const SizedBox(height: 16), + SettingsDropdownField( + label: 'Pagination Size', + value: uiPrefs.paginationSize, + items: const [10, 20, 50, 100], + itemLabel: (v) => '$v rows', + onChanged: (v) { + if (v != null) { + ref.read(appSettingsProvider.notifier).updateUiPreferences( + uiPrefs.copyWith(paginationSize: v), + ); + } + }, + ), + ], + ), + const SizedBox(height: 24), + AppButton( + label: 'Settings Auto-Saved', + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Appearance settings are saved automatically')), + ); + }, + ), + ], + ), + ); + } + + String _themeLabel(ThemeModeOption mode) => switch (mode) { + ThemeModeOption.light => 'Light Theme', + ThemeModeOption.dark => 'Dark Theme', + ThemeModeOption.system => 'System Theme', + }; +} + +class _ColorPreset extends StatelessWidget { + const _ColorPreset({ + required this.label, + required this.primary, + required this.secondary, + required this.branding, + required this.ref, + }); + + final String label; + final int primary; + final int secondary; + final BrandingConfig branding; + final WidgetRef ref; + + @override + Widget build(BuildContext context) { + return OutlinedButton( + onPressed: () { + ref.read(brandingProvider.notifier).updateBranding( + branding.copyWith( + primaryColorValue: primary, + secondaryColorValue: secondary, + ), + ); + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar(radius: 8, backgroundColor: Color(primary)), + const SizedBox(width: 6), + CircleAvatar(radius: 8, backgroundColor: Color(secondary)), + const SizedBox(width: 8), + Text(label), + ], + ), + ); + } +} diff --git a/lib/modules/settings/presentation/screens/asset_settings_screen.dart b/lib/modules/settings/presentation/screens/asset_settings_screen.dart new file mode 100644 index 0000000..d0f7eaa --- /dev/null +++ b/lib/modules/settings/presentation/screens/asset_settings_screen.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../domain/entities/app_settings.dart'; +import '../providers/settings_provider.dart'; +import '../widgets/settings_widgets.dart'; + +class AssetSettingsScreen extends ConsumerStatefulWidget { + const AssetSettingsScreen({super.key}); + + @override + ConsumerState createState() => _AssetSettingsScreenState(); +} + +class _AssetSettingsScreenState extends ConsumerState { + final _formKey = GlobalKey(); + late final TextEditingController _prefixController; + late final TextEditingController _runningLengthController; + late final TextEditingController _warrantyReminderController; + late final TextEditingController _expiryAlertController; + late bool _autoGenerate; + late bool _qrEnabled; + late String _qrFormat; + late String _labelSize; + late Set _enabledStatuses; + + @override + void initState() { + super.initState(); + final asset = ref.read(appSettingsProvider).asset; + _prefixController = TextEditingController(text: asset.codePrefix); + _runningLengthController = + TextEditingController(text: '${asset.runningNumberLength}'); + _warrantyReminderController = + TextEditingController(text: '${asset.warrantyReminderDays}'); + _expiryAlertController = + TextEditingController(text: '${asset.expiryAlertDays}'); + _autoGenerate = asset.autoGenerateCode; + _qrEnabled = asset.qrEnabled; + _qrFormat = asset.qrFormat; + _labelSize = asset.labelSize; + _enabledStatuses = asset.enabledStatuses.toSet(); + } + + @override + void dispose() { + _prefixController.dispose(); + _runningLengthController.dispose(); + _warrantyReminderController.dispose(); + _expiryAlertController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + await ref.read(appSettingsProvider.notifier).updateAsset( + AssetSettingsConfig( + codePrefix: _prefixController.text.trim(), + runningNumberLength: int.parse(_runningLengthController.text.trim()), + autoGenerateCode: _autoGenerate, + warrantyReminderDays: + int.parse(_warrantyReminderController.text.trim()), + expiryAlertDays: int.parse(_expiryAlertController.text.trim()), + qrEnabled: _qrEnabled, + qrFormat: _qrFormat, + labelSize: _labelSize, + enabledStatuses: _enabledStatuses.toList(), + ), + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Asset settings saved')), + ); + } + } + + @override + Widget build(BuildContext context) { + final prefix = _prefixController.text.trim().isEmpty + ? 'AST' + : _prefixController.text.trim(); + final length = int.tryParse(_runningLengthController.text.trim()) ?? 6; + final preview = '$prefix-${'0' * (length - 1)}1'; + + return SettingsPageLayout( + title: 'Asset Settings', + subtitle: 'Asset codes, statuses, warranty, and QR configuration', + child: Form( + key: _formKey, + child: Column( + children: [ + SettingsFormCard( + title: 'Asset Code Generation', + subtitle: 'Preview: $preview', + children: [ + AppTextField( + controller: _prefixController, + label: 'Prefix', + hint: 'AST, LAP, MOB', + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 16), + AppTextField( + controller: _runningLengthController, + label: 'Running Number Length', + keyboardType: TextInputType.number, + onChanged: (_) => setState(() {}), + ), + SettingsSwitchTile( + title: 'Auto Generate Asset Code', + value: _autoGenerate, + onChanged: (v) => setState(() => _autoGenerate = v), + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Asset Statuses', + subtitle: 'Enable or disable statuses used in the system', + children: AssetStatus.values.map((status) { + return CheckboxListTile( + contentPadding: EdgeInsets.zero, + title: Text(status.label), + value: _enabledStatuses.contains(status.value), + onChanged: (checked) { + setState(() { + if (checked == true) { + _enabledStatuses.add(status.value); + } else { + _enabledStatuses.remove(status.value); + } + }); + }, + ); + }).toList(), + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Warranty Settings', + children: [ + AppTextField( + controller: _warrantyReminderController, + label: 'Warranty Reminder Days', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + AppTextField( + controller: _expiryAlertController, + label: 'Expiry Alert Days', + keyboardType: TextInputType.number, + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'QR Settings', + children: [ + SettingsSwitchTile( + title: 'Enable QR Codes', + value: _qrEnabled, + onChanged: (v) => setState(() => _qrEnabled = v), + ), + SettingsDropdownField( + label: 'QR Format', + value: _qrFormat, + items: const ['asset_id', 'asset_code', 'url'], + itemLabel: (v) => switch (v) { + 'asset_id' => 'Asset ID', + 'asset_code' => 'Asset Code', + 'url' => 'URL Link', + _ => v, + }, + onChanged: (v) => setState(() => _qrFormat = v!), + ), + const SizedBox(height: 16), + SettingsDropdownField( + label: 'Print Label Size', + value: _labelSize, + items: const ['50x25mm', '40x20mm', '70x35mm'], + itemLabel: (v) => v, + onChanged: (v) => setState(() => _labelSize = v!), + ), + ], + ), + const SizedBox(height: 24), + AppButton(label: 'Save Changes', onPressed: _save), + ], + ), + ), + ); + } +} diff --git a/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart b/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart new file mode 100644 index 0000000..ee091d8 --- /dev/null +++ b/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart @@ -0,0 +1,218 @@ +import 'dart:convert'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/theme/theme_provider.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/sidebar_logo.dart'; +import '../../domain/entities/app_settings.dart'; +import '../providers/settings_provider.dart'; +import '../widgets/settings_widgets.dart'; + +class CompanyProfileSettingsScreen extends ConsumerStatefulWidget { + const CompanyProfileSettingsScreen({super.key}); + + @override + ConsumerState createState() => + _CompanyProfileSettingsScreenState(); +} + +class _CompanyProfileSettingsScreenState + extends ConsumerState { + final _formKey = GlobalKey(); + late final TextEditingController _nameController; + late final TextEditingController _codeController; + late final TextEditingController _registrationController; + late final TextEditingController _gstController; + late final TextEditingController _addressController; + late final TextEditingController _emailController; + late final TextEditingController _phoneController; + late final TextEditingController _websiteController; + late final TextEditingController _logoUrlController; + + @override + void initState() { + super.initState(); + final profile = ref.read(appSettingsProvider).companyProfile; + _nameController = TextEditingController(text: profile.companyName); + _codeController = TextEditingController(text: profile.companyCode); + _registrationController = + TextEditingController(text: profile.registrationNumber); + _gstController = TextEditingController(text: profile.gstNumber); + _addressController = TextEditingController(text: profile.address); + _emailController = TextEditingController(text: profile.email); + _phoneController = TextEditingController(text: profile.phone); + _websiteController = TextEditingController(text: profile.website); + _logoUrlController = TextEditingController(text: profile.logoUrl); + } + + @override + void dispose() { + _nameController.dispose(); + _codeController.dispose(); + _registrationController.dispose(); + _gstController.dispose(); + _addressController.dispose(); + _emailController.dispose(); + _phoneController.dispose(); + _websiteController.dispose(); + _logoUrlController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + final logoUrl = _logoUrlController.text.trim(); + final companyName = _nameController.text.trim(); + + await ref.read(appSettingsProvider.notifier).updateCompanyProfile( + CompanyProfileSettings( + companyName: companyName, + companyCode: _codeController.text.trim(), + registrationNumber: _registrationController.text.trim(), + gstNumber: _gstController.text.trim(), + address: _addressController.text.trim(), + email: _emailController.text.trim(), + phone: _phoneController.text.trim(), + website: _websiteController.text.trim(), + logoUrl: logoUrl, + ), + ); + + await ref.read(brandingProvider.notifier).updateBranding( + ref.read(brandingProvider).copyWith( + logoUrl: logoUrl.isEmpty ? null : logoUrl, + companyName: companyName.isEmpty ? null : companyName, + ), + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Company profile saved')), + ); + } + } + + Future _pickLogo() async { + final result = await FilePicker.pickFiles( + type: FileType.image, + withData: true, + ); + + if (result == null || result.files.isEmpty) return; + + final file = result.files.single; + final bytes = file.bytes; + if (bytes == null) return; + + final ext = (file.extension ?? 'png').toLowerCase(); + final mime = ext == 'jpg' ? 'jpeg' : ext; + final dataUri = 'data:image/$mime;base64,${base64Encode(bytes)}'; + + setState(() { + _logoUrlController.text = dataUri; + }); + } + + @override + Widget build(BuildContext context) { + return SettingsPageLayout( + title: 'Company Profile', + subtitle: 'Company information and branding assets', + child: Form( + key: _formKey, + child: Column( + children: [ + SettingsFormCard( + title: 'Company Information', + children: [ + AppTextField( + controller: _nameController, + label: 'Company Name', + validator: (v) => Validators.required(v, fieldName: 'Company name'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _codeController, + label: 'Company Code', + validator: (v) => Validators.required(v, fieldName: 'Company code'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _registrationController, + label: 'Registration Number', + ), + const SizedBox(height: 16), + AppTextField( + controller: _gstController, + label: 'GST/VAT Number', + validator: Validators.gstNumber, + ), + const SizedBox(height: 16), + AppTextField( + controller: _addressController, + label: 'Address', + maxLines: 3, + ), + const SizedBox(height: 16), + AppTextField( + controller: _emailController, + label: 'Email', + keyboardType: TextInputType.emailAddress, + validator: Validators.email, + ), + const SizedBox(height: 16), + AppTextField( + controller: _phoneController, + label: 'Phone', + keyboardType: TextInputType.phone, + ), + const SizedBox(height: 16), + AppTextField( + controller: _websiteController, + label: 'Website', + keyboardType: TextInputType.url, + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Logo Upload', + subtitle: 'Upload an image or provide a logo URL', + children: [ + Center( + child: SidebarLogo( + logoUrl: _logoUrlController.text.trim().isEmpty + ? null + : _logoUrlController.text.trim(), + size: 72, + ), + ), + const SizedBox(height: 16), + AppTextField( + controller: _logoUrlController, + label: 'Logo URL', + hint: 'https://example.com/logo.png', + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 12), + OutlinedButton.icon( + onPressed: _pickLogo, + icon: const Icon(Icons.upload_file), + label: const Text('Upload Logo'), + ), + ], + ), + const SizedBox(height: 24), + AppButton(label: 'Save Changes', onPressed: _save), + ], + ), + ), + ); + } +} diff --git a/lib/modules/settings/presentation/screens/email_configuration_screen.dart b/lib/modules/settings/presentation/screens/email_configuration_screen.dart new file mode 100644 index 0000000..7af9cd7 --- /dev/null +++ b/lib/modules/settings/presentation/screens/email_configuration_screen.dart @@ -0,0 +1,177 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/utils/validators.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../domain/entities/app_settings.dart'; +import '../providers/settings_provider.dart'; +import '../widgets/settings_widgets.dart'; + +class EmailConfigurationScreen extends ConsumerStatefulWidget { + const EmailConfigurationScreen({super.key}); + + @override + ConsumerState createState() => + _EmailConfigurationScreenState(); +} + +class _EmailConfigurationScreenState + extends ConsumerState { + final _formKey = GlobalKey(); + late final TextEditingController _hostController; + late final TextEditingController _portController; + late final TextEditingController _usernameController; + late final TextEditingController _passwordController; + late final TextEditingController _senderEmailController; + late final TextEditingController _senderNameController; + late final TextEditingController _allocationTemplateController; + late final TextEditingController _returnTemplateController; + late final TextEditingController _maintenanceTemplateController; + late final TextEditingController _warrantyTemplateController; + + @override + void initState() { + super.initState(); + final email = ref.read(appSettingsProvider).email; + _hostController = TextEditingController(text: email.smtpHost); + _portController = TextEditingController(text: '${email.smtpPort}'); + _usernameController = TextEditingController(text: email.smtpUsername); + _passwordController = TextEditingController(text: email.smtpPassword); + _senderEmailController = TextEditingController(text: email.senderEmail); + _senderNameController = TextEditingController(text: email.senderName); + _allocationTemplateController = + TextEditingController(text: email.allocationTemplate); + _returnTemplateController = TextEditingController(text: email.returnTemplate); + _maintenanceTemplateController = + TextEditingController(text: email.maintenanceTemplate); + _warrantyTemplateController = + TextEditingController(text: email.warrantyTemplate); + } + + @override + void dispose() { + _hostController.dispose(); + _portController.dispose(); + _usernameController.dispose(); + _passwordController.dispose(); + _senderEmailController.dispose(); + _senderNameController.dispose(); + _allocationTemplateController.dispose(); + _returnTemplateController.dispose(); + _maintenanceTemplateController.dispose(); + _warrantyTemplateController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + await ref.read(appSettingsProvider.notifier).updateEmail( + EmailConfigurationSettings( + smtpHost: _hostController.text.trim(), + smtpPort: int.parse(_portController.text.trim()), + smtpUsername: _usernameController.text.trim(), + smtpPassword: _passwordController.text.trim(), + senderEmail: _senderEmailController.text.trim(), + senderName: _senderNameController.text.trim(), + allocationTemplate: _allocationTemplateController.text.trim(), + returnTemplate: _returnTemplateController.text.trim(), + maintenanceTemplate: _maintenanceTemplateController.text.trim(), + warrantyTemplate: _warrantyTemplateController.text.trim(), + ), + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Email configuration saved')), + ); + } + } + + @override + Widget build(BuildContext context) { + return SettingsPageLayout( + title: 'Email Configuration', + subtitle: 'SMTP server settings and email templates', + child: Form( + key: _formKey, + child: Column( + children: [ + SettingsFormCard( + title: 'SMTP Settings', + children: [ + AppTextField( + controller: _hostController, + label: 'SMTP Host', + hint: 'smtp.gmail.com', + ), + const SizedBox(height: 16), + AppTextField( + controller: _portController, + label: 'SMTP Port', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + AppTextField( + controller: _usernameController, + label: 'Username', + ), + const SizedBox(height: 16), + AppTextField( + controller: _passwordController, + label: 'Password', + obscureText: true, + ), + const SizedBox(height: 16), + AppTextField( + controller: _senderEmailController, + label: 'Sender Email', + keyboardType: TextInputType.emailAddress, + validator: Validators.email, + ), + const SizedBox(height: 16), + AppTextField( + controller: _senderNameController, + label: 'Sender Name', + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Email Templates', + subtitle: 'Use {{asset_name}} and {{date}} as placeholders', + children: [ + AppTextField( + controller: _allocationTemplateController, + label: 'Asset Allocation Email', + maxLines: 2, + ), + const SizedBox(height: 16), + AppTextField( + controller: _returnTemplateController, + label: 'Asset Return Email', + maxLines: 2, + ), + const SizedBox(height: 16), + AppTextField( + controller: _maintenanceTemplateController, + label: 'Maintenance Email', + maxLines: 2, + ), + const SizedBox(height: 16), + AppTextField( + controller: _warrantyTemplateController, + label: 'Warranty Expiry Email', + maxLines: 2, + ), + ], + ), + const SizedBox(height: 24), + AppButton(label: 'Save Changes', onPressed: _save), + ], + ), + ), + ); + } +} diff --git a/lib/modules/settings/presentation/screens/general_settings_screen.dart b/lib/modules/settings/presentation/screens/general_settings_screen.dart new file mode 100644 index 0000000..4299eb6 --- /dev/null +++ b/lib/modules/settings/presentation/screens/general_settings_screen.dart @@ -0,0 +1,170 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../domain/entities/app_settings.dart'; +import '../providers/settings_provider.dart'; +import '../widgets/settings_widgets.dart'; + +class GeneralSettingsScreen extends ConsumerStatefulWidget { + const GeneralSettingsScreen({super.key}); + + @override + ConsumerState createState() => + _GeneralSettingsScreenState(); +} + +class _GeneralSettingsScreenState extends ConsumerState { + final _formKey = GlobalKey(); + late final TextEditingController _defaultBranchController; + late final TextEditingController _branchCodeFormatController; + late String _timeZone; + late String _language; + late String _currency; + late String _dateFormat; + late String _timeFormat; + late String _numberFormat; + + @override + void initState() { + super.initState(); + final general = ref.read(appSettingsProvider).general; + _defaultBranchController = TextEditingController(text: general.defaultBranch); + _branchCodeFormatController = + TextEditingController(text: general.branchCodeFormat); + _timeZone = general.timeZone; + _language = general.language; + _currency = general.currency; + _dateFormat = general.dateFormat; + _timeFormat = general.timeFormat; + _numberFormat = general.numberFormat; + } + + @override + void dispose() { + _defaultBranchController.dispose(); + _branchCodeFormatController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + await ref.read(appSettingsProvider.notifier).updateGeneral( + GeneralSettings( + defaultBranch: _defaultBranchController.text.trim(), + branchCodeFormat: _branchCodeFormatController.text.trim(), + timeZone: _timeZone, + language: _language, + currency: _currency, + dateFormat: _dateFormat, + timeFormat: _timeFormat, + numberFormat: _numberFormat, + ), + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('General settings saved')), + ); + } + } + + @override + Widget build(BuildContext context) { + return SettingsPageLayout( + title: 'General Settings', + subtitle: 'Regional preferences and branch defaults', + child: Form( + key: _formKey, + child: Column( + children: [ + SettingsFormCard( + title: 'Branch Settings', + subtitle: 'Default branch and code format', + children: [ + AppTextField( + controller: _defaultBranchController, + label: 'Default Branch', + ), + const SizedBox(height: 16), + AppTextField( + controller: _branchCodeFormatController, + label: 'Branch Code Format', + hint: 'BR-{000}', + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Regional Settings', + subtitle: 'Time zone, language, currency, and formats', + children: [ + SettingsDropdownField( + label: 'Time Zone', + value: _timeZone, + items: const [ + 'Asia/Kolkata', + 'Asia/Dubai', + 'Europe/London', + 'America/New_York', + ], + itemLabel: (v) => v, + onChanged: (v) => setState(() => _timeZone = v!), + ), + const SizedBox(height: 16), + SettingsDropdownField( + label: 'Language', + value: _language, + items: const ['en', 'hi', 'ta'], + itemLabel: (v) => switch (v) { + 'en' => 'English', + 'hi' => 'Hindi', + 'ta' => 'Tamil', + _ => v, + }, + onChanged: (v) => setState(() => _language = v!), + ), + const SizedBox(height: 16), + SettingsDropdownField( + label: 'Currency', + value: _currency, + items: const ['INR', 'USD', 'EUR', 'AED'], + itemLabel: (v) => v, + onChanged: (v) => setState(() => _currency = v!), + ), + const SizedBox(height: 16), + SettingsDropdownField( + label: 'Date Format', + value: _dateFormat, + items: const ['dd/MM/yyyy', 'MM/dd/yyyy', 'yyyy-MM-dd'], + itemLabel: (v) => v, + onChanged: (v) => setState(() => _dateFormat = v!), + ), + const SizedBox(height: 16), + SettingsDropdownField( + label: 'Time Format', + value: _timeFormat, + items: const ['HH:mm', 'hh:mm a'], + itemLabel: (v) => v, + onChanged: (v) => setState(() => _timeFormat = v!), + ), + const SizedBox(height: 16), + SettingsDropdownField( + label: 'Number Format', + value: _numberFormat, + items: const ['1,234.56', '1.234,56'], + itemLabel: (v) => v, + onChanged: (v) => setState(() => _numberFormat = v!), + ), + ], + ), + const SizedBox(height: 24), + AppButton(label: 'Save Changes', onPressed: _save), + ], + ), + ), + ); + } +} diff --git a/lib/modules/settings/presentation/screens/notification_settings_screen.dart b/lib/modules/settings/presentation/screens/notification_settings_screen.dart new file mode 100644 index 0000000..45e48a1 --- /dev/null +++ b/lib/modules/settings/presentation/screens/notification_settings_screen.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/widgets/app_button.dart'; +import '../../domain/entities/app_settings.dart'; +import '../providers/settings_provider.dart'; +import '../widgets/settings_widgets.dart'; + +class NotificationSettingsScreen extends ConsumerWidget { + const NotificationSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final notifications = ref.watch(appSettingsProvider).notifications; + + Future update(NotificationSettingsConfig updated) async { + await ref.read(appSettingsProvider.notifier).updateNotifications(updated); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Notification settings saved')), + ); + } + } + + return SettingsPageLayout( + title: 'Notification Settings', + subtitle: 'Configure email, SMS, push, and in-app notifications', + child: Column( + children: [ + SettingsFormCard( + title: 'Email Notifications', + children: [ + SettingsSwitchTile( + title: 'Asset Allocation', + subtitle: 'Notify when an asset is allocated to an employee', + value: notifications.emailAssetAllocation, + onChanged: (v) => + update(notifications.copyWith(emailAssetAllocation: v)), + ), + SettingsSwitchTile( + title: 'Asset Return', + subtitle: 'Notify when an asset is returned', + value: notifications.emailAssetReturn, + onChanged: (v) => + update(notifications.copyWith(emailAssetReturn: v)), + ), + SettingsSwitchTile( + title: 'Maintenance Request', + subtitle: 'Notify when a maintenance request is created', + value: notifications.emailMaintenanceRequest, + onChanged: (v) => + update(notifications.copyWith(emailMaintenanceRequest: v)), + ), + SettingsSwitchTile( + title: 'Warranty Expiry', + subtitle: 'Notify before asset warranty expires', + value: notifications.emailWarrantyExpiry, + onChanged: (v) => + update(notifications.copyWith(emailWarrantyExpiry: v)), + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Other Channels', + children: [ + SettingsSwitchTile( + title: 'SMS Notifications', + value: notifications.smsEnabled, + onChanged: (v) => update(notifications.copyWith(smsEnabled: v)), + ), + SettingsSwitchTile( + title: 'Push Notifications', + value: notifications.pushEnabled, + onChanged: (v) => update(notifications.copyWith(pushEnabled: v)), + ), + SettingsSwitchTile( + title: 'In-App Notifications', + value: notifications.inAppEnabled, + onChanged: (v) => + update(notifications.copyWith(inAppEnabled: v)), + ), + ], + ), + const SizedBox(height: 24), + AppButton( + label: 'Settings Auto-Saved', + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Notification settings are saved automatically'), + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/modules/settings/presentation/screens/roles_permissions_settings_screen.dart b/lib/modules/settings/presentation/screens/roles_permissions_settings_screen.dart new file mode 100644 index 0000000..0693957 --- /dev/null +++ b/lib/modules/settings/presentation/screens/roles_permissions_settings_screen.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../widgets/settings_widgets.dart'; + +class RolesPermissionsSettingsScreen extends StatelessWidget { + const RolesPermissionsSettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return SettingsPageLayout( + title: 'Roles & Permissions', + subtitle: 'Manage roles, permissions, and menu visibility', + child: Column( + children: [ + SettingsFormCard( + title: 'Role Management', + subtitle: 'Create, edit, and clone roles with module permissions', + children: [ + ListTile( + leading: const Icon(Icons.add_moderator_outlined), + title: const Text('Manage Roles'), + subtitle: const Text('Create, edit, clone roles and assign permissions'), + trailing: const Icon(Icons.chevron_right), + onTap: () => context.go(RouteConstants.usersRoleManagement), + ), + const Divider(), + _PermissionInfoRow( + module: 'Dashboard', + permissions: 'Read', + ), + _PermissionInfoRow( + module: 'Assets', + permissions: 'Create, Read, Update, Delete, Export, Approve', + ), + _PermissionInfoRow( + module: 'Reports', + permissions: 'Read, Export', + ), + _PermissionInfoRow( + module: 'Settings', + permissions: 'Read, Update', + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Menu Visibility', + subtitle: 'Menus are shown based on role permissions automatically', + children: [ + const ListTile( + leading: Icon(Icons.visibility_outlined), + title: Text('Dynamic Menu'), + subtitle: Text( + 'The sidebar shows only modules the user has read access to. ' + 'Super Admin and Company Admin see all permitted modules.', + ), + ), + ListTile( + leading: const Icon(Icons.dashboard_outlined), + title: const Text('Dashboard Access'), + subtitle: const Text('Controlled by dashboard.read permission'), + trailing: OutlinedButton( + onPressed: () => context.go(RouteConstants.dashboard), + child: const Text('View Dashboard'), + ), + ), + ], + ), + ], + ), + ); + } +} + +class _PermissionInfoRow extends StatelessWidget { + const _PermissionInfoRow({ + required this.module, + required this.permissions, + }); + + final String module; + final String permissions; + + @override + Widget build(BuildContext context) { + return ListTile( + contentPadding: EdgeInsets.zero, + title: Text(module), + subtitle: Text(permissions), + leading: Icon( + Icons.check_circle_outline, + color: Theme.of(context).colorScheme.primary, + size: 20, + ), + ); + } +} diff --git a/lib/modules/settings/presentation/screens/security_settings_screen.dart b/lib/modules/settings/presentation/screens/security_settings_screen.dart new file mode 100644 index 0000000..7c3a16f --- /dev/null +++ b/lib/modules/settings/presentation/screens/security_settings_screen.dart @@ -0,0 +1,195 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../domain/entities/app_settings.dart'; +import '../providers/settings_provider.dart'; +import '../widgets/settings_widgets.dart'; + +class SecuritySettingsScreen extends ConsumerStatefulWidget { + const SecuritySettingsScreen({super.key}); + + @override + ConsumerState createState() => + _SecuritySettingsScreenState(); +} + +class _SecuritySettingsScreenState extends ConsumerState { + final _formKey = GlobalKey(); + late final TextEditingController _minPasswordController; + late final TextEditingController _passwordExpiryController; + late final TextEditingController _sessionTimeoutController; + late final TextEditingController _loginAttemptController; + late final TextEditingController _lockDurationController; + late final TextEditingController _ipWhitelistController; + late bool _otpEnabled; + late bool _mfaEnabled; + late bool _autoLogout; + late bool _concurrentLogin; + late bool _auditLogging; + + @override + void initState() { + super.initState(); + final security = ref.read(appSettingsProvider).security; + _minPasswordController = + TextEditingController(text: '${security.minPasswordLength}'); + _passwordExpiryController = + TextEditingController(text: '${security.passwordExpiryDays}'); + _sessionTimeoutController = + TextEditingController(text: '${security.sessionTimeoutMinutes}'); + _loginAttemptController = + TextEditingController(text: '${security.loginAttemptLimit}'); + _lockDurationController = + TextEditingController(text: '${security.accountLockDurationMinutes}'); + _ipWhitelistController = TextEditingController(text: security.ipWhitelist); + _otpEnabled = security.otpEnabled; + _mfaEnabled = security.mfaEnabled; + _autoLogout = security.autoLogout; + _concurrentLogin = security.concurrentLoginControl; + _auditLogging = security.auditLoggingEnabled; + } + + @override + void dispose() { + _minPasswordController.dispose(); + _passwordExpiryController.dispose(); + _sessionTimeoutController.dispose(); + _loginAttemptController.dispose(); + _lockDurationController.dispose(); + _ipWhitelistController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + await ref.read(appSettingsProvider.notifier).updateSecurity( + SecuritySettingsConfig( + minPasswordLength: int.parse(_minPasswordController.text.trim()), + passwordExpiryDays: int.parse(_passwordExpiryController.text.trim()), + otpEnabled: _otpEnabled, + mfaEnabled: _mfaEnabled, + sessionTimeoutMinutes: + int.parse(_sessionTimeoutController.text.trim()), + autoLogout: _autoLogout, + concurrentLoginControl: _concurrentLogin, + loginAttemptLimit: int.parse(_loginAttemptController.text.trim()), + accountLockDurationMinutes: + int.parse(_lockDurationController.text.trim()), + auditLoggingEnabled: _auditLogging, + ipWhitelist: _ipWhitelistController.text.trim(), + ), + ); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Security settings saved')), + ); + } + } + + @override + Widget build(BuildContext context) { + return SettingsPageLayout( + title: 'Security Settings', + subtitle: 'Authentication, session, and audit policies', + actions: [ + TextButton.icon( + onPressed: () => context.push(RouteConstants.changePassword), + icon: const Icon(Icons.password), + label: const Text('Change Password'), + ), + ], + child: Form( + key: _formKey, + child: Column( + children: [ + SettingsFormCard( + title: 'Authentication', + children: [ + AppTextField( + controller: _minPasswordController, + label: 'Minimum Password Length', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + AppTextField( + controller: _passwordExpiryController, + label: 'Password Expiry Days', + keyboardType: TextInputType.number, + ), + SettingsSwitchTile( + title: 'OTP Enable/Disable', + value: _otpEnabled, + onChanged: (v) => setState(() => _otpEnabled = v), + ), + SettingsSwitchTile( + title: 'MFA Enable/Disable', + value: _mfaEnabled, + onChanged: (v) => setState(() => _mfaEnabled = v), + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Session Settings', + children: [ + AppTextField( + controller: _sessionTimeoutController, + label: 'Session Timeout (minutes)', + keyboardType: TextInputType.number, + ), + SettingsSwitchTile( + title: 'Auto Logout', + value: _autoLogout, + onChanged: (v) => setState(() => _autoLogout = v), + ), + SettingsSwitchTile( + title: 'Concurrent Login Control', + value: _concurrentLogin, + onChanged: (v) => setState(() => _concurrentLogin = v), + ), + ], + ), + const SizedBox(height: 16), + SettingsFormCard( + title: 'Security Policies', + children: [ + AppTextField( + controller: _loginAttemptController, + label: 'Login Attempt Limit', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + AppTextField( + controller: _lockDurationController, + label: 'Account Lock Duration (minutes)', + keyboardType: TextInputType.number, + ), + const SizedBox(height: 16), + AppTextField( + controller: _ipWhitelistController, + label: 'IP Whitelist', + hint: '192.168.1.1, 10.0.0.0/24', + maxLines: 2, + ), + SettingsSwitchTile( + title: 'Audit Logging', + subtitle: 'Track login, asset changes, and allocations', + value: _auditLogging, + onChanged: (v) => setState(() => _auditLogging = v), + ), + ], + ), + const SizedBox(height: 24), + AppButton(label: 'Save Changes', onPressed: _save), + ], + ), + ), + ); + } +} diff --git a/lib/modules/settings/presentation/screens/settings_screen.dart b/lib/modules/settings/presentation/screens/settings_screen.dart new file mode 100644 index 0000000..766bd39 --- /dev/null +++ b/lib/modules/settings/presentation/screens/settings_screen.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../../domain/entities/app_settings.dart'; +import '../widgets/settings_widgets.dart'; + +class SettingsScreen extends StatelessWidget { + const SettingsScreen({super.key}); + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: context.contentMaxWidth), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const PageHeader( + title: 'Settings', + subtitle: 'Configure company, security, assets, and system behavior', + ), + Text( + 'Phase 1 — Asset Management MVP', + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + SettingsSectionGrid( + sections: phase1SettingsSections, + onSectionTap: (section) => context.go(section.route), + ), + const SizedBox(height: 24), + Text( + 'Coming in Phase 2', + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + SettingsSectionGrid( + sections: phase2SettingsSections, + enabled: false, + badge: 'Phase 2', + onSectionTap: (_) {}, + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/settings/presentation/widgets/settings_widgets.dart b/lib/modules/settings/presentation/widgets/settings_widgets.dart new file mode 100644 index 0000000..2bd8fa6 --- /dev/null +++ b/lib/modules/settings/presentation/widgets/settings_widgets.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/widgets/app_card.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../../domain/entities/app_settings.dart'; + +class SettingsPageLayout extends StatelessWidget { + const SettingsPageLayout({ + super.key, + required this.title, + this.subtitle, + required this.child, + this.actions, + }); + + final String title; + final String? subtitle; + final Widget child; + final List? actions; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 900), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back), + tooltip: 'Back to Settings', + onPressed: () => context.go('/settings'), + ), + Expanded( + child: PageHeader( + title: title, + subtitle: subtitle, + actions: actions, + ), + ), + ], + ), + child, + ], + ), + ), + ), + ); + } +} + +class SettingsFormCard extends StatelessWidget { + const SettingsFormCard({ + super.key, + required this.title, + required this.children, + this.subtitle, + }); + + final String title; + final String? subtitle; + final List children; + + @override + Widget build(BuildContext context) { + return AppCard( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleMedium), + if (subtitle != null) ...[ + const SizedBox(height: 4), + Text( + subtitle!, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 20), + ...children, + ], + ), + ), + ); + } +} + +class SettingsSwitchTile extends StatelessWidget { + const SettingsSwitchTile({ + super.key, + required this.title, + required this.value, + required this.onChanged, + this.subtitle, + }); + + final String title; + final String? subtitle; + final bool value; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return SwitchListTile( + contentPadding: EdgeInsets.zero, + title: Text(title), + subtitle: subtitle != null ? Text(subtitle!) : null, + value: value, + onChanged: onChanged, + ); + } +} + +class SettingsDropdownField extends StatelessWidget { + const SettingsDropdownField({ + super.key, + required this.label, + required this.value, + required this.items, + required this.itemLabel, + required this.onChanged, + }); + + final String label; + final T value; + final List items; + final String Function(T) itemLabel; + final ValueChanged onChanged; + + @override + Widget build(BuildContext context) { + return DropdownButtonFormField( + value: value, + decoration: InputDecoration(labelText: label), + items: items + .map( + (item) => DropdownMenuItem( + value: item, + child: Text(itemLabel(item)), + ), + ) + .toList(), + onChanged: onChanged, + ); + } +} + +class SettingsSectionTile extends StatelessWidget { + const SettingsSectionTile({ + super.key, + required this.title, + required this.subtitle, + required this.icon, + required this.onTap, + this.badge, + this.enabled = true, + }); + + final String title; + final String subtitle; + final IconData icon; + final VoidCallback onTap; + final String? badge; + final bool enabled; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AppCard( + clipBehavior: Clip.antiAlias, + onTap: enabled ? onTap : null, + enableHover: enabled, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: theme.colorScheme.primary.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: theme.colorScheme.primary), + ), + const Spacer(), + Icon( + enabled ? Icons.chevron_right : Icons.lock_outline, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: Text( + title, + style: theme.textTheme.titleSmall, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + if (badge != null) ...[ + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 2, + ), + decoration: BoxDecoration( + color: theme.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + badge!, + style: theme.textTheme.labelSmall, + ), + ), + ], + ], + ), + const SizedBox(height: 6), + Expanded( + child: Text( + subtitle, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ), + ); + } +} + +class SettingsSectionGrid extends StatelessWidget { + const SettingsSectionGrid({ + super.key, + required this.sections, + required this.onSectionTap, + this.enabled = true, + this.badge, + }); + + final List sections; + final void Function(SettingsSection section) onSectionTap; + final bool enabled; + final String? badge; + + int _crossAxisCount(BuildContext context) { + if (context.isMobile) return 1; + if (context.isTablet) return 2; + return 3; + } + + @override + Widget build(BuildContext context) { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: sections.length, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: _crossAxisCount(context), + crossAxisSpacing: 16, + mainAxisSpacing: 16, + mainAxisExtent: 168, + ), + itemBuilder: (context, index) { + final section = sections[index]; + return SettingsSectionTile( + title: section.title, + subtitle: section.subtitle, + icon: section.icon, + badge: badge, + enabled: enabled, + onTap: () => onSectionTap(section), + ); + }, + ); + } +} diff --git a/lib/modules/users/data/datasources/user_remote_data_source.dart b/lib/modules/users/data/datasources/user_remote_data_source.dart new file mode 100644 index 0000000..cd48800 --- /dev/null +++ b/lib/modules/users/data/datasources/user_remote_data_source.dart @@ -0,0 +1,159 @@ +import 'package:dio/dio.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/user_management_models.dart'; + +class UserRemoteDataSource { + UserRemoteDataSource({required this.dio}); + + final Dio dio; + + Future getSummary() async { + final response = await dio.get(ApiEndpoints.usersSummary); + final data = response.data['data'] as Map; + final users = data['users'] as Map? ?? {}; + final roles = data['roles'] as Map? ?? {}; + + return UserSummaryModel( + totalUsers: (users['total'] as num?)?.toInt() ?? 0, + activeUsers: (users['active'] as num?)?.toInt() ?? 0, + inactiveUsers: (users['inactive'] as num?)?.toInt() ?? 0, + lockedUsers: (users['locked'] as num?)?.toInt() ?? 0, + rolesCount: (roles['total'] as num?)?.toInt() ?? 0, + ); + } + + Future getFilters() async { + final response = await dio.get(ApiEndpoints.usersFilters); + final data = response.data['data'] as Map; + + List parseIdNameOptions(List? list) { + return (list ?? []) + .map( + (item) => FilterOptionModel.fromJson(item as Map), + ) + .toList(); + } + + List parseStatusOptions(List? list) { + return (list ?? []) + .map( + (item) { + final map = item as Map; + return FilterOptionModel( + id: map['value'] as String? ?? '', + name: map['label'] as String? ?? '', + ); + }, + ) + .toList(); + } + + return UserFiltersModel( + roles: parseIdNameOptions(data['roles'] as List?), + departments: parseIdNameOptions(data['departments'] as List?), + statuses: parseStatusOptions(data['statuses'] as List?), + ); + } + + Future> getUsers(UserListQuery query) async { + final response = await dio.get( + ApiEndpoints.users, + queryParameters: _queryToMap(query), + ); + final body = response.data as Map; + final rawItems = body['data']; + + final items = rawItems is List + ? rawItems + .map( + (item) => ManagedUserModel.fromJson(item as Map), + ) + .toList() + : PaginatedResponse.fromJson( + rawItems as Map, + (json) => ManagedUserModel.fromJson(json! as Map), + ).items; + + final meta = body['meta'] as Map? ?? {}; + final page = (meta['page'] as num?)?.toInt() ?? query.page; + final limit = (meta['limit'] as num?)?.toInt() ?? query.limit; + final total = (meta['total'] as num?)?.toInt() ?? items.length; + final totalPages = limit > 0 ? ((total + limit - 1) / limit).ceil().clamp(1, 999999) : 1; + + return PaginatedResponse( + items: items, + page: page, + limit: limit, + total: total, + totalPages: totalPages, + ); + } + + Future getUserById(String id) async { + final response = await dio.get(ApiEndpoints.userById(id)); + return ManagedUserModel.fromJson(response.data['data'] as Map); + } + + Future createUser(CreateUserRequest request) async { + final response = await dio.post( + ApiEndpoints.users, + data: request.toJson()..removeWhere((_, v) => v == null), + ); + final body = response.data; + if (body is! Map) { + throw FormatException('Unexpected create user response'); + } + + final data = body['data']; + if (data is Map) { + return ManagedUserModel.fromJson(data); + } + + return ManagedUserModel.fromJson(body); + } + + Future updateUser(String id, UpdateUserRequest request) async { + final response = await dio.put( + ApiEndpoints.userById(id), + data: request.toJson()..removeWhere((_, v) => v == null), + ); + return ManagedUserModel.fromJson(response.data['data'] as Map); + } + + Future deleteUser(String id) async { + await dio.delete(ApiEndpoints.userById(id)); + } + + Future> exportUsers(UserListQuery query) async { + final response = await dio.get>( + ApiEndpoints.usersExport, + queryParameters: _queryToMap(query), + options: Options(responseType: ResponseType.bytes), + ); + return response.data ?? []; + } + + Future updateProfile(UpdateProfileRequest request) async { + final response = await dio.put( + ApiEndpoints.me, + data: request.toJson()..removeWhere((_, v) => v == null), + ); + return ManagedUserModel.fromJson(response.data['data'] as Map); + } + + Map _queryToMap(UserListQuery query) { + return { + 'page': query.page, + 'limit': query.limit, + if (query.search != null && query.search!.isNotEmpty) 'search': query.search, + if (query.status != null) 'status': query.status, + if (query.roleId != null) 'role_id': query.roleId, + if (query.departmentId != null) 'department_id': query.departmentId, + if (query.sortBy != null) 'sort_by': query.sortBy, + if (query.sortOrder.isNotEmpty) 'sort_order': query.sortOrder, + if (query.isActive != null) 'is_active': query.isActive, + }; + } +} diff --git a/lib/modules/users/data/repositories/user_repository_impl.dart b/lib/modules/users/data/repositories/user_repository_impl.dart new file mode 100644 index 0000000..c223673 --- /dev/null +++ b/lib/modules/users/data/repositories/user_repository_impl.dart @@ -0,0 +1,63 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/network/api_handler.dart'; +import '../../../../core/network/dio_client.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../domain/repositories/user_repository.dart'; +import '../datasources/user_remote_data_source.dart'; + +final userRemoteDataSourceProvider = Provider((ref) { + return UserRemoteDataSource(dio: ref.watch(dioProvider)); +}); + +final userRepositoryProvider = Provider((ref) { + return UserRepositoryImpl(remote: ref.watch(userRemoteDataSourceProvider)); +}); + +class UserRepositoryImpl implements UserRepository { + UserRepositoryImpl({required this.remote}); + + final UserRemoteDataSource remote; + + @override + Future> getSummary() => + safeApiCall(remote.getSummary); + + @override + Future> getFilters() => + safeApiCall(remote.getFilters); + + @override + Future>> getUsers( + UserListQuery query, + ) => + safeApiCall(() => remote.getUsers(query)); + + @override + Future> getUserById(String id) => + safeApiCall(() => remote.getUserById(id)); + + @override + Future> createUser(CreateUserRequest request) => + safeApiCall(() => remote.createUser(request)); + + @override + Future> updateUser( + String id, + UpdateUserRequest request, + ) => + safeApiCall(() => remote.updateUser(id, request)); + + @override + Future> deleteUser(String id) => + safeApiCall(() => remote.deleteUser(id)); + + @override + Future>> exportUsers(UserListQuery query) => + safeApiCall(() => remote.exportUsers(query)); + + @override + Future> updateProfile(UpdateProfileRequest request) => + safeApiCall(() => remote.updateProfile(request)); +} diff --git a/lib/modules/users/domain/repositories/user_repository.dart b/lib/modules/users/domain/repositories/user_repository.dart new file mode 100644 index 0000000..9d59579 --- /dev/null +++ b/lib/modules/users/domain/repositories/user_repository.dart @@ -0,0 +1,15 @@ +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/user_management_models.dart'; + +abstract class UserRepository { + Future> getSummary(); + Future> getFilters(); + Future>> getUsers(UserListQuery query); + Future> getUserById(String id); + Future> createUser(CreateUserRequest request); + Future> updateUser(String id, UpdateUserRequest request); + Future> deleteUser(String id); + Future>> exportUsers(UserListQuery query); + Future> updateProfile(UpdateProfileRequest request); +} diff --git a/lib/modules/users/domain/usecases/user_usecases.dart b/lib/modules/users/domain/usecases/user_usecases.dart new file mode 100644 index 0000000..8a744ba --- /dev/null +++ b/lib/modules/users/domain/usecases/user_usecases.dart @@ -0,0 +1,72 @@ +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../repositories/user_repository.dart'; + +class GetUsersUseCase { + GetUsersUseCase(this._repository); + + final UserRepository _repository; + + Future>> call(UserListQuery query) => + _repository.getUsers(query); +} + +class GetUserByIdUseCase { + GetUserByIdUseCase(this._repository); + + final UserRepository _repository; + + Future> call(String id) => _repository.getUserById(id); +} + +class CreateUserUseCase { + CreateUserUseCase(this._repository); + + final UserRepository _repository; + + Future> call(CreateUserRequest request) => + _repository.createUser(request); +} + +class UpdateUserUseCase { + UpdateUserUseCase(this._repository); + + final UserRepository _repository; + + Future> call(String id, UpdateUserRequest request) => + _repository.updateUser(id, request); +} + +class DeleteUserUseCase { + DeleteUserUseCase(this._repository); + + final UserRepository _repository; + + Future> call(String id) => _repository.deleteUser(id); +} + +class GetUserSummaryUseCase { + GetUserSummaryUseCase(this._repository); + + final UserRepository _repository; + + Future> call() => _repository.getSummary(); +} + +class GetUserFiltersUseCase { + GetUserFiltersUseCase(this._repository); + + final UserRepository _repository; + + Future> call() => _repository.getFilters(); +} + +class UpdateProfileUseCase { + UpdateProfileUseCase(this._repository); + + final UserRepository _repository; + + Future> call(UpdateProfileRequest request) => + _repository.updateProfile(request); +} diff --git a/lib/modules/users/presentation/providers/users_provider.dart b/lib/modules/users/presentation/providers/users_provider.dart new file mode 100644 index 0000000..b1a2b4f --- /dev/null +++ b/lib/modules/users/presentation/providers/users_provider.dart @@ -0,0 +1,265 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../data/repositories/user_repository_impl.dart'; +import '../../domain/usecases/user_usecases.dart'; + +class UsersListState { + const UsersListState({ + this.users = const [], + this.summary, + this.filters, + this.query = const UserListQuery(), + this.total = 0, + this.totalPages = 1, + this.isRefreshing = false, + this.actionError, + this.actionSuccess, + }); + + final List users; + final UserSummaryModel? summary; + final UserFiltersModel? filters; + final UserListQuery query; + final int total; + final int totalPages; + final bool isRefreshing; + final String? actionError; + final String? actionSuccess; + + UsersListState copyWith({ + List? users, + UserSummaryModel? summary, + UserFiltersModel? filters, + UserListQuery? query, + int? total, + int? totalPages, + bool? isRefreshing, + String? actionError, + String? actionSuccess, + bool clearMessages = false, + }) { + return UsersListState( + users: users ?? this.users, + summary: summary ?? this.summary, + filters: filters ?? this.filters, + query: query ?? this.query, + total: total ?? this.total, + totalPages: totalPages ?? this.totalPages, + isRefreshing: isRefreshing ?? this.isRefreshing, + actionError: clearMessages ? null : actionError ?? this.actionError, + actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess, + ); + } +} + +final getUsersUseCaseProvider = Provider((ref) { + return GetUsersUseCase(ref.watch(userRepositoryProvider)); +}); + +final getUserSummaryUseCaseProvider = Provider((ref) { + return GetUserSummaryUseCase(ref.watch(userRepositoryProvider)); +}); + +final getUserFiltersUseCaseProvider = Provider((ref) { + return GetUserFiltersUseCase(ref.watch(userRepositoryProvider)); +}); + +final createUserUseCaseProvider = Provider((ref) { + return CreateUserUseCase(ref.watch(userRepositoryProvider)); +}); + +final updateUserUseCaseProvider = Provider((ref) { + return UpdateUserUseCase(ref.watch(userRepositoryProvider)); +}); + +final deleteUserUseCaseProvider = Provider((ref) { + return DeleteUserUseCase(ref.watch(userRepositoryProvider)); +}); + +final updateProfileUseCaseProvider = Provider((ref) { + return UpdateProfileUseCase(ref.watch(userRepositoryProvider)); +}); + +final getUserByIdUseCaseProvider = Provider((ref) { + return GetUserByIdUseCase(ref.watch(userRepositoryProvider)); +}); + +final usersListProvider = + AsyncNotifierProvider(UsersListNotifier.new); + +class UsersListNotifier extends AsyncNotifier { + @override + Future build() async { + return _loadAll(const UserListQuery(limit: 10)); + } + + Future _loadAll(UserListQuery query) async { + final summaryResult = await ref.read(getUserSummaryUseCaseProvider)(); + final filtersResult = await ref.read(getUserFiltersUseCaseProvider)(); + final usersResult = await ref.read(getUsersUseCaseProvider)(query); + + if (usersResult.failure != null) throw usersResult.failure!; + + final page = usersResult.data!; + return UsersListState( + users: page.items, + summary: summaryResult.data, + filters: filtersResult.data, + query: query, + total: page.total, + totalPages: page.totalPages, + ); + } + + Future refresh() async { + final current = state.valueOrNull ?? const UsersListState(); + state = AsyncData(current.copyWith(isRefreshing: true, clearMessages: true)); + try { + state = AsyncData(await _loadAll(current.query)); + } catch (e, st) { + state = AsyncError(e, st); + } + } + + Future applyQuery(UserListQuery query) async { + state = const AsyncLoading(); + try { + state = AsyncData(await _loadAll(query)); + } catch (e, st) { + state = AsyncError(e, st); + } + } + + void setSearch(String search) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(search: search, page: 1)); + } + + void setPage(int page) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(page: page)); + } + + void setPageSize(int limit) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(limit: limit, page: 1)); + } + + void setSort(String sortBy, String sortOrder) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(sortBy: sortBy, sortOrder: sortOrder, page: 1)); + } + + void setStatusFilter(String? status) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(status: status, page: 1)); + } + + void setRoleFilter(int? roleId) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(roleId: roleId, page: 1)); + } + + void setDepartmentFilter(int? departmentId) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(departmentId: departmentId, page: 1)); + } + + Future deactivateUser(String id) async { + final result = await ref.read(deleteUserUseCaseProvider)(id); + if (result.failure != null) { + final current = state.valueOrNull; + if (current != null) { + state = AsyncData(current.copyWith(actionError: result.failure.toString())); + } + return false; + } + await refresh(); + return true; + } + + Future toggleUserStatus(ManagedUserModel user) async { + final updateUseCase = ref.read(updateUserUseCaseProvider); + final activating = user.status != 'active'; + final result = await updateUseCase( + user.id, + UpdateUserRequest( + status: activating ? 'active' : 'inactive', + isActive: activating, + ), + ); + if (result.failure != null) { + final current = state.valueOrNull; + if (current != null) { + state = AsyncData(current.copyWith(actionError: result.failure.toString())); + } + return false; + } + await refresh(); + return true; + } +} + +final userDetailProvider = AsyncNotifierProvider.family( + UserDetailNotifier.new, +); + +class UserDetailNotifier extends FamilyAsyncNotifier { + @override + Future build(String arg) async { + final useCase = ref.read(getUserByIdUseCaseProvider); + final result = await useCase(arg); + if (result.failure != null) throw result.failure!; + return result.data!; + } + + Future deactivate() async { + final deleteUseCase = ref.read(deleteUserUseCaseProvider); + final result = await deleteUseCase(arg); + if (result.failure != null) return false; + ref.invalidateSelf(); + await future; + return true; + } +} + +final userFormProvider = AsyncNotifierProvider.family( + UserFormNotifier.new, +); + +class UserFormNotifier extends FamilyAsyncNotifier { + @override + Future build(String? arg) async { + if (arg == null) return null; + final useCase = ref.read(getUserByIdUseCaseProvider); + final result = await useCase(arg); + if (result.failure != null) throw result.failure!; + return result.data; + } + + Future submitCreate(CreateUserRequest request) async { + final useCase = ref.read(createUserUseCaseProvider); + final result = await useCase(request); + if (result.failure != null) throw result.failure!; + ref.invalidate(usersListProvider); + return result.data; + } + + Future submitUpdate(String id, UpdateUserRequest request) async { + final useCase = ref.read(updateUserUseCaseProvider); + final result = await useCase(id, request); + if (result.failure != null) throw result.failure!; + ref.invalidate(usersListProvider); + ref.invalidate(userDetailProvider(id)); + return result.data; + } +} diff --git a/lib/modules/users/presentation/screens/user_detail_screen.dart b/lib/modules/users/presentation/screens/user_detail_screen.dart new file mode 100644 index 0000000..9d1acdc --- /dev/null +++ b/lib/modules/users/presentation/screens/user_detail_screen.dart @@ -0,0 +1,143 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:intl/intl.dart'; + +import '../../../../core/errors/failure.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_confirmation_dialog.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_status_chip.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../providers/users_provider.dart'; + +class UserDetailScreen extends ConsumerWidget { + const UserDetailScreen({super.key, required this.userId}); + + final String userId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final userAsync = ref.watch(userDetailProvider(userId)); + + return Padding( + padding: const EdgeInsets.all(24), + child: userAsync.when( + loading: () => const AppLoadingView(message: 'Loading user details...'), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(userDetailProvider(userId)), + ), + data: (user) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: user.fullName, + subtitle: user.employeeCode, + actions: [ + OutlinedButton.icon( + onPressed: () => context.push('/users/$userId/edit'), + icon: const Icon(Icons.edit_outlined), + label: const Text('Edit'), + ), + const SizedBox(width: 8), + AppButton( + label: 'Deactivate', + expand: false, + onPressed: () => _deactivate(context, ref), + ), + ], + ), + const SizedBox(height: 24), + AppCard( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _DetailRow(label: 'Email', value: user.email), + _DetailRow(label: 'Mobile', value: user.mobile), + _DetailRow(label: 'Role', value: user.roleLabel), + _DetailRow(label: 'Department', value: user.departmentLabel), + _DetailRow( + label: 'Status', + valueWidget: AppStatusChip(status: user.status), + ), + if (user.createdAt != null) + _DetailRow( + label: 'Created', + value: DateFormat.yMMMd().add_jm().format(user.createdAt!), + ), + if (user.updatedAt != null) + _DetailRow( + label: 'Updated', + value: DateFormat.yMMMd().add_jm().format(user.updatedAt!), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Future _deactivate(BuildContext context, WidgetRef ref) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Deactivate user', + message: 'Are you sure you want to deactivate this user?', + confirmLabel: 'Deactivate', + isDestructive: true, + ); + if (confirmed != true || !context.mounted) return; + + final success = await ref.read(userDetailProvider(userId).notifier).deactivate(); + if (!context.mounted) return; + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(success ? 'User deactivated' : 'Failed to deactivate')), + ); + if (success) context.pop(); + } +} + +class _DetailRow extends StatelessWidget { + const _DetailRow({ + required this.label, + this.value, + this.valueWidget, + }); + + final String label; + final String? value; + final Widget? valueWidget; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 140, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded( + child: valueWidget ?? Text(value ?? '—'), + ), + ], + ), + ); + } +} diff --git a/lib/modules/users/presentation/screens/user_form_screen.dart b/lib/modules/users/presentation/screens/user_form_screen.dart new file mode 100644 index 0000000..8144528 --- /dev/null +++ b/lib/modules/users/presentation/screens/user_form_screen.dart @@ -0,0 +1,290 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../providers/users_provider.dart'; + +class UserFormScreen extends ConsumerStatefulWidget { + const UserFormScreen({super.key, this.userId}); + + final String? userId; + + @override + ConsumerState createState() => _UserFormScreenState(); +} + +class _UserFormScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _employeeIdController = TextEditingController(); + final _firstNameController = TextEditingController(); + final _lastNameController = TextEditingController(); + final _emailController = TextEditingController(); + final _mobileController = TextEditingController(); + final _passwordController = TextEditingController(); + int? _selectedRoleId; + int? _selectedDepartmentId; + String _selectedStatus = 'active'; + bool _isSubmitting = false; + bool _prefilled = false; + + bool get isEditing => widget.userId != null; + + @override + void dispose() { + _employeeIdController.dispose(); + _firstNameController.dispose(); + _lastNameController.dispose(); + _emailController.dispose(); + _mobileController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + void _prefill(ManagedUserModel user) { + if (_prefilled) return; + _prefilled = true; + _employeeIdController.text = user.employeeCode; + final parts = user.fullName.split(' '); + _firstNameController.text = user.firstName ?? parts.first; + _lastNameController.text = + user.lastName ?? (parts.length > 1 ? parts.sublist(1).join(' ') : ''); + _emailController.text = user.email; + _mobileController.text = user.mobile; + _selectedRoleId = int.tryParse(user.roleId ?? ''); + _selectedDepartmentId = int.tryParse(user.departmentId ?? ''); + _selectedStatus = user.status; + } + + void _resetForm() { + _formKey.currentState?.reset(); + _employeeIdController.clear(); + _firstNameController.clear(); + _lastNameController.clear(); + _emailController.clear(); + _mobileController.clear(); + _passwordController.clear(); + setState(() { + _selectedRoleId = null; + _selectedDepartmentId = null; + _selectedStatus = 'active'; + }); + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + if (_selectedRoleId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please select a role')), + ); + return; + } + + setState(() => _isSubmitting = true); + final notifier = ref.read(userFormProvider(widget.userId).notifier); + final fullName = + '${_firstNameController.text.trim()} ${_lastNameController.text.trim()}'.trim(); + + try { + if (isEditing) { + await notifier.submitUpdate( + widget.userId!, + UpdateUserRequest( + employeeCode: _employeeIdController.text.trim(), + fullName: fullName, + email: _emailController.text.trim(), + mobile: _mobileController.text.trim(), + roleId: _selectedRoleId, + departmentId: _selectedDepartmentId, + status: _selectedStatus, + isActive: _selectedStatus == 'active', + password: _passwordController.text.isEmpty ? null : _passwordController.text, + ), + ); + } else { + await notifier.submitCreate( + CreateUserRequest( + employeeCode: _employeeIdController.text.trim(), + fullName: fullName, + email: _emailController.text.trim(), + password: _passwordController.text, + mobile: _mobileController.text.trim(), + roleId: _selectedRoleId!, + departmentId: _selectedDepartmentId, + status: _selectedStatus, + isActive: _selectedStatus == 'active', + ), + ); + } + + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(isEditing ? 'User updated' : 'User created')), + ); + context.pop(); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + @override + Widget build(BuildContext context) { + final formAsync = ref.watch(userFormProvider(widget.userId)); + final filters = ref.watch(usersListProvider).valueOrNull?.filters; + + return Scaffold( + appBar: AppBar(title: Text(isEditing ? 'Edit User' : 'Add User')), + body: formAsync.when( + loading: () => const AppLoadingView(message: 'Loading user...'), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(userFormProvider(widget.userId)), + ), + data: (user) { + if (user != null) _prefill(user); + + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _employeeIdController, + label: 'Employee ID', + validator: (v) => Validators.required(v, fieldName: 'Employee ID'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _firstNameController, + label: 'First Name', + validator: (v) => Validators.required(v, fieldName: 'First name'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _lastNameController, + label: 'Last Name', + validator: (v) => Validators.required(v, fieldName: 'Last name'), + ), + const SizedBox(height: 16), + AppTextField( + controller: _emailController, + label: 'Email', + keyboardType: TextInputType.emailAddress, + validator: Validators.email, + ), + const SizedBox(height: 16), + AppTextField( + controller: _mobileController, + label: 'Mobile', + keyboardType: TextInputType.phone, + validator: Validators.phone, + ), + const SizedBox(height: 16), + if (!isEditing) ...[ + AppTextField( + controller: _passwordController, + label: 'Password', + obscureText: true, + validator: Validators.password, + ), + const SizedBox(height: 16), + ], + AppSearchableDropdown( + label: 'Role', + value: _selectedRoleId, + searchHint: 'Search role...', + options: (filters?.roles ?? []) + .map( + (r) => AppDropdownOption( + value: int.tryParse(r.id), + label: r.name, + ), + ) + .toList(), + onChanged: (v) => setState(() => _selectedRoleId = v), + validator: (v) => v == null ? 'Role is required' : null, + ), + const SizedBox(height: 16), + AppSearchableDropdown( + label: 'Department', + value: _selectedDepartmentId, + searchHint: 'Search department...', + options: [ + const AppDropdownOption(value: null, label: 'None'), + ...(filters?.departments ?? []).map( + (d) => AppDropdownOption( + value: int.tryParse(d.id), + label: d.name, + ), + ), + ], + onChanged: (v) => setState(() => _selectedDepartmentId = v), + ), + const SizedBox(height: 16), + AppSearchableDropdown( + label: 'Status', + value: _selectedStatus, + searchHint: 'Search status...', + options: const [ + AppDropdownOption(value: 'active', label: 'Active'), + AppDropdownOption(value: 'inactive', label: 'Inactive'), + AppDropdownOption(value: 'locked', label: 'Locked'), + ], + onChanged: (v) => setState(() => _selectedStatus = v ?? 'active'), + ), + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: AppButton( + label: isEditing ? 'Update User' : 'Create User', + isLoading: _isSubmitting, + onPressed: _submit, + ), + ), + const SizedBox(width: 12), + Expanded( + child: AppButton( + label: 'Cancel', + isOutlined: true, + onPressed: () => context.pop(), + ), + ), + ], + ), + if (!isEditing) ...[ + const SizedBox(height: 12), + AppButton( + label: 'Reset Form', + isOutlined: true, + onPressed: _resetForm, + ), + ], + ], + ), + ), + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/modules/users/presentation/screens/user_list_screen.dart b/lib/modules/users/presentation/screens/user_list_screen.dart new file mode 100644 index 0000000..65f1a2f --- /dev/null +++ b/lib/modules/users/presentation/screens/user_list_screen.dart @@ -0,0 +1,445 @@ +import '../../../../shared/widgets/app_card.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_confirmation_dialog.dart'; +import '../../../../shared/widgets/app_data_table.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_empty_state.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_pagination.dart'; +import '../../../../shared/widgets/app_search_field.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; +import '../../../../shared/widgets/app_status_chip.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/kpi_card.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/users_provider.dart'; + +class UserListScreen extends ConsumerStatefulWidget { + const UserListScreen({super.key}); + + @override + ConsumerState createState() => _UserListScreenState(); +} + +class _UserListScreenState extends ConsumerState { + final _searchController = TextEditingController(); + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final usersAsync = ref.watch(usersListProvider); + + return Padding( + padding: const EdgeInsets.all(24), + child: usersAsync.when( + loading: () => const AppLoadingView(message: 'Loading users...'), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(usersListProvider), + ), + data: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Users', + subtitle: 'Manage employee accounts', + actions: [ + ElevatedButton.icon( + onPressed: () => context.push(RouteConstants.userAdd), + icon: const Icon(Icons.person_add), + label: const Text('Add User'), + ), + ], + ), + if (state.summary != null) ...[ + const SizedBox(height: 16), + _SummaryStrip(summary: state.summary!), + ], + const SizedBox(height: 16), + _FiltersBar( + searchController: _searchController, + filters: state.filters, + query: state.query, + onSearch: ref.read(usersListProvider.notifier).setSearch, + onStatusChanged: (status) => + ref.read(usersListProvider.notifier).setStatusFilter(status), + onRoleChanged: (roleId) => + ref.read(usersListProvider.notifier).setRoleFilter(roleId), + onDepartmentChanged: (deptId) => + ref.read(usersListProvider.notifier).setDepartmentFilter(deptId), + ), + const SizedBox(height: 16), + Expanded( + child: RefreshIndicator( + onRefresh: () => ref.read(usersListProvider.notifier).refresh(), + child: state.users.isEmpty + ? ListView( + children: const [ + AppEmptyState( + title: 'No users found', + description: 'Try adjusting filters or add a new user.', + icon: Icons.people_outline, + ), + ], + ) + : context.isMobile + ? _UserCardList( + users: state.users, + onView: _viewUser, + onEdit: _editUser, + onToggleStatus: _toggleStatus, + onDeactivate: _deactivateUser, + ) + : _UserDataTable( + users: state.users, + sortBy: state.query.sortBy, + sortOrder: state.query.sortOrder, + onSort: (column, ascending) => ref + .read(usersListProvider.notifier) + .setSort(column, ascending ? 'asc' : 'desc'), + onView: _viewUser, + onEdit: _editUser, + onToggleStatus: _toggleStatus, + onDeactivate: _deactivateUser, + ), + ), + ), + const SizedBox(height: 12), + AppPagination( + currentPage: state.query.page, + totalPages: state.totalPages, + totalItems: state.total, + pageSize: state.query.limit, + onPageChanged: ref.read(usersListProvider.notifier).setPage, + onPageSizeChanged: ref.read(usersListProvider.notifier).setPageSize, + ), + ], + ), + ), + ); + } + + void _viewUser(ManagedUserModel user) { + context.push('/users/${user.id}'); + } + + void _editUser(ManagedUserModel user) { + context.push('/users/${user.id}/edit'); + } + + Future _toggleStatus(ManagedUserModel user) async { + final success = await ref.read(usersListProvider.notifier).toggleUserStatus(user); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(success ? 'User status updated' : 'Failed to update status'), + ), + ); + } + + Future _deactivateUser(ManagedUserModel user) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Deactivate user', + message: 'Deactivate ${user.fullName}? This is a soft delete.', + confirmLabel: 'Deactivate', + isDestructive: true, + ); + if (confirmed != true || !mounted) return; + + final success = await ref.read(usersListProvider.notifier).deactivateUser(user.id); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(success ? 'User deactivated' : 'Failed to deactivate user')), + ); + } +} + +class _SummaryStrip extends StatelessWidget { + const _SummaryStrip({required this.summary}); + + final UserSummaryModel summary; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 12, + runSpacing: 12, + children: [ + KpiCard(title: 'Total', value: '${summary.totalUsers}', icon: Icons.people), + KpiCard(title: 'Active', value: '${summary.activeUsers}', icon: Icons.check_circle), + KpiCard(title: 'Inactive', value: '${summary.inactiveUsers}', icon: Icons.pause_circle), + KpiCard(title: 'Locked', value: '${summary.lockedUsers}', icon: Icons.lock), + KpiCard(title: 'Roles', value: '${summary.rolesCount}', icon: Icons.security), + ], + ); + } +} + +class _FiltersBar extends StatelessWidget { + const _FiltersBar({ + required this.searchController, + required this.filters, + required this.query, + required this.onSearch, + required this.onStatusChanged, + required this.onRoleChanged, + required this.onDepartmentChanged, + }); + + final TextEditingController searchController; + final UserFiltersModel? filters; + final UserListQuery query; + final ValueChanged onSearch; + final ValueChanged onStatusChanged; + final ValueChanged onRoleChanged; + final ValueChanged onDepartmentChanged; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 12, + runSpacing: 12, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + SizedBox( + width: context.isMobile ? double.infinity : 280, + child: AppSearchField( + controller: searchController, + hint: 'Search users...', + onChanged: onSearch, + ), + ), + SizedBox( + width: 180, + child: AppSearchableDropdown( + label: 'Status', + value: query.status, + searchHint: 'Search status...', + isDense: true, + options: [ + const AppDropdownOption(value: null, label: 'All statuses'), + ...(filters?.statuses ?? []) + .map((s) => AppDropdownOption(value: s.id, label: s.name)), + ], + onChanged: onStatusChanged, + ), + ), + SizedBox( + width: 180, + child: AppSearchableDropdown( + label: 'Role', + value: query.roleId, + searchHint: 'Search role...', + isDense: true, + options: [ + const AppDropdownOption(value: null, label: 'All roles'), + ...(filters?.roles ?? []).map( + (r) => AppDropdownOption( + value: int.tryParse(r.id), + label: r.name, + ), + ), + ], + onChanged: onRoleChanged, + ), + ), + SizedBox( + width: 180, + child: AppSearchableDropdown( + label: 'Department', + value: query.departmentId, + searchHint: 'Search department...', + isDense: true, + options: [ + const AppDropdownOption(value: null, label: 'All departments'), + ...(filters?.departments ?? []).map( + (d) => AppDropdownOption( + value: int.tryParse(d.id), + label: d.name, + ), + ), + ], + onChanged: onDepartmentChanged, + ), + ), + ], + ); + } +} + +class _UserDataTable extends StatelessWidget { + const _UserDataTable({ + required this.users, + required this.sortBy, + required this.sortOrder, + required this.onSort, + required this.onView, + required this.onEdit, + required this.onToggleStatus, + required this.onDeactivate, + }); + + final List users; + final String? sortBy; + final String sortOrder; + final void Function(String column, bool ascending) onSort; + final void Function(ManagedUserModel user) onView; + final void Function(ManagedUserModel user) onEdit; + final Future Function(ManagedUserModel user) onToggleStatus; + final Future Function(ManagedUserModel user) onDeactivate; + + @override + Widget build(BuildContext context) { + return AppDataTable( + sortColumn: sortBy, + sortAscending: sortOrder == 'asc', + onSort: onSort, + columns: [ + AppDataColumn( + label: 'Employee ID', + sortKey: 'employee_code', + cellBuilder: (_, user) => Text(user.employeeCode), + ), + AppDataColumn( + label: 'Name', + sortKey: 'full_name', + cellBuilder: (_, user) => Text(user.fullName), + ), + AppDataColumn(label: 'Email', cellBuilder: (_, user) => Text(user.email)), + AppDataColumn(label: 'Mobile', cellBuilder: (_, user) => Text(user.mobile)), + AppDataColumn(label: 'Role', cellBuilder: (_, user) => Text(user.roleLabel)), + AppDataColumn( + label: 'Status', + cellBuilder: (_, user) => AppStatusChip(status: user.status), + ), + AppDataColumn( + label: 'Actions', + cellBuilder: (_, user) => _UserActions( + user: user, + onView: onView, + onEdit: onEdit, + onToggleStatus: onToggleStatus, + onDeactivate: onDeactivate, + ), + ), + ], + rows: users, + ); + } +} + +class _UserCardList extends StatelessWidget { + const _UserCardList({ + required this.users, + required this.onView, + required this.onEdit, + required this.onToggleStatus, + required this.onDeactivate, + }); + + final List users; + final void Function(ManagedUserModel user) onView; + final void Function(ManagedUserModel user) onEdit; + final Future Function(ManagedUserModel user) onToggleStatus; + final Future Function(ManagedUserModel user) onDeactivate; + + @override + Widget build(BuildContext context) { + return ListView.separated( + itemCount: users.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final user = users[index]; + return AppCard( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text(user.fullName, style: Theme.of(context).textTheme.titleMedium), + ), + AppStatusChip(status: user.status, compact: true), + ], + ), + const SizedBox(height: 4), + Text(user.employeeCode), + Text(user.email), + Text(user.mobile), + Text('${user.roleLabel} · ${user.departmentLabel}'), + const SizedBox(height: 8), + _UserActions( + user: user, + onView: onView, + onEdit: onEdit, + onToggleStatus: onToggleStatus, + onDeactivate: onDeactivate, + ), + ], + ), + ), + ); + }, + ); + } +} + +class _UserActions extends StatelessWidget { + const _UserActions({ + required this.user, + required this.onView, + required this.onEdit, + required this.onToggleStatus, + required this.onDeactivate, + }); + + final ManagedUserModel user; + final void Function(ManagedUserModel user) onView; + final void Function(ManagedUserModel user) onEdit; + final Future Function(ManagedUserModel user) onToggleStatus; + final Future Function(ManagedUserModel user) onDeactivate; + + @override + Widget build(BuildContext context) { + return Wrap( + spacing: 4, + children: [ + IconButton( + tooltip: 'View', + icon: const Icon(Icons.visibility_outlined), + onPressed: () => onView(user), + ), + IconButton( + tooltip: 'Edit', + icon: const Icon(Icons.edit_outlined), + onPressed: () => onEdit(user), + ), + IconButton( + tooltip: user.status == 'active' ? 'Deactivate' : 'Activate', + icon: Icon(user.status == 'active' ? Icons.pause_circle : Icons.play_circle), + onPressed: () => onToggleStatus(user), + ), + IconButton( + tooltip: 'Delete', + icon: const Icon(Icons.delete_outline), + onPressed: () => onDeactivate(user), + ), + ], + ); + } +} diff --git a/lib/modules/users/presentation/screens/user_profile_screen.dart b/lib/modules/users/presentation/screens/user_profile_screen.dart new file mode 100644 index 0000000..b5058cc --- /dev/null +++ b/lib/modules/users/presentation/screens/user_profile_screen.dart @@ -0,0 +1,266 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/utils/validators.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/models/user_model.dart'; +import '../../../../shared/providers/auth_provider.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../auth/data/repositories/auth_repository_impl.dart'; +import '../providers/users_provider.dart'; + +class UserProfileScreen extends ConsumerStatefulWidget { + const UserProfileScreen({super.key}); + + @override + ConsumerState createState() => _UserProfileScreenState(); +} + +class _UserProfileScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _mobileController = TextEditingController(); + final _currentPasswordController = TextEditingController(); + final _newPasswordController = TextEditingController(); + final _confirmPasswordController = TextEditingController(); + String? _avatarUrl; + bool _isUpdatingProfile = false; + bool _isChangingPassword = false; + + @override + void initState() { + super.initState(); + _loadUser(); + } + + void _loadUser() { + final user = ref.read(authStateProvider).user; + if (user == null) return; + _nameController.text = user.name; + _mobileController.text = user.mobile; + _avatarUrl = user.avatarUrl; + } + + @override + void dispose() { + _nameController.dispose(); + _mobileController.dispose(); + _currentPasswordController.dispose(); + _newPasswordController.dispose(); + _confirmPasswordController.dispose(); + super.dispose(); + } + + Future _pickImage() async { + final result = await FilePicker.pickFiles( + type: FileType.image, + withData: false, + ); + if (result == null || result.files.isEmpty) return; + setState(() => _avatarUrl = result.files.first.path); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Image selected. Upload will use avatar_url on save.')), + ); + } + + Future _updateProfile() async { + if (!_formKey.currentState!.validate()) return; + setState(() => _isUpdatingProfile = true); + + final useCase = ref.read(updateProfileUseCaseProvider); + final result = await useCase( + UpdateProfileRequest( + fullName: _nameController.text.trim(), + mobile: _mobileController.text.trim(), + avatarUrl: _avatarUrl, + ), + ); + + if (!mounted) return; + setState(() => _isUpdatingProfile = false); + + if (result.failure != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(result.failure.toString())), + ); + return; + } + + await ref.read(authStateProvider.notifier).checkAuth(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Profile updated')), + ); + } + + Future _changePassword() async { + if (_newPasswordController.text != _confirmPasswordController.text) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Passwords do not match')), + ); + return; + } + + final passwordError = Validators.password(_newPasswordController.text); + if (passwordError != null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(passwordError))); + return; + } + + setState(() => _isChangingPassword = true); + final repository = ref.read(authRepositoryProvider); + final result = await repository.changePassword( + ChangePasswordRequest( + currentPassword: _currentPasswordController.text, + newPassword: _newPasswordController.text, + confirmPassword: _confirmPasswordController.text, + ), + ); + + if (!mounted) return; + setState(() => _isChangingPassword = false); + + if (result.failure != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(result.failure.toString())), + ); + return; + } + + _currentPasswordController.clear(); + _newPasswordController.clear(); + _confirmPasswordController.clear(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Password changed successfully')), + ); + } + + @override + Widget build(BuildContext context) { + final user = ref.watch(authStateProvider).user; + + if (user == null) { + return const Center(child: Text('Not signed in')); + } + + return SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 640), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('User Profile', style: Theme.of(context).textTheme.headlineSmall), + const SizedBox(height: 24), + Center( + child: Stack( + children: [ + CircleAvatar( + radius: 48, + backgroundImage: + _avatarUrl != null ? NetworkImage(_avatarUrl!) : null, + child: _avatarUrl == null + ? Text( + user.name.isNotEmpty ? user.name[0].toUpperCase() : '?', + style: const TextStyle(fontSize: 32), + ) + : null, + ), + Positioned( + bottom: 0, + right: 0, + child: IconButton.filled( + onPressed: _pickImage, + icon: const Icon(Icons.camera_alt, size: 18), + ), + ), + ], + ), + ), + const SizedBox(height: 24), + Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _nameController, + label: 'Name', + validator: (v) => Validators.required(v, fieldName: 'Name'), + ), + const SizedBox(height: 16), + InputDecorator( + decoration: const InputDecoration(labelText: 'Email'), + child: Text(user.email), + ), + const SizedBox(height: 16), + AppTextField( + controller: _mobileController, + label: 'Mobile', + keyboardType: TextInputType.phone, + validator: Validators.phone, + ), + const SizedBox(height: 8), + Align( + alignment: Alignment.centerLeft, + child: Text('Department: ${user.department ?? '—'}'), + ), + const SizedBox(height: 4), + Align( + alignment: Alignment.centerLeft, + child: Text('Role: ${user.role}'), + ), + const SizedBox(height: 24), + AppButton( + label: 'Update Profile', + isLoading: _isUpdatingProfile, + onPressed: _updateProfile, + ), + ], + ), + ), + const SizedBox(height: 32), + const Divider(), + const SizedBox(height: 16), + Text('Change Password', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + AppTextField( + controller: _currentPasswordController, + label: 'Current Password', + obscureText: true, + ), + const SizedBox(height: 16), + AppTextField( + controller: _newPasswordController, + label: 'New Password', + obscureText: true, + validator: Validators.password, + ), + const SizedBox(height: 16), + AppTextField( + controller: _confirmPasswordController, + label: 'Confirm Password', + obscureText: true, + validator: (v) => Validators.required(v, fieldName: 'Confirm password'), + ), + const SizedBox(height: 16), + AppButton( + label: 'Change Password', + isLoading: _isChangingPassword, + onPressed: _changePassword, + ), + const SizedBox(height: 12), + TextButton( + onPressed: () => context.push(RouteConstants.changePassword), + child: const Text('Open full change password screen'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/modules/users/presentation/screens/users_roles_hub_screen.dart b/lib/modules/users/presentation/screens/users_roles_hub_screen.dart new file mode 100644 index 0000000..54bcea6 --- /dev/null +++ b/lib/modules/users/presentation/screens/users_roles_hub_screen.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/constants/route_constants.dart'; +import '../../../roles/presentation/screens/role_list_screen.dart'; +import 'user_list_screen.dart'; + +class UsersRolesHubScreen extends StatefulWidget { + const UsersRolesHubScreen({super.key, this.initialTab = 0}); + + final int initialTab; + + @override + State createState() => _UsersRolesHubScreenState(); +} + +class _UsersRolesHubScreenState extends State + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this, initialIndex: widget.initialTab); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Material( + color: Theme.of(context).colorScheme.surface, + child: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: 'Users', icon: Icon(Icons.people_outline)), + Tab(text: 'Roles', icon: Icon(Icons.security_outlined)), + ], + ), + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: const [ + UserListScreen(), + RoleListScreen(), + ], + ), + ), + ], + ); + } +} + +int usersRolesTabIndex(String location) { + if (location.startsWith(RouteConstants.roles)) return 1; + return 0; +} diff --git a/lib/shared/models/api_response.dart b/lib/shared/models/api_response.dart new file mode 100644 index 0000000..6d79179 --- /dev/null +++ b/lib/shared/models/api_response.dart @@ -0,0 +1,48 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'api_response.freezed.dart'; +part 'api_response.g.dart'; + +@Freezed(genericArgumentFactories: true) +class ApiResponse with _$ApiResponse { + const factory ApiResponse({ + required bool success, + required String message, + T? data, + Map? meta, + }) = _ApiResponse; + + factory ApiResponse.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) => + _$ApiResponseFromJson(json, fromJsonT); +} + +@Freezed(genericArgumentFactories: true) +class PaginatedResponse with _$PaginatedResponse { + const factory PaginatedResponse({ + required List items, + required int page, + required int limit, + required int total, + required int totalPages, + }) = _PaginatedResponse; + + factory PaginatedResponse.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) => + _$PaginatedResponseFromJson(json, fromJsonT); +} + +@freezed +class PaginationParams with _$PaginationParams { + const factory PaginationParams({ + @Default(1) int page, + @Default(20) int limit, + String? search, + String? sortBy, + @Default('asc') String sortOrder, + }) = _PaginationParams; +} diff --git a/lib/shared/models/api_response.freezed.dart b/lib/shared/models/api_response.freezed.dart new file mode 100644 index 0000000..090b515 --- /dev/null +++ b/lib/shared/models/api_response.freezed.dart @@ -0,0 +1,756 @@ +// 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 'api_response.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +ApiResponse _$ApiResponseFromJson( + Map json, + T Function(Object?) fromJsonT, +) { + return _ApiResponse.fromJson(json, fromJsonT); +} + +/// @nodoc +mixin _$ApiResponse { + bool get success => throw _privateConstructorUsedError; + String get message => throw _privateConstructorUsedError; + T? get data => throw _privateConstructorUsedError; + Map? get meta => throw _privateConstructorUsedError; + + /// Serializes this ApiResponse to a JSON map. + Map toJson(Object? Function(T) toJsonT) => + throw _privateConstructorUsedError; + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ApiResponseCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ApiResponseCopyWith { + factory $ApiResponseCopyWith( + ApiResponse value, + $Res Function(ApiResponse) then, + ) = _$ApiResponseCopyWithImpl>; + @useResult + $Res call({ + bool success, + String message, + T? data, + Map? meta, + }); +} + +/// @nodoc +class _$ApiResponseCopyWithImpl> + implements $ApiResponseCopyWith { + _$ApiResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? success = null, + Object? message = null, + Object? data = freezed, + Object? meta = freezed, + }) { + return _then( + _value.copyWith( + success: null == success + ? _value.success + : success // ignore: cast_nullable_to_non_nullable + as bool, + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as T?, + meta: freezed == meta + ? _value.meta + : meta // ignore: cast_nullable_to_non_nullable + as Map?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ApiResponseImplCopyWith + implements $ApiResponseCopyWith { + factory _$$ApiResponseImplCopyWith( + _$ApiResponseImpl value, + $Res Function(_$ApiResponseImpl) then, + ) = __$$ApiResponseImplCopyWithImpl; + @override + @useResult + $Res call({ + bool success, + String message, + T? data, + Map? meta, + }); +} + +/// @nodoc +class __$$ApiResponseImplCopyWithImpl + extends _$ApiResponseCopyWithImpl> + implements _$$ApiResponseImplCopyWith { + __$$ApiResponseImplCopyWithImpl( + _$ApiResponseImpl _value, + $Res Function(_$ApiResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? success = null, + Object? message = null, + Object? data = freezed, + Object? meta = freezed, + }) { + return _then( + _$ApiResponseImpl( + success: null == success + ? _value.success + : success // ignore: cast_nullable_to_non_nullable + as bool, + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String, + data: freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as T?, + meta: freezed == meta + ? _value._meta + : meta // ignore: cast_nullable_to_non_nullable + as Map?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable(genericArgumentFactories: true) +class _$ApiResponseImpl implements _ApiResponse { + const _$ApiResponseImpl({ + required this.success, + required this.message, + this.data, + final Map? meta, + }) : _meta = meta; + + factory _$ApiResponseImpl.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) => _$$ApiResponseImplFromJson(json, fromJsonT); + + @override + final bool success; + @override + final String message; + @override + final T? data; + final Map? _meta; + @override + Map? get meta { + final value = _meta; + if (value == null) return null; + if (_meta is EqualUnmodifiableMapView) return _meta; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + String toString() { + return 'ApiResponse<$T>(success: $success, message: $message, data: $data, meta: $meta)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ApiResponseImpl && + (identical(other.success, success) || other.success == success) && + (identical(other.message, message) || other.message == message) && + const DeepCollectionEquality().equals(other.data, data) && + const DeepCollectionEquality().equals(other._meta, _meta)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + success, + message, + const DeepCollectionEquality().hash(data), + const DeepCollectionEquality().hash(_meta), + ); + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ApiResponseImplCopyWith> get copyWith => + __$$ApiResponseImplCopyWithImpl>( + this, + _$identity, + ); + + @override + Map toJson(Object? Function(T) toJsonT) { + return _$$ApiResponseImplToJson(this, toJsonT); + } +} + +abstract class _ApiResponse implements ApiResponse { + const factory _ApiResponse({ + required final bool success, + required final String message, + final T? data, + final Map? meta, + }) = _$ApiResponseImpl; + + factory _ApiResponse.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) = _$ApiResponseImpl.fromJson; + + @override + bool get success; + @override + String get message; + @override + T? get data; + @override + Map? get meta; + + /// Create a copy of ApiResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ApiResponseImplCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +PaginatedResponse _$PaginatedResponseFromJson( + Map json, + T Function(Object?) fromJsonT, +) { + return _PaginatedResponse.fromJson(json, fromJsonT); +} + +/// @nodoc +mixin _$PaginatedResponse { + List get items => throw _privateConstructorUsedError; + int get page => throw _privateConstructorUsedError; + int get limit => throw _privateConstructorUsedError; + int get total => throw _privateConstructorUsedError; + int get totalPages => throw _privateConstructorUsedError; + + /// Serializes this PaginatedResponse to a JSON map. + Map toJson(Object? Function(T) toJsonT) => + throw _privateConstructorUsedError; + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PaginatedResponseCopyWith> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PaginatedResponseCopyWith { + factory $PaginatedResponseCopyWith( + PaginatedResponse value, + $Res Function(PaginatedResponse) then, + ) = _$PaginatedResponseCopyWithImpl>; + @useResult + $Res call({List items, int page, int limit, int total, int totalPages}); +} + +/// @nodoc +class _$PaginatedResponseCopyWithImpl< + T, + $Res, + $Val extends PaginatedResponse +> + implements $PaginatedResponseCopyWith { + _$PaginatedResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + Object? page = null, + Object? limit = null, + Object? total = null, + Object? totalPages = null, + }) { + return _then( + _value.copyWith( + items: null == items + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as int, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + totalPages: null == totalPages + ? _value.totalPages + : totalPages // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PaginatedResponseImplCopyWith + implements $PaginatedResponseCopyWith { + factory _$$PaginatedResponseImplCopyWith( + _$PaginatedResponseImpl value, + $Res Function(_$PaginatedResponseImpl) then, + ) = __$$PaginatedResponseImplCopyWithImpl; + @override + @useResult + $Res call({List items, int page, int limit, int total, int totalPages}); +} + +/// @nodoc +class __$$PaginatedResponseImplCopyWithImpl + extends _$PaginatedResponseCopyWithImpl> + implements _$$PaginatedResponseImplCopyWith { + __$$PaginatedResponseImplCopyWithImpl( + _$PaginatedResponseImpl _value, + $Res Function(_$PaginatedResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? items = null, + Object? page = null, + Object? limit = null, + Object? total = null, + Object? totalPages = null, + }) { + return _then( + _$PaginatedResponseImpl( + items: null == items + ? _value._items + : items // ignore: cast_nullable_to_non_nullable + as List, + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as int, + total: null == total + ? _value.total + : total // ignore: cast_nullable_to_non_nullable + as int, + totalPages: null == totalPages + ? _value.totalPages + : totalPages // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc +@JsonSerializable(genericArgumentFactories: true) +class _$PaginatedResponseImpl implements _PaginatedResponse { + const _$PaginatedResponseImpl({ + required final List items, + required this.page, + required this.limit, + required this.total, + required this.totalPages, + }) : _items = items; + + factory _$PaginatedResponseImpl.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) => _$$PaginatedResponseImplFromJson(json, fromJsonT); + + final List _items; + @override + List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } + + @override + final int page; + @override + final int limit; + @override + final int total; + @override + final int totalPages; + + @override + String toString() { + return 'PaginatedResponse<$T>(items: $items, page: $page, limit: $limit, total: $total, totalPages: $totalPages)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PaginatedResponseImpl && + const DeepCollectionEquality().equals(other._items, _items) && + (identical(other.page, page) || other.page == page) && + (identical(other.limit, limit) || other.limit == limit) && + (identical(other.total, total) || other.total == total) && + (identical(other.totalPages, totalPages) || + other.totalPages == totalPages)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_items), + page, + limit, + total, + totalPages, + ); + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PaginatedResponseImplCopyWith> + get copyWith => + __$$PaginatedResponseImplCopyWithImpl>( + this, + _$identity, + ); + + @override + Map toJson(Object? Function(T) toJsonT) { + return _$$PaginatedResponseImplToJson(this, toJsonT); + } +} + +abstract class _PaginatedResponse implements PaginatedResponse { + const factory _PaginatedResponse({ + required final List items, + required final int page, + required final int limit, + required final int total, + required final int totalPages, + }) = _$PaginatedResponseImpl; + + factory _PaginatedResponse.fromJson( + Map json, + T Function(Object?) fromJsonT, + ) = _$PaginatedResponseImpl.fromJson; + + @override + List get items; + @override + int get page; + @override + int get limit; + @override + int get total; + @override + int get totalPages; + + /// Create a copy of PaginatedResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PaginatedResponseImplCopyWith> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$PaginationParams { + int get page => throw _privateConstructorUsedError; + int get limit => throw _privateConstructorUsedError; + String? get search => throw _privateConstructorUsedError; + String? get sortBy => throw _privateConstructorUsedError; + String get sortOrder => throw _privateConstructorUsedError; + + /// Create a copy of PaginationParams + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PaginationParamsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PaginationParamsCopyWith<$Res> { + factory $PaginationParamsCopyWith( + PaginationParams value, + $Res Function(PaginationParams) then, + ) = _$PaginationParamsCopyWithImpl<$Res, PaginationParams>; + @useResult + $Res call({ + int page, + int limit, + String? search, + String? sortBy, + String sortOrder, + }); +} + +/// @nodoc +class _$PaginationParamsCopyWithImpl<$Res, $Val extends PaginationParams> + implements $PaginationParamsCopyWith<$Res> { + _$PaginationParamsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PaginationParams + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? page = null, + Object? limit = null, + Object? search = freezed, + Object? sortBy = freezed, + Object? sortOrder = null, + }) { + return _then( + _value.copyWith( + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as int, + search: freezed == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String?, + sortBy: freezed == sortBy + ? _value.sortBy + : sortBy // ignore: cast_nullable_to_non_nullable + as String?, + sortOrder: null == sortOrder + ? _value.sortOrder + : sortOrder // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PaginationParamsImplCopyWith<$Res> + implements $PaginationParamsCopyWith<$Res> { + factory _$$PaginationParamsImplCopyWith( + _$PaginationParamsImpl value, + $Res Function(_$PaginationParamsImpl) then, + ) = __$$PaginationParamsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int page, + int limit, + String? search, + String? sortBy, + String sortOrder, + }); +} + +/// @nodoc +class __$$PaginationParamsImplCopyWithImpl<$Res> + extends _$PaginationParamsCopyWithImpl<$Res, _$PaginationParamsImpl> + implements _$$PaginationParamsImplCopyWith<$Res> { + __$$PaginationParamsImplCopyWithImpl( + _$PaginationParamsImpl _value, + $Res Function(_$PaginationParamsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PaginationParams + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? page = null, + Object? limit = null, + Object? search = freezed, + Object? sortBy = freezed, + Object? sortOrder = null, + }) { + return _then( + _$PaginationParamsImpl( + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as int, + search: freezed == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String?, + sortBy: freezed == sortBy + ? _value.sortBy + : sortBy // ignore: cast_nullable_to_non_nullable + as String?, + sortOrder: null == sortOrder + ? _value.sortOrder + : sortOrder // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc + +class _$PaginationParamsImpl implements _PaginationParams { + const _$PaginationParamsImpl({ + this.page = 1, + this.limit = 20, + this.search, + this.sortBy, + this.sortOrder = 'asc', + }); + + @override + @JsonKey() + final int page; + @override + @JsonKey() + final int limit; + @override + final String? search; + @override + final String? sortBy; + @override + @JsonKey() + final String sortOrder; + + @override + String toString() { + return 'PaginationParams(page: $page, limit: $limit, search: $search, sortBy: $sortBy, sortOrder: $sortOrder)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PaginationParamsImpl && + (identical(other.page, page) || other.page == page) && + (identical(other.limit, limit) || other.limit == limit) && + (identical(other.search, search) || other.search == search) && + (identical(other.sortBy, sortBy) || other.sortBy == sortBy) && + (identical(other.sortOrder, sortOrder) || + other.sortOrder == sortOrder)); + } + + @override + int get hashCode => + Object.hash(runtimeType, page, limit, search, sortBy, sortOrder); + + /// Create a copy of PaginationParams + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PaginationParamsImplCopyWith<_$PaginationParamsImpl> get copyWith => + __$$PaginationParamsImplCopyWithImpl<_$PaginationParamsImpl>( + this, + _$identity, + ); +} + +abstract class _PaginationParams implements PaginationParams { + const factory _PaginationParams({ + final int page, + final int limit, + final String? search, + final String? sortBy, + final String sortOrder, + }) = _$PaginationParamsImpl; + + @override + int get page; + @override + int get limit; + @override + String? get search; + @override + String? get sortBy; + @override + String get sortOrder; + + /// Create a copy of PaginationParams + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PaginationParamsImplCopyWith<_$PaginationParamsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/api_response.g.dart b/lib/shared/models/api_response.g.dart new file mode 100644 index 0000000..52a95a8 --- /dev/null +++ b/lib/shared/models/api_response.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_response.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$ApiResponseImpl _$$ApiResponseImplFromJson( + Map json, + T Function(Object? json) fromJsonT, +) => _$ApiResponseImpl( + success: json['success'] as bool, + message: json['message'] as String, + data: _$nullableGenericFromJson(json['data'], fromJsonT), + meta: json['meta'] as Map?, +); + +Map _$$ApiResponseImplToJson( + _$ApiResponseImpl instance, + Object? Function(T value) toJsonT, +) => { + 'success': instance.success, + 'message': instance.message, + 'data': _$nullableGenericToJson(instance.data, toJsonT), + 'meta': instance.meta, +}; + +T? _$nullableGenericFromJson( + Object? input, + T Function(Object? json) fromJson, +) => input == null ? null : fromJson(input); + +Object? _$nullableGenericToJson( + T? input, + Object? Function(T value) toJson, +) => input == null ? null : toJson(input); + +_$PaginatedResponseImpl _$$PaginatedResponseImplFromJson( + Map json, + T Function(Object? json) fromJsonT, +) => _$PaginatedResponseImpl( + items: (json['items'] as List).map(fromJsonT).toList(), + page: (json['page'] as num).toInt(), + limit: (json['limit'] as num).toInt(), + total: (json['total'] as num).toInt(), + totalPages: (json['totalPages'] as num).toInt(), +); + +Map _$$PaginatedResponseImplToJson( + _$PaginatedResponseImpl instance, + Object? Function(T value) toJsonT, +) => { + 'items': instance.items.map(toJsonT).toList(), + 'page': instance.page, + 'limit': instance.limit, + 'total': instance.total, + 'totalPages': instance.totalPages, +}; diff --git a/lib/shared/models/asset_model.dart b/lib/shared/models/asset_model.dart new file mode 100644 index 0000000..4a07f1d --- /dev/null +++ b/lib/shared/models/asset_model.dart @@ -0,0 +1,105 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'asset_model.freezed.dart'; +part 'asset_model.g.dart'; + +@freezed +class AssetCategoryModel with _$AssetCategoryModel { + const factory AssetCategoryModel({ + required String id, + required String name, + required String slug, + String? description, + @JsonKey(name: 'company_id') String? companyId, + DateTime? createdAt, + }) = _AssetCategoryModel; + + factory AssetCategoryModel.fromJson(Map json) => + _$AssetCategoryModelFromJson(json); +} + +@freezed +class AssetModel with _$AssetModel { + const factory AssetModel({ + required String id, + required String name, + @JsonKey(name: 'asset_code') required String assetCode, + @JsonKey(name: 'category_id') required String categoryId, + @JsonKey(name: 'category_name') String? categoryName, + String? brand, + String? model, + @JsonKey(name: 'serial_number') String? serialNumber, + @JsonKey(name: 'purchase_date') DateTime? purchaseDate, + @JsonKey(name: 'purchase_cost') double? purchaseCost, + String? vendor, + @JsonKey(name: 'warranty_start') DateTime? warrantyStart, + @JsonKey(name: 'warranty_end') DateTime? warrantyEnd, + @Default('available') String status, + @JsonKey(name: 'branch_id') String? branchId, + @JsonKey(name: 'branch_name') String? branchName, + @JsonKey(name: 'company_id') String? companyId, + @JsonKey(name: 'qr_code_url') String? qrCodeUrl, + DateTime? createdAt, + DateTime? updatedAt, + }) = _AssetModel; + + factory AssetModel.fromJson(Map json) => _$AssetModelFromJson(json); +} + +@freezed +class AssetAllocationModel with _$AssetAllocationModel { + const factory AssetAllocationModel({ + required String id, + @JsonKey(name: 'asset_id') required String assetId, + @JsonKey(name: 'asset_name') String? assetName, + @JsonKey(name: 'employee_id') required String employeeId, + @JsonKey(name: 'employee_name') String? employeeName, + @JsonKey(name: 'assigned_date') required DateTime assignedDate, + @JsonKey(name: 'returned_date') DateTime? returnedDate, + String? remarks, + @Default('active') String status, + DateTime? createdAt, + }) = _AssetAllocationModel; + + factory AssetAllocationModel.fromJson(Map json) => + _$AssetAllocationModelFromJson(json); +} + +@freezed +class AssetMaintenanceModel with _$AssetMaintenanceModel { + const factory AssetMaintenanceModel({ + required String id, + @JsonKey(name: 'asset_id') required String assetId, + @JsonKey(name: 'asset_name') String? assetName, + required String description, + @JsonKey(name: 'service_vendor') String? serviceVendor, + double? cost, + @Default('open') String status, + @JsonKey(name: 'requested_by') String? requestedBy, + @JsonKey(name: 'completed_date') DateTime? completedDate, + DateTime? createdAt, + DateTime? updatedAt, + }) = _AssetMaintenanceModel; + + factory AssetMaintenanceModel.fromJson(Map json) => + _$AssetMaintenanceModelFromJson(json); +} + +@freezed +class AssetDisposalModel with _$AssetDisposalModel { + const factory AssetDisposalModel({ + required String id, + @JsonKey(name: 'asset_id') required String assetId, + @JsonKey(name: 'asset_name') String? assetName, + @JsonKey(name: 'disposal_reason') required String disposalReason, + @JsonKey(name: 'disposal_date') DateTime? disposalDate, + @Default('pending') String status, + @JsonKey(name: 'requested_by') String? requestedBy, + @JsonKey(name: 'approved_by') String? approvedBy, + DateTime? createdAt, + DateTime? updatedAt, + }) = _AssetDisposalModel; + + factory AssetDisposalModel.fromJson(Map json) => + _$AssetDisposalModelFromJson(json); +} diff --git a/lib/shared/models/asset_model.freezed.dart b/lib/shared/models/asset_model.freezed.dart new file mode 100644 index 0000000..1cb8027 --- /dev/null +++ b/lib/shared/models/asset_model.freezed.dart @@ -0,0 +1,2092 @@ +// 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 'asset_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +AssetCategoryModel _$AssetCategoryModelFromJson(Map json) { + return _AssetCategoryModel.fromJson(json); +} + +/// @nodoc +mixin _$AssetCategoryModel { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get slug => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + @JsonKey(name: 'company_id') + String? get companyId => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + + /// Serializes this AssetCategoryModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AssetCategoryModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AssetCategoryModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AssetCategoryModelCopyWith<$Res> { + factory $AssetCategoryModelCopyWith( + AssetCategoryModel value, + $Res Function(AssetCategoryModel) then, + ) = _$AssetCategoryModelCopyWithImpl<$Res, AssetCategoryModel>; + @useResult + $Res call({ + String id, + String name, + String slug, + String? description, + @JsonKey(name: 'company_id') String? companyId, + DateTime? createdAt, + }); +} + +/// @nodoc +class _$AssetCategoryModelCopyWithImpl<$Res, $Val extends AssetCategoryModel> + implements $AssetCategoryModelCopyWith<$Res> { + _$AssetCategoryModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AssetCategoryModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? slug = null, + Object? description = freezed, + Object? companyId = freezed, + Object? createdAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + slug: null == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + companyId: freezed == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AssetCategoryModelImplCopyWith<$Res> + implements $AssetCategoryModelCopyWith<$Res> { + factory _$$AssetCategoryModelImplCopyWith( + _$AssetCategoryModelImpl value, + $Res Function(_$AssetCategoryModelImpl) then, + ) = __$$AssetCategoryModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + String slug, + String? description, + @JsonKey(name: 'company_id') String? companyId, + DateTime? createdAt, + }); +} + +/// @nodoc +class __$$AssetCategoryModelImplCopyWithImpl<$Res> + extends _$AssetCategoryModelCopyWithImpl<$Res, _$AssetCategoryModelImpl> + implements _$$AssetCategoryModelImplCopyWith<$Res> { + __$$AssetCategoryModelImplCopyWithImpl( + _$AssetCategoryModelImpl _value, + $Res Function(_$AssetCategoryModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AssetCategoryModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? slug = null, + Object? description = freezed, + Object? companyId = freezed, + Object? createdAt = freezed, + }) { + return _then( + _$AssetCategoryModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + slug: null == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + companyId: freezed == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AssetCategoryModelImpl implements _AssetCategoryModel { + const _$AssetCategoryModelImpl({ + required this.id, + required this.name, + required this.slug, + this.description, + @JsonKey(name: 'company_id') this.companyId, + this.createdAt, + }); + + factory _$AssetCategoryModelImpl.fromJson(Map json) => + _$$AssetCategoryModelImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String slug; + @override + final String? description; + @override + @JsonKey(name: 'company_id') + final String? companyId; + @override + final DateTime? createdAt; + + @override + String toString() { + return 'AssetCategoryModel(id: $id, name: $name, slug: $slug, description: $description, companyId: $companyId, createdAt: $createdAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AssetCategoryModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.slug, slug) || other.slug == slug) && + (identical(other.description, description) || + other.description == description) && + (identical(other.companyId, companyId) || + other.companyId == companyId) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + slug, + description, + companyId, + createdAt, + ); + + /// Create a copy of AssetCategoryModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AssetCategoryModelImplCopyWith<_$AssetCategoryModelImpl> get copyWith => + __$$AssetCategoryModelImplCopyWithImpl<_$AssetCategoryModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AssetCategoryModelImplToJson(this); + } +} + +abstract class _AssetCategoryModel implements AssetCategoryModel { + const factory _AssetCategoryModel({ + required final String id, + required final String name, + required final String slug, + final String? description, + @JsonKey(name: 'company_id') final String? companyId, + final DateTime? createdAt, + }) = _$AssetCategoryModelImpl; + + factory _AssetCategoryModel.fromJson(Map json) = + _$AssetCategoryModelImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String get slug; + @override + String? get description; + @override + @JsonKey(name: 'company_id') + String? get companyId; + @override + DateTime? get createdAt; + + /// Create a copy of AssetCategoryModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AssetCategoryModelImplCopyWith<_$AssetCategoryModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +AssetModel _$AssetModelFromJson(Map json) { + return _AssetModel.fromJson(json); +} + +/// @nodoc +mixin _$AssetModel { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + @JsonKey(name: 'asset_code') + String get assetCode => throw _privateConstructorUsedError; + @JsonKey(name: 'category_id') + String get categoryId => throw _privateConstructorUsedError; + @JsonKey(name: 'category_name') + String? get categoryName => throw _privateConstructorUsedError; + String? get brand => throw _privateConstructorUsedError; + String? get model => throw _privateConstructorUsedError; + @JsonKey(name: 'serial_number') + String? get serialNumber => throw _privateConstructorUsedError; + @JsonKey(name: 'purchase_date') + DateTime? get purchaseDate => throw _privateConstructorUsedError; + @JsonKey(name: 'purchase_cost') + double? get purchaseCost => throw _privateConstructorUsedError; + String? get vendor => throw _privateConstructorUsedError; + @JsonKey(name: 'warranty_start') + DateTime? get warrantyStart => throw _privateConstructorUsedError; + @JsonKey(name: 'warranty_end') + DateTime? get warrantyEnd => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + @JsonKey(name: 'branch_id') + String? get branchId => throw _privateConstructorUsedError; + @JsonKey(name: 'branch_name') + String? get branchName => throw _privateConstructorUsedError; + @JsonKey(name: 'company_id') + String? get companyId => throw _privateConstructorUsedError; + @JsonKey(name: 'qr_code_url') + String? get qrCodeUrl => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this AssetModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AssetModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AssetModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AssetModelCopyWith<$Res> { + factory $AssetModelCopyWith( + AssetModel value, + $Res Function(AssetModel) then, + ) = _$AssetModelCopyWithImpl<$Res, AssetModel>; + @useResult + $Res call({ + String id, + String name, + @JsonKey(name: 'asset_code') String assetCode, + @JsonKey(name: 'category_id') String categoryId, + @JsonKey(name: 'category_name') String? categoryName, + String? brand, + String? model, + @JsonKey(name: 'serial_number') String? serialNumber, + @JsonKey(name: 'purchase_date') DateTime? purchaseDate, + @JsonKey(name: 'purchase_cost') double? purchaseCost, + String? vendor, + @JsonKey(name: 'warranty_start') DateTime? warrantyStart, + @JsonKey(name: 'warranty_end') DateTime? warrantyEnd, + String status, + @JsonKey(name: 'branch_id') String? branchId, + @JsonKey(name: 'branch_name') String? branchName, + @JsonKey(name: 'company_id') String? companyId, + @JsonKey(name: 'qr_code_url') String? qrCodeUrl, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$AssetModelCopyWithImpl<$Res, $Val extends AssetModel> + implements $AssetModelCopyWith<$Res> { + _$AssetModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AssetModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? assetCode = null, + Object? categoryId = null, + Object? categoryName = freezed, + Object? brand = freezed, + Object? model = freezed, + Object? serialNumber = freezed, + Object? purchaseDate = freezed, + Object? purchaseCost = freezed, + Object? vendor = freezed, + Object? warrantyStart = freezed, + Object? warrantyEnd = freezed, + Object? status = null, + Object? branchId = freezed, + Object? branchName = freezed, + Object? companyId = freezed, + Object? qrCodeUrl = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + assetCode: null == assetCode + ? _value.assetCode + : assetCode // ignore: cast_nullable_to_non_nullable + as String, + categoryId: null == categoryId + ? _value.categoryId + : categoryId // ignore: cast_nullable_to_non_nullable + as String, + categoryName: freezed == categoryName + ? _value.categoryName + : categoryName // ignore: cast_nullable_to_non_nullable + as String?, + brand: freezed == brand + ? _value.brand + : brand // ignore: cast_nullable_to_non_nullable + as String?, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String?, + serialNumber: freezed == serialNumber + ? _value.serialNumber + : serialNumber // ignore: cast_nullable_to_non_nullable + as String?, + purchaseDate: freezed == purchaseDate + ? _value.purchaseDate + : purchaseDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + purchaseCost: freezed == purchaseCost + ? _value.purchaseCost + : purchaseCost // ignore: cast_nullable_to_non_nullable + as double?, + vendor: freezed == vendor + ? _value.vendor + : vendor // ignore: cast_nullable_to_non_nullable + as String?, + warrantyStart: freezed == warrantyStart + ? _value.warrantyStart + : warrantyStart // ignore: cast_nullable_to_non_nullable + as DateTime?, + warrantyEnd: freezed == warrantyEnd + ? _value.warrantyEnd + : warrantyEnd // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + branchId: freezed == branchId + ? _value.branchId + : branchId // ignore: cast_nullable_to_non_nullable + as String?, + branchName: freezed == branchName + ? _value.branchName + : branchName // ignore: cast_nullable_to_non_nullable + as String?, + companyId: freezed == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as String?, + qrCodeUrl: freezed == qrCodeUrl + ? _value.qrCodeUrl + : qrCodeUrl // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AssetModelImplCopyWith<$Res> + implements $AssetModelCopyWith<$Res> { + factory _$$AssetModelImplCopyWith( + _$AssetModelImpl value, + $Res Function(_$AssetModelImpl) then, + ) = __$$AssetModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + @JsonKey(name: 'asset_code') String assetCode, + @JsonKey(name: 'category_id') String categoryId, + @JsonKey(name: 'category_name') String? categoryName, + String? brand, + String? model, + @JsonKey(name: 'serial_number') String? serialNumber, + @JsonKey(name: 'purchase_date') DateTime? purchaseDate, + @JsonKey(name: 'purchase_cost') double? purchaseCost, + String? vendor, + @JsonKey(name: 'warranty_start') DateTime? warrantyStart, + @JsonKey(name: 'warranty_end') DateTime? warrantyEnd, + String status, + @JsonKey(name: 'branch_id') String? branchId, + @JsonKey(name: 'branch_name') String? branchName, + @JsonKey(name: 'company_id') String? companyId, + @JsonKey(name: 'qr_code_url') String? qrCodeUrl, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class __$$AssetModelImplCopyWithImpl<$Res> + extends _$AssetModelCopyWithImpl<$Res, _$AssetModelImpl> + implements _$$AssetModelImplCopyWith<$Res> { + __$$AssetModelImplCopyWithImpl( + _$AssetModelImpl _value, + $Res Function(_$AssetModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AssetModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? assetCode = null, + Object? categoryId = null, + Object? categoryName = freezed, + Object? brand = freezed, + Object? model = freezed, + Object? serialNumber = freezed, + Object? purchaseDate = freezed, + Object? purchaseCost = freezed, + Object? vendor = freezed, + Object? warrantyStart = freezed, + Object? warrantyEnd = freezed, + Object? status = null, + Object? branchId = freezed, + Object? branchName = freezed, + Object? companyId = freezed, + Object? qrCodeUrl = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$AssetModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + assetCode: null == assetCode + ? _value.assetCode + : assetCode // ignore: cast_nullable_to_non_nullable + as String, + categoryId: null == categoryId + ? _value.categoryId + : categoryId // ignore: cast_nullable_to_non_nullable + as String, + categoryName: freezed == categoryName + ? _value.categoryName + : categoryName // ignore: cast_nullable_to_non_nullable + as String?, + brand: freezed == brand + ? _value.brand + : brand // ignore: cast_nullable_to_non_nullable + as String?, + model: freezed == model + ? _value.model + : model // ignore: cast_nullable_to_non_nullable + as String?, + serialNumber: freezed == serialNumber + ? _value.serialNumber + : serialNumber // ignore: cast_nullable_to_non_nullable + as String?, + purchaseDate: freezed == purchaseDate + ? _value.purchaseDate + : purchaseDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + purchaseCost: freezed == purchaseCost + ? _value.purchaseCost + : purchaseCost // ignore: cast_nullable_to_non_nullable + as double?, + vendor: freezed == vendor + ? _value.vendor + : vendor // ignore: cast_nullable_to_non_nullable + as String?, + warrantyStart: freezed == warrantyStart + ? _value.warrantyStart + : warrantyStart // ignore: cast_nullable_to_non_nullable + as DateTime?, + warrantyEnd: freezed == warrantyEnd + ? _value.warrantyEnd + : warrantyEnd // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + branchId: freezed == branchId + ? _value.branchId + : branchId // ignore: cast_nullable_to_non_nullable + as String?, + branchName: freezed == branchName + ? _value.branchName + : branchName // ignore: cast_nullable_to_non_nullable + as String?, + companyId: freezed == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as String?, + qrCodeUrl: freezed == qrCodeUrl + ? _value.qrCodeUrl + : qrCodeUrl // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AssetModelImpl implements _AssetModel { + const _$AssetModelImpl({ + required this.id, + required this.name, + @JsonKey(name: 'asset_code') required this.assetCode, + @JsonKey(name: 'category_id') required this.categoryId, + @JsonKey(name: 'category_name') this.categoryName, + this.brand, + this.model, + @JsonKey(name: 'serial_number') this.serialNumber, + @JsonKey(name: 'purchase_date') this.purchaseDate, + @JsonKey(name: 'purchase_cost') this.purchaseCost, + this.vendor, + @JsonKey(name: 'warranty_start') this.warrantyStart, + @JsonKey(name: 'warranty_end') this.warrantyEnd, + this.status = 'available', + @JsonKey(name: 'branch_id') this.branchId, + @JsonKey(name: 'branch_name') this.branchName, + @JsonKey(name: 'company_id') this.companyId, + @JsonKey(name: 'qr_code_url') this.qrCodeUrl, + this.createdAt, + this.updatedAt, + }); + + factory _$AssetModelImpl.fromJson(Map json) => + _$$AssetModelImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + @JsonKey(name: 'asset_code') + final String assetCode; + @override + @JsonKey(name: 'category_id') + final String categoryId; + @override + @JsonKey(name: 'category_name') + final String? categoryName; + @override + final String? brand; + @override + final String? model; + @override + @JsonKey(name: 'serial_number') + final String? serialNumber; + @override + @JsonKey(name: 'purchase_date') + final DateTime? purchaseDate; + @override + @JsonKey(name: 'purchase_cost') + final double? purchaseCost; + @override + final String? vendor; + @override + @JsonKey(name: 'warranty_start') + final DateTime? warrantyStart; + @override + @JsonKey(name: 'warranty_end') + final DateTime? warrantyEnd; + @override + @JsonKey() + final String status; + @override + @JsonKey(name: 'branch_id') + final String? branchId; + @override + @JsonKey(name: 'branch_name') + final String? branchName; + @override + @JsonKey(name: 'company_id') + final String? companyId; + @override + @JsonKey(name: 'qr_code_url') + final String? qrCodeUrl; + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'AssetModel(id: $id, name: $name, assetCode: $assetCode, categoryId: $categoryId, categoryName: $categoryName, brand: $brand, model: $model, serialNumber: $serialNumber, purchaseDate: $purchaseDate, purchaseCost: $purchaseCost, vendor: $vendor, warrantyStart: $warrantyStart, warrantyEnd: $warrantyEnd, status: $status, branchId: $branchId, branchName: $branchName, companyId: $companyId, qrCodeUrl: $qrCodeUrl, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AssetModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.assetCode, assetCode) || + other.assetCode == assetCode) && + (identical(other.categoryId, categoryId) || + other.categoryId == categoryId) && + (identical(other.categoryName, categoryName) || + other.categoryName == categoryName) && + (identical(other.brand, brand) || other.brand == brand) && + (identical(other.model, model) || other.model == model) && + (identical(other.serialNumber, serialNumber) || + other.serialNumber == serialNumber) && + (identical(other.purchaseDate, purchaseDate) || + other.purchaseDate == purchaseDate) && + (identical(other.purchaseCost, purchaseCost) || + other.purchaseCost == purchaseCost) && + (identical(other.vendor, vendor) || other.vendor == vendor) && + (identical(other.warrantyStart, warrantyStart) || + other.warrantyStart == warrantyStart) && + (identical(other.warrantyEnd, warrantyEnd) || + other.warrantyEnd == warrantyEnd) && + (identical(other.status, status) || other.status == status) && + (identical(other.branchId, branchId) || + other.branchId == branchId) && + (identical(other.branchName, branchName) || + other.branchName == branchName) && + (identical(other.companyId, companyId) || + other.companyId == companyId) && + (identical(other.qrCodeUrl, qrCodeUrl) || + other.qrCodeUrl == qrCodeUrl) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hashAll([ + runtimeType, + id, + name, + assetCode, + categoryId, + categoryName, + brand, + model, + serialNumber, + purchaseDate, + purchaseCost, + vendor, + warrantyStart, + warrantyEnd, + status, + branchId, + branchName, + companyId, + qrCodeUrl, + createdAt, + updatedAt, + ]); + + /// Create a copy of AssetModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AssetModelImplCopyWith<_$AssetModelImpl> get copyWith => + __$$AssetModelImplCopyWithImpl<_$AssetModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AssetModelImplToJson(this); + } +} + +abstract class _AssetModel implements AssetModel { + const factory _AssetModel({ + required final String id, + required final String name, + @JsonKey(name: 'asset_code') required final String assetCode, + @JsonKey(name: 'category_id') required final String categoryId, + @JsonKey(name: 'category_name') final String? categoryName, + final String? brand, + final String? model, + @JsonKey(name: 'serial_number') final String? serialNumber, + @JsonKey(name: 'purchase_date') final DateTime? purchaseDate, + @JsonKey(name: 'purchase_cost') final double? purchaseCost, + final String? vendor, + @JsonKey(name: 'warranty_start') final DateTime? warrantyStart, + @JsonKey(name: 'warranty_end') final DateTime? warrantyEnd, + final String status, + @JsonKey(name: 'branch_id') final String? branchId, + @JsonKey(name: 'branch_name') final String? branchName, + @JsonKey(name: 'company_id') final String? companyId, + @JsonKey(name: 'qr_code_url') final String? qrCodeUrl, + final DateTime? createdAt, + final DateTime? updatedAt, + }) = _$AssetModelImpl; + + factory _AssetModel.fromJson(Map json) = + _$AssetModelImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + @JsonKey(name: 'asset_code') + String get assetCode; + @override + @JsonKey(name: 'category_id') + String get categoryId; + @override + @JsonKey(name: 'category_name') + String? get categoryName; + @override + String? get brand; + @override + String? get model; + @override + @JsonKey(name: 'serial_number') + String? get serialNumber; + @override + @JsonKey(name: 'purchase_date') + DateTime? get purchaseDate; + @override + @JsonKey(name: 'purchase_cost') + double? get purchaseCost; + @override + String? get vendor; + @override + @JsonKey(name: 'warranty_start') + DateTime? get warrantyStart; + @override + @JsonKey(name: 'warranty_end') + DateTime? get warrantyEnd; + @override + String get status; + @override + @JsonKey(name: 'branch_id') + String? get branchId; + @override + @JsonKey(name: 'branch_name') + String? get branchName; + @override + @JsonKey(name: 'company_id') + String? get companyId; + @override + @JsonKey(name: 'qr_code_url') + String? get qrCodeUrl; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of AssetModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AssetModelImplCopyWith<_$AssetModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +AssetAllocationModel _$AssetAllocationModelFromJson(Map json) { + return _AssetAllocationModel.fromJson(json); +} + +/// @nodoc +mixin _$AssetAllocationModel { + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'asset_id') + String get assetId => throw _privateConstructorUsedError; + @JsonKey(name: 'asset_name') + String? get assetName => throw _privateConstructorUsedError; + @JsonKey(name: 'employee_id') + String get employeeId => throw _privateConstructorUsedError; + @JsonKey(name: 'employee_name') + String? get employeeName => throw _privateConstructorUsedError; + @JsonKey(name: 'assigned_date') + DateTime get assignedDate => throw _privateConstructorUsedError; + @JsonKey(name: 'returned_date') + DateTime? get returnedDate => throw _privateConstructorUsedError; + String? get remarks => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + + /// Serializes this AssetAllocationModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AssetAllocationModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AssetAllocationModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AssetAllocationModelCopyWith<$Res> { + factory $AssetAllocationModelCopyWith( + AssetAllocationModel value, + $Res Function(AssetAllocationModel) then, + ) = _$AssetAllocationModelCopyWithImpl<$Res, AssetAllocationModel>; + @useResult + $Res call({ + String id, + @JsonKey(name: 'asset_id') String assetId, + @JsonKey(name: 'asset_name') String? assetName, + @JsonKey(name: 'employee_id') String employeeId, + @JsonKey(name: 'employee_name') String? employeeName, + @JsonKey(name: 'assigned_date') DateTime assignedDate, + @JsonKey(name: 'returned_date') DateTime? returnedDate, + String? remarks, + String status, + DateTime? createdAt, + }); +} + +/// @nodoc +class _$AssetAllocationModelCopyWithImpl< + $Res, + $Val extends AssetAllocationModel +> + implements $AssetAllocationModelCopyWith<$Res> { + _$AssetAllocationModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AssetAllocationModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? assetId = null, + Object? assetName = freezed, + Object? employeeId = null, + Object? employeeName = freezed, + Object? assignedDate = null, + Object? returnedDate = freezed, + Object? remarks = freezed, + Object? status = null, + Object? createdAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + assetId: null == assetId + ? _value.assetId + : assetId // ignore: cast_nullable_to_non_nullable + as String, + assetName: freezed == assetName + ? _value.assetName + : assetName // ignore: cast_nullable_to_non_nullable + as String?, + employeeId: null == employeeId + ? _value.employeeId + : employeeId // ignore: cast_nullable_to_non_nullable + as String, + employeeName: freezed == employeeName + ? _value.employeeName + : employeeName // ignore: cast_nullable_to_non_nullable + as String?, + assignedDate: null == assignedDate + ? _value.assignedDate + : assignedDate // ignore: cast_nullable_to_non_nullable + as DateTime, + returnedDate: freezed == returnedDate + ? _value.returnedDate + : returnedDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AssetAllocationModelImplCopyWith<$Res> + implements $AssetAllocationModelCopyWith<$Res> { + factory _$$AssetAllocationModelImplCopyWith( + _$AssetAllocationModelImpl value, + $Res Function(_$AssetAllocationModelImpl) then, + ) = __$$AssetAllocationModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + @JsonKey(name: 'asset_id') String assetId, + @JsonKey(name: 'asset_name') String? assetName, + @JsonKey(name: 'employee_id') String employeeId, + @JsonKey(name: 'employee_name') String? employeeName, + @JsonKey(name: 'assigned_date') DateTime assignedDate, + @JsonKey(name: 'returned_date') DateTime? returnedDate, + String? remarks, + String status, + DateTime? createdAt, + }); +} + +/// @nodoc +class __$$AssetAllocationModelImplCopyWithImpl<$Res> + extends _$AssetAllocationModelCopyWithImpl<$Res, _$AssetAllocationModelImpl> + implements _$$AssetAllocationModelImplCopyWith<$Res> { + __$$AssetAllocationModelImplCopyWithImpl( + _$AssetAllocationModelImpl _value, + $Res Function(_$AssetAllocationModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AssetAllocationModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? assetId = null, + Object? assetName = freezed, + Object? employeeId = null, + Object? employeeName = freezed, + Object? assignedDate = null, + Object? returnedDate = freezed, + Object? remarks = freezed, + Object? status = null, + Object? createdAt = freezed, + }) { + return _then( + _$AssetAllocationModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + assetId: null == assetId + ? _value.assetId + : assetId // ignore: cast_nullable_to_non_nullable + as String, + assetName: freezed == assetName + ? _value.assetName + : assetName // ignore: cast_nullable_to_non_nullable + as String?, + employeeId: null == employeeId + ? _value.employeeId + : employeeId // ignore: cast_nullable_to_non_nullable + as String, + employeeName: freezed == employeeName + ? _value.employeeName + : employeeName // ignore: cast_nullable_to_non_nullable + as String?, + assignedDate: null == assignedDate + ? _value.assignedDate + : assignedDate // ignore: cast_nullable_to_non_nullable + as DateTime, + returnedDate: freezed == returnedDate + ? _value.returnedDate + : returnedDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AssetAllocationModelImpl implements _AssetAllocationModel { + const _$AssetAllocationModelImpl({ + required this.id, + @JsonKey(name: 'asset_id') required this.assetId, + @JsonKey(name: 'asset_name') this.assetName, + @JsonKey(name: 'employee_id') required this.employeeId, + @JsonKey(name: 'employee_name') this.employeeName, + @JsonKey(name: 'assigned_date') required this.assignedDate, + @JsonKey(name: 'returned_date') this.returnedDate, + this.remarks, + this.status = 'active', + this.createdAt, + }); + + factory _$AssetAllocationModelImpl.fromJson(Map json) => + _$$AssetAllocationModelImplFromJson(json); + + @override + final String id; + @override + @JsonKey(name: 'asset_id') + final String assetId; + @override + @JsonKey(name: 'asset_name') + final String? assetName; + @override + @JsonKey(name: 'employee_id') + final String employeeId; + @override + @JsonKey(name: 'employee_name') + final String? employeeName; + @override + @JsonKey(name: 'assigned_date') + final DateTime assignedDate; + @override + @JsonKey(name: 'returned_date') + final DateTime? returnedDate; + @override + final String? remarks; + @override + @JsonKey() + final String status; + @override + final DateTime? createdAt; + + @override + String toString() { + return 'AssetAllocationModel(id: $id, assetId: $assetId, assetName: $assetName, employeeId: $employeeId, employeeName: $employeeName, assignedDate: $assignedDate, returnedDate: $returnedDate, remarks: $remarks, status: $status, createdAt: $createdAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AssetAllocationModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.assetId, assetId) || other.assetId == assetId) && + (identical(other.assetName, assetName) || + other.assetName == assetName) && + (identical(other.employeeId, employeeId) || + other.employeeId == employeeId) && + (identical(other.employeeName, employeeName) || + other.employeeName == employeeName) && + (identical(other.assignedDate, assignedDate) || + other.assignedDate == assignedDate) && + (identical(other.returnedDate, returnedDate) || + other.returnedDate == returnedDate) && + (identical(other.remarks, remarks) || other.remarks == remarks) && + (identical(other.status, status) || other.status == status) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + assetId, + assetName, + employeeId, + employeeName, + assignedDate, + returnedDate, + remarks, + status, + createdAt, + ); + + /// Create a copy of AssetAllocationModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AssetAllocationModelImplCopyWith<_$AssetAllocationModelImpl> + get copyWith => + __$$AssetAllocationModelImplCopyWithImpl<_$AssetAllocationModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AssetAllocationModelImplToJson(this); + } +} + +abstract class _AssetAllocationModel implements AssetAllocationModel { + const factory _AssetAllocationModel({ + required final String id, + @JsonKey(name: 'asset_id') required final String assetId, + @JsonKey(name: 'asset_name') final String? assetName, + @JsonKey(name: 'employee_id') required final String employeeId, + @JsonKey(name: 'employee_name') final String? employeeName, + @JsonKey(name: 'assigned_date') required final DateTime assignedDate, + @JsonKey(name: 'returned_date') final DateTime? returnedDate, + final String? remarks, + final String status, + final DateTime? createdAt, + }) = _$AssetAllocationModelImpl; + + factory _AssetAllocationModel.fromJson(Map json) = + _$AssetAllocationModelImpl.fromJson; + + @override + String get id; + @override + @JsonKey(name: 'asset_id') + String get assetId; + @override + @JsonKey(name: 'asset_name') + String? get assetName; + @override + @JsonKey(name: 'employee_id') + String get employeeId; + @override + @JsonKey(name: 'employee_name') + String? get employeeName; + @override + @JsonKey(name: 'assigned_date') + DateTime get assignedDate; + @override + @JsonKey(name: 'returned_date') + DateTime? get returnedDate; + @override + String? get remarks; + @override + String get status; + @override + DateTime? get createdAt; + + /// Create a copy of AssetAllocationModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AssetAllocationModelImplCopyWith<_$AssetAllocationModelImpl> + get copyWith => throw _privateConstructorUsedError; +} + +AssetMaintenanceModel _$AssetMaintenanceModelFromJson( + Map json, +) { + return _AssetMaintenanceModel.fromJson(json); +} + +/// @nodoc +mixin _$AssetMaintenanceModel { + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'asset_id') + String get assetId => throw _privateConstructorUsedError; + @JsonKey(name: 'asset_name') + String? get assetName => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + @JsonKey(name: 'service_vendor') + String? get serviceVendor => throw _privateConstructorUsedError; + double? get cost => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + @JsonKey(name: 'requested_by') + String? get requestedBy => throw _privateConstructorUsedError; + @JsonKey(name: 'completed_date') + DateTime? get completedDate => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this AssetMaintenanceModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AssetMaintenanceModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AssetMaintenanceModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AssetMaintenanceModelCopyWith<$Res> { + factory $AssetMaintenanceModelCopyWith( + AssetMaintenanceModel value, + $Res Function(AssetMaintenanceModel) then, + ) = _$AssetMaintenanceModelCopyWithImpl<$Res, AssetMaintenanceModel>; + @useResult + $Res call({ + String id, + @JsonKey(name: 'asset_id') String assetId, + @JsonKey(name: 'asset_name') String? assetName, + String description, + @JsonKey(name: 'service_vendor') String? serviceVendor, + double? cost, + String status, + @JsonKey(name: 'requested_by') String? requestedBy, + @JsonKey(name: 'completed_date') DateTime? completedDate, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$AssetMaintenanceModelCopyWithImpl< + $Res, + $Val extends AssetMaintenanceModel +> + implements $AssetMaintenanceModelCopyWith<$Res> { + _$AssetMaintenanceModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AssetMaintenanceModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? assetId = null, + Object? assetName = freezed, + Object? description = null, + Object? serviceVendor = freezed, + Object? cost = freezed, + Object? status = null, + Object? requestedBy = freezed, + Object? completedDate = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + assetId: null == assetId + ? _value.assetId + : assetId // ignore: cast_nullable_to_non_nullable + as String, + assetName: freezed == assetName + ? _value.assetName + : assetName // ignore: cast_nullable_to_non_nullable + as String?, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + serviceVendor: freezed == serviceVendor + ? _value.serviceVendor + : serviceVendor // ignore: cast_nullable_to_non_nullable + as String?, + cost: freezed == cost + ? _value.cost + : cost // ignore: cast_nullable_to_non_nullable + as double?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + requestedBy: freezed == requestedBy + ? _value.requestedBy + : requestedBy // ignore: cast_nullable_to_non_nullable + as String?, + completedDate: freezed == completedDate + ? _value.completedDate + : completedDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AssetMaintenanceModelImplCopyWith<$Res> + implements $AssetMaintenanceModelCopyWith<$Res> { + factory _$$AssetMaintenanceModelImplCopyWith( + _$AssetMaintenanceModelImpl value, + $Res Function(_$AssetMaintenanceModelImpl) then, + ) = __$$AssetMaintenanceModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + @JsonKey(name: 'asset_id') String assetId, + @JsonKey(name: 'asset_name') String? assetName, + String description, + @JsonKey(name: 'service_vendor') String? serviceVendor, + double? cost, + String status, + @JsonKey(name: 'requested_by') String? requestedBy, + @JsonKey(name: 'completed_date') DateTime? completedDate, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class __$$AssetMaintenanceModelImplCopyWithImpl<$Res> + extends + _$AssetMaintenanceModelCopyWithImpl<$Res, _$AssetMaintenanceModelImpl> + implements _$$AssetMaintenanceModelImplCopyWith<$Res> { + __$$AssetMaintenanceModelImplCopyWithImpl( + _$AssetMaintenanceModelImpl _value, + $Res Function(_$AssetMaintenanceModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AssetMaintenanceModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? assetId = null, + Object? assetName = freezed, + Object? description = null, + Object? serviceVendor = freezed, + Object? cost = freezed, + Object? status = null, + Object? requestedBy = freezed, + Object? completedDate = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$AssetMaintenanceModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + assetId: null == assetId + ? _value.assetId + : assetId // ignore: cast_nullable_to_non_nullable + as String, + assetName: freezed == assetName + ? _value.assetName + : assetName // ignore: cast_nullable_to_non_nullable + as String?, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + serviceVendor: freezed == serviceVendor + ? _value.serviceVendor + : serviceVendor // ignore: cast_nullable_to_non_nullable + as String?, + cost: freezed == cost + ? _value.cost + : cost // ignore: cast_nullable_to_non_nullable + as double?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + requestedBy: freezed == requestedBy + ? _value.requestedBy + : requestedBy // ignore: cast_nullable_to_non_nullable + as String?, + completedDate: freezed == completedDate + ? _value.completedDate + : completedDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AssetMaintenanceModelImpl implements _AssetMaintenanceModel { + const _$AssetMaintenanceModelImpl({ + required this.id, + @JsonKey(name: 'asset_id') required this.assetId, + @JsonKey(name: 'asset_name') this.assetName, + required this.description, + @JsonKey(name: 'service_vendor') this.serviceVendor, + this.cost, + this.status = 'open', + @JsonKey(name: 'requested_by') this.requestedBy, + @JsonKey(name: 'completed_date') this.completedDate, + this.createdAt, + this.updatedAt, + }); + + factory _$AssetMaintenanceModelImpl.fromJson(Map json) => + _$$AssetMaintenanceModelImplFromJson(json); + + @override + final String id; + @override + @JsonKey(name: 'asset_id') + final String assetId; + @override + @JsonKey(name: 'asset_name') + final String? assetName; + @override + final String description; + @override + @JsonKey(name: 'service_vendor') + final String? serviceVendor; + @override + final double? cost; + @override + @JsonKey() + final String status; + @override + @JsonKey(name: 'requested_by') + final String? requestedBy; + @override + @JsonKey(name: 'completed_date') + final DateTime? completedDate; + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'AssetMaintenanceModel(id: $id, assetId: $assetId, assetName: $assetName, description: $description, serviceVendor: $serviceVendor, cost: $cost, status: $status, requestedBy: $requestedBy, completedDate: $completedDate, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AssetMaintenanceModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.assetId, assetId) || other.assetId == assetId) && + (identical(other.assetName, assetName) || + other.assetName == assetName) && + (identical(other.description, description) || + other.description == description) && + (identical(other.serviceVendor, serviceVendor) || + other.serviceVendor == serviceVendor) && + (identical(other.cost, cost) || other.cost == cost) && + (identical(other.status, status) || other.status == status) && + (identical(other.requestedBy, requestedBy) || + other.requestedBy == requestedBy) && + (identical(other.completedDate, completedDate) || + other.completedDate == completedDate) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + assetId, + assetName, + description, + serviceVendor, + cost, + status, + requestedBy, + completedDate, + createdAt, + updatedAt, + ); + + /// Create a copy of AssetMaintenanceModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AssetMaintenanceModelImplCopyWith<_$AssetMaintenanceModelImpl> + get copyWith => + __$$AssetMaintenanceModelImplCopyWithImpl<_$AssetMaintenanceModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AssetMaintenanceModelImplToJson(this); + } +} + +abstract class _AssetMaintenanceModel implements AssetMaintenanceModel { + const factory _AssetMaintenanceModel({ + required final String id, + @JsonKey(name: 'asset_id') required final String assetId, + @JsonKey(name: 'asset_name') final String? assetName, + required final String description, + @JsonKey(name: 'service_vendor') final String? serviceVendor, + final double? cost, + final String status, + @JsonKey(name: 'requested_by') final String? requestedBy, + @JsonKey(name: 'completed_date') final DateTime? completedDate, + final DateTime? createdAt, + final DateTime? updatedAt, + }) = _$AssetMaintenanceModelImpl; + + factory _AssetMaintenanceModel.fromJson(Map json) = + _$AssetMaintenanceModelImpl.fromJson; + + @override + String get id; + @override + @JsonKey(name: 'asset_id') + String get assetId; + @override + @JsonKey(name: 'asset_name') + String? get assetName; + @override + String get description; + @override + @JsonKey(name: 'service_vendor') + String? get serviceVendor; + @override + double? get cost; + @override + String get status; + @override + @JsonKey(name: 'requested_by') + String? get requestedBy; + @override + @JsonKey(name: 'completed_date') + DateTime? get completedDate; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of AssetMaintenanceModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AssetMaintenanceModelImplCopyWith<_$AssetMaintenanceModelImpl> + get copyWith => throw _privateConstructorUsedError; +} + +AssetDisposalModel _$AssetDisposalModelFromJson(Map json) { + return _AssetDisposalModel.fromJson(json); +} + +/// @nodoc +mixin _$AssetDisposalModel { + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'asset_id') + String get assetId => throw _privateConstructorUsedError; + @JsonKey(name: 'asset_name') + String? get assetName => throw _privateConstructorUsedError; + @JsonKey(name: 'disposal_reason') + String get disposalReason => throw _privateConstructorUsedError; + @JsonKey(name: 'disposal_date') + DateTime? get disposalDate => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + @JsonKey(name: 'requested_by') + String? get requestedBy => throw _privateConstructorUsedError; + @JsonKey(name: 'approved_by') + String? get approvedBy => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this AssetDisposalModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AssetDisposalModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AssetDisposalModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AssetDisposalModelCopyWith<$Res> { + factory $AssetDisposalModelCopyWith( + AssetDisposalModel value, + $Res Function(AssetDisposalModel) then, + ) = _$AssetDisposalModelCopyWithImpl<$Res, AssetDisposalModel>; + @useResult + $Res call({ + String id, + @JsonKey(name: 'asset_id') String assetId, + @JsonKey(name: 'asset_name') String? assetName, + @JsonKey(name: 'disposal_reason') String disposalReason, + @JsonKey(name: 'disposal_date') DateTime? disposalDate, + String status, + @JsonKey(name: 'requested_by') String? requestedBy, + @JsonKey(name: 'approved_by') String? approvedBy, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$AssetDisposalModelCopyWithImpl<$Res, $Val extends AssetDisposalModel> + implements $AssetDisposalModelCopyWith<$Res> { + _$AssetDisposalModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AssetDisposalModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? assetId = null, + Object? assetName = freezed, + Object? disposalReason = null, + Object? disposalDate = freezed, + Object? status = null, + Object? requestedBy = freezed, + Object? approvedBy = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + assetId: null == assetId + ? _value.assetId + : assetId // ignore: cast_nullable_to_non_nullable + as String, + assetName: freezed == assetName + ? _value.assetName + : assetName // ignore: cast_nullable_to_non_nullable + as String?, + disposalReason: null == disposalReason + ? _value.disposalReason + : disposalReason // ignore: cast_nullable_to_non_nullable + as String, + disposalDate: freezed == disposalDate + ? _value.disposalDate + : disposalDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + requestedBy: freezed == requestedBy + ? _value.requestedBy + : requestedBy // ignore: cast_nullable_to_non_nullable + as String?, + approvedBy: freezed == approvedBy + ? _value.approvedBy + : approvedBy // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AssetDisposalModelImplCopyWith<$Res> + implements $AssetDisposalModelCopyWith<$Res> { + factory _$$AssetDisposalModelImplCopyWith( + _$AssetDisposalModelImpl value, + $Res Function(_$AssetDisposalModelImpl) then, + ) = __$$AssetDisposalModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + @JsonKey(name: 'asset_id') String assetId, + @JsonKey(name: 'asset_name') String? assetName, + @JsonKey(name: 'disposal_reason') String disposalReason, + @JsonKey(name: 'disposal_date') DateTime? disposalDate, + String status, + @JsonKey(name: 'requested_by') String? requestedBy, + @JsonKey(name: 'approved_by') String? approvedBy, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class __$$AssetDisposalModelImplCopyWithImpl<$Res> + extends _$AssetDisposalModelCopyWithImpl<$Res, _$AssetDisposalModelImpl> + implements _$$AssetDisposalModelImplCopyWith<$Res> { + __$$AssetDisposalModelImplCopyWithImpl( + _$AssetDisposalModelImpl _value, + $Res Function(_$AssetDisposalModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AssetDisposalModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? assetId = null, + Object? assetName = freezed, + Object? disposalReason = null, + Object? disposalDate = freezed, + Object? status = null, + Object? requestedBy = freezed, + Object? approvedBy = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$AssetDisposalModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + assetId: null == assetId + ? _value.assetId + : assetId // ignore: cast_nullable_to_non_nullable + as String, + assetName: freezed == assetName + ? _value.assetName + : assetName // ignore: cast_nullable_to_non_nullable + as String?, + disposalReason: null == disposalReason + ? _value.disposalReason + : disposalReason // ignore: cast_nullable_to_non_nullable + as String, + disposalDate: freezed == disposalDate + ? _value.disposalDate + : disposalDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + requestedBy: freezed == requestedBy + ? _value.requestedBy + : requestedBy // ignore: cast_nullable_to_non_nullable + as String?, + approvedBy: freezed == approvedBy + ? _value.approvedBy + : approvedBy // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AssetDisposalModelImpl implements _AssetDisposalModel { + const _$AssetDisposalModelImpl({ + required this.id, + @JsonKey(name: 'asset_id') required this.assetId, + @JsonKey(name: 'asset_name') this.assetName, + @JsonKey(name: 'disposal_reason') required this.disposalReason, + @JsonKey(name: 'disposal_date') this.disposalDate, + this.status = 'pending', + @JsonKey(name: 'requested_by') this.requestedBy, + @JsonKey(name: 'approved_by') this.approvedBy, + this.createdAt, + this.updatedAt, + }); + + factory _$AssetDisposalModelImpl.fromJson(Map json) => + _$$AssetDisposalModelImplFromJson(json); + + @override + final String id; + @override + @JsonKey(name: 'asset_id') + final String assetId; + @override + @JsonKey(name: 'asset_name') + final String? assetName; + @override + @JsonKey(name: 'disposal_reason') + final String disposalReason; + @override + @JsonKey(name: 'disposal_date') + final DateTime? disposalDate; + @override + @JsonKey() + final String status; + @override + @JsonKey(name: 'requested_by') + final String? requestedBy; + @override + @JsonKey(name: 'approved_by') + final String? approvedBy; + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'AssetDisposalModel(id: $id, assetId: $assetId, assetName: $assetName, disposalReason: $disposalReason, disposalDate: $disposalDate, status: $status, requestedBy: $requestedBy, approvedBy: $approvedBy, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AssetDisposalModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.assetId, assetId) || other.assetId == assetId) && + (identical(other.assetName, assetName) || + other.assetName == assetName) && + (identical(other.disposalReason, disposalReason) || + other.disposalReason == disposalReason) && + (identical(other.disposalDate, disposalDate) || + other.disposalDate == disposalDate) && + (identical(other.status, status) || other.status == status) && + (identical(other.requestedBy, requestedBy) || + other.requestedBy == requestedBy) && + (identical(other.approvedBy, approvedBy) || + other.approvedBy == approvedBy) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + assetId, + assetName, + disposalReason, + disposalDate, + status, + requestedBy, + approvedBy, + createdAt, + updatedAt, + ); + + /// Create a copy of AssetDisposalModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AssetDisposalModelImplCopyWith<_$AssetDisposalModelImpl> get copyWith => + __$$AssetDisposalModelImplCopyWithImpl<_$AssetDisposalModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$AssetDisposalModelImplToJson(this); + } +} + +abstract class _AssetDisposalModel implements AssetDisposalModel { + const factory _AssetDisposalModel({ + required final String id, + @JsonKey(name: 'asset_id') required final String assetId, + @JsonKey(name: 'asset_name') final String? assetName, + @JsonKey(name: 'disposal_reason') required final String disposalReason, + @JsonKey(name: 'disposal_date') final DateTime? disposalDate, + final String status, + @JsonKey(name: 'requested_by') final String? requestedBy, + @JsonKey(name: 'approved_by') final String? approvedBy, + final DateTime? createdAt, + final DateTime? updatedAt, + }) = _$AssetDisposalModelImpl; + + factory _AssetDisposalModel.fromJson(Map json) = + _$AssetDisposalModelImpl.fromJson; + + @override + String get id; + @override + @JsonKey(name: 'asset_id') + String get assetId; + @override + @JsonKey(name: 'asset_name') + String? get assetName; + @override + @JsonKey(name: 'disposal_reason') + String get disposalReason; + @override + @JsonKey(name: 'disposal_date') + DateTime? get disposalDate; + @override + String get status; + @override + @JsonKey(name: 'requested_by') + String? get requestedBy; + @override + @JsonKey(name: 'approved_by') + String? get approvedBy; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of AssetDisposalModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AssetDisposalModelImplCopyWith<_$AssetDisposalModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/asset_model.g.dart b/lib/shared/models/asset_model.g.dart new file mode 100644 index 0000000..3cb858b --- /dev/null +++ b/lib/shared/models/asset_model.g.dart @@ -0,0 +1,197 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'asset_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$AssetCategoryModelImpl _$$AssetCategoryModelImplFromJson( + Map json, +) => _$AssetCategoryModelImpl( + id: json['id'] as String, + name: json['name'] as String, + slug: json['slug'] as String, + description: json['description'] as String?, + companyId: json['company_id'] as String?, + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), +); + +Map _$$AssetCategoryModelImplToJson( + _$AssetCategoryModelImpl instance, +) => { + 'id': instance.id, + 'name': instance.name, + 'slug': instance.slug, + 'description': instance.description, + 'company_id': instance.companyId, + 'createdAt': instance.createdAt?.toIso8601String(), +}; + +_$AssetModelImpl _$$AssetModelImplFromJson(Map json) => + _$AssetModelImpl( + id: json['id'] as String, + name: json['name'] as String, + assetCode: json['asset_code'] as String, + categoryId: json['category_id'] as String, + categoryName: json['category_name'] as String?, + brand: json['brand'] as String?, + model: json['model'] as String?, + serialNumber: json['serial_number'] as String?, + purchaseDate: json['purchase_date'] == null + ? null + : DateTime.parse(json['purchase_date'] as String), + purchaseCost: (json['purchase_cost'] as num?)?.toDouble(), + vendor: json['vendor'] as String?, + warrantyStart: json['warranty_start'] == null + ? null + : DateTime.parse(json['warranty_start'] as String), + warrantyEnd: json['warranty_end'] == null + ? null + : DateTime.parse(json['warranty_end'] as String), + status: json['status'] as String? ?? 'available', + branchId: json['branch_id'] as String?, + branchName: json['branch_name'] as String?, + companyId: json['company_id'] as String?, + qrCodeUrl: json['qr_code_url'] as String?, + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + ); + +Map _$$AssetModelImplToJson(_$AssetModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'asset_code': instance.assetCode, + 'category_id': instance.categoryId, + 'category_name': instance.categoryName, + 'brand': instance.brand, + 'model': instance.model, + 'serial_number': instance.serialNumber, + 'purchase_date': instance.purchaseDate?.toIso8601String(), + 'purchase_cost': instance.purchaseCost, + 'vendor': instance.vendor, + 'warranty_start': instance.warrantyStart?.toIso8601String(), + 'warranty_end': instance.warrantyEnd?.toIso8601String(), + 'status': instance.status, + 'branch_id': instance.branchId, + 'branch_name': instance.branchName, + 'company_id': instance.companyId, + 'qr_code_url': instance.qrCodeUrl, + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), + }; + +_$AssetAllocationModelImpl _$$AssetAllocationModelImplFromJson( + Map json, +) => _$AssetAllocationModelImpl( + id: json['id'] as String, + assetId: json['asset_id'] as String, + assetName: json['asset_name'] as String?, + employeeId: json['employee_id'] as String, + employeeName: json['employee_name'] as String?, + assignedDate: DateTime.parse(json['assigned_date'] as String), + returnedDate: json['returned_date'] == null + ? null + : DateTime.parse(json['returned_date'] as String), + remarks: json['remarks'] as String?, + status: json['status'] as String? ?? 'active', + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), +); + +Map _$$AssetAllocationModelImplToJson( + _$AssetAllocationModelImpl instance, +) => { + 'id': instance.id, + 'asset_id': instance.assetId, + 'asset_name': instance.assetName, + 'employee_id': instance.employeeId, + 'employee_name': instance.employeeName, + 'assigned_date': instance.assignedDate.toIso8601String(), + 'returned_date': instance.returnedDate?.toIso8601String(), + 'remarks': instance.remarks, + 'status': instance.status, + 'createdAt': instance.createdAt?.toIso8601String(), +}; + +_$AssetMaintenanceModelImpl _$$AssetMaintenanceModelImplFromJson( + Map json, +) => _$AssetMaintenanceModelImpl( + id: json['id'] as String, + assetId: json['asset_id'] as String, + assetName: json['asset_name'] as String?, + description: json['description'] as String, + serviceVendor: json['service_vendor'] as String?, + cost: (json['cost'] as num?)?.toDouble(), + status: json['status'] as String? ?? 'open', + requestedBy: json['requested_by'] as String?, + completedDate: json['completed_date'] == null + ? null + : DateTime.parse(json['completed_date'] as String), + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), +); + +Map _$$AssetMaintenanceModelImplToJson( + _$AssetMaintenanceModelImpl instance, +) => { + 'id': instance.id, + 'asset_id': instance.assetId, + 'asset_name': instance.assetName, + 'description': instance.description, + 'service_vendor': instance.serviceVendor, + 'cost': instance.cost, + 'status': instance.status, + 'requested_by': instance.requestedBy, + 'completed_date': instance.completedDate?.toIso8601String(), + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), +}; + +_$AssetDisposalModelImpl _$$AssetDisposalModelImplFromJson( + Map json, +) => _$AssetDisposalModelImpl( + id: json['id'] as String, + assetId: json['asset_id'] as String, + assetName: json['asset_name'] as String?, + disposalReason: json['disposal_reason'] as String, + disposalDate: json['disposal_date'] == null + ? null + : DateTime.parse(json['disposal_date'] as String), + status: json['status'] as String? ?? 'pending', + requestedBy: json['requested_by'] as String?, + approvedBy: json['approved_by'] as String?, + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), +); + +Map _$$AssetDisposalModelImplToJson( + _$AssetDisposalModelImpl instance, +) => { + 'id': instance.id, + 'asset_id': instance.assetId, + 'asset_name': instance.assetName, + 'disposal_reason': instance.disposalReason, + 'disposal_date': instance.disposalDate?.toIso8601String(), + 'status': instance.status, + 'requested_by': instance.requestedBy, + 'approved_by': instance.approvedBy, + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), +}; diff --git a/lib/shared/models/branch_model.dart b/lib/shared/models/branch_model.dart new file mode 100644 index 0000000..d6c5d35 --- /dev/null +++ b/lib/shared/models/branch_model.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'branch_model.freezed.dart'; +part 'branch_model.g.dart'; + +@freezed +class BranchModel with _$BranchModel { + const factory BranchModel({ + required String id, + required String name, + @JsonKey(name: 'branch_code') required String branchCode, + String? location, + String? manager, + @JsonKey(name: 'manager_id') String? managerId, + @JsonKey(name: 'company_id') required String companyId, + @Default('active') String status, + DateTime? createdAt, + DateTime? updatedAt, + }) = _BranchModel; + + factory BranchModel.fromJson(Map json) => + _$BranchModelFromJson(json); +} diff --git a/lib/shared/models/branch_model.freezed.dart b/lib/shared/models/branch_model.freezed.dart new file mode 100644 index 0000000..85bbc53 --- /dev/null +++ b/lib/shared/models/branch_model.freezed.dart @@ -0,0 +1,387 @@ +// 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 'branch_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +BranchModel _$BranchModelFromJson(Map json) { + return _BranchModel.fromJson(json); +} + +/// @nodoc +mixin _$BranchModel { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + @JsonKey(name: 'branch_code') + String get branchCode => throw _privateConstructorUsedError; + String? get location => throw _privateConstructorUsedError; + String? get manager => throw _privateConstructorUsedError; + @JsonKey(name: 'manager_id') + String? get managerId => throw _privateConstructorUsedError; + @JsonKey(name: 'company_id') + String get companyId => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this BranchModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of BranchModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $BranchModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $BranchModelCopyWith<$Res> { + factory $BranchModelCopyWith( + BranchModel value, + $Res Function(BranchModel) then, + ) = _$BranchModelCopyWithImpl<$Res, BranchModel>; + @useResult + $Res call({ + String id, + String name, + @JsonKey(name: 'branch_code') String branchCode, + String? location, + String? manager, + @JsonKey(name: 'manager_id') String? managerId, + @JsonKey(name: 'company_id') String companyId, + String status, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$BranchModelCopyWithImpl<$Res, $Val extends BranchModel> + implements $BranchModelCopyWith<$Res> { + _$BranchModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of BranchModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? branchCode = null, + Object? location = freezed, + Object? manager = freezed, + Object? managerId = freezed, + Object? companyId = null, + Object? status = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + branchCode: null == branchCode + ? _value.branchCode + : branchCode // ignore: cast_nullable_to_non_nullable + as String, + location: freezed == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String?, + manager: freezed == manager + ? _value.manager + : manager // ignore: cast_nullable_to_non_nullable + as String?, + managerId: freezed == managerId + ? _value.managerId + : managerId // ignore: cast_nullable_to_non_nullable + as String?, + companyId: null == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as String, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$BranchModelImplCopyWith<$Res> + implements $BranchModelCopyWith<$Res> { + factory _$$BranchModelImplCopyWith( + _$BranchModelImpl value, + $Res Function(_$BranchModelImpl) then, + ) = __$$BranchModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + @JsonKey(name: 'branch_code') String branchCode, + String? location, + String? manager, + @JsonKey(name: 'manager_id') String? managerId, + @JsonKey(name: 'company_id') String companyId, + String status, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class __$$BranchModelImplCopyWithImpl<$Res> + extends _$BranchModelCopyWithImpl<$Res, _$BranchModelImpl> + implements _$$BranchModelImplCopyWith<$Res> { + __$$BranchModelImplCopyWithImpl( + _$BranchModelImpl _value, + $Res Function(_$BranchModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of BranchModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? branchCode = null, + Object? location = freezed, + Object? manager = freezed, + Object? managerId = freezed, + Object? companyId = null, + Object? status = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$BranchModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + branchCode: null == branchCode + ? _value.branchCode + : branchCode // ignore: cast_nullable_to_non_nullable + as String, + location: freezed == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String?, + manager: freezed == manager + ? _value.manager + : manager // ignore: cast_nullable_to_non_nullable + as String?, + managerId: freezed == managerId + ? _value.managerId + : managerId // ignore: cast_nullable_to_non_nullable + as String?, + companyId: null == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as String, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$BranchModelImpl implements _BranchModel { + const _$BranchModelImpl({ + required this.id, + required this.name, + @JsonKey(name: 'branch_code') required this.branchCode, + this.location, + this.manager, + @JsonKey(name: 'manager_id') this.managerId, + @JsonKey(name: 'company_id') required this.companyId, + this.status = 'active', + this.createdAt, + this.updatedAt, + }); + + factory _$BranchModelImpl.fromJson(Map json) => + _$$BranchModelImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + @JsonKey(name: 'branch_code') + final String branchCode; + @override + final String? location; + @override + final String? manager; + @override + @JsonKey(name: 'manager_id') + final String? managerId; + @override + @JsonKey(name: 'company_id') + final String companyId; + @override + @JsonKey() + final String status; + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'BranchModel(id: $id, name: $name, branchCode: $branchCode, location: $location, manager: $manager, managerId: $managerId, companyId: $companyId, status: $status, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$BranchModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.branchCode, branchCode) || + other.branchCode == branchCode) && + (identical(other.location, location) || + other.location == location) && + (identical(other.manager, manager) || other.manager == manager) && + (identical(other.managerId, managerId) || + other.managerId == managerId) && + (identical(other.companyId, companyId) || + other.companyId == companyId) && + (identical(other.status, status) || other.status == status) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + branchCode, + location, + manager, + managerId, + companyId, + status, + createdAt, + updatedAt, + ); + + /// Create a copy of BranchModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$BranchModelImplCopyWith<_$BranchModelImpl> get copyWith => + __$$BranchModelImplCopyWithImpl<_$BranchModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$BranchModelImplToJson(this); + } +} + +abstract class _BranchModel implements BranchModel { + const factory _BranchModel({ + required final String id, + required final String name, + @JsonKey(name: 'branch_code') required final String branchCode, + final String? location, + final String? manager, + @JsonKey(name: 'manager_id') final String? managerId, + @JsonKey(name: 'company_id') required final String companyId, + final String status, + final DateTime? createdAt, + final DateTime? updatedAt, + }) = _$BranchModelImpl; + + factory _BranchModel.fromJson(Map json) = + _$BranchModelImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + @JsonKey(name: 'branch_code') + String get branchCode; + @override + String? get location; + @override + String? get manager; + @override + @JsonKey(name: 'manager_id') + String? get managerId; + @override + @JsonKey(name: 'company_id') + String get companyId; + @override + String get status; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of BranchModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$BranchModelImplCopyWith<_$BranchModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/branch_model.g.dart b/lib/shared/models/branch_model.g.dart new file mode 100644 index 0000000..27d3d67 --- /dev/null +++ b/lib/shared/models/branch_model.g.dart @@ -0,0 +1,39 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'branch_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$BranchModelImpl _$$BranchModelImplFromJson(Map json) => + _$BranchModelImpl( + id: json['id'] as String, + name: json['name'] as String, + branchCode: json['branch_code'] as String, + location: json['location'] as String?, + manager: json['manager'] as String?, + managerId: json['manager_id'] as String?, + companyId: json['company_id'] as String, + status: json['status'] as String? ?? 'active', + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + ); + +Map _$$BranchModelImplToJson(_$BranchModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'branch_code': instance.branchCode, + 'location': instance.location, + 'manager': instance.manager, + 'manager_id': instance.managerId, + 'company_id': instance.companyId, + 'status': instance.status, + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), + }; diff --git a/lib/shared/models/company_model.dart b/lib/shared/models/company_model.dart new file mode 100644 index 0000000..7952cc3 --- /dev/null +++ b/lib/shared/models/company_model.dart @@ -0,0 +1,37 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'company_model.freezed.dart'; +part 'company_model.g.dart'; + +@freezed +class CompanyModel with _$CompanyModel { + const factory CompanyModel({ + required String id, + required String name, + @JsonKey(name: 'company_code') required String companyCode, + @JsonKey(name: 'gst_number') String? gstNumber, + String? address, + String? email, + String? phone, + String? logo, + @Default('active') String status, + DateTime? createdAt, + DateTime? updatedAt, + }) = _CompanyModel; + + factory CompanyModel.fromJson(Map json) => + _$CompanyModelFromJson(json); +} + +@freezed +class CompanySettingsModel with _$CompanySettingsModel { + const factory CompanySettingsModel({ + @JsonKey(name: 'primary_color') String? primaryColor, + @JsonKey(name: 'secondary_color') String? secondaryColor, + String? logo, + @JsonKey(name: 'company_name') String? companyName, + }) = _CompanySettingsModel; + + factory CompanySettingsModel.fromJson(Map json) => + _$CompanySettingsModelFromJson(json); +} diff --git a/lib/shared/models/company_model.freezed.dart b/lib/shared/models/company_model.freezed.dart new file mode 100644 index 0000000..d6e63a2 --- /dev/null +++ b/lib/shared/models/company_model.freezed.dart @@ -0,0 +1,646 @@ +// 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 'company_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +CompanyModel _$CompanyModelFromJson(Map json) { + return _CompanyModel.fromJson(json); +} + +/// @nodoc +mixin _$CompanyModel { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + @JsonKey(name: 'company_code') + String get companyCode => throw _privateConstructorUsedError; + @JsonKey(name: 'gst_number') + String? get gstNumber => throw _privateConstructorUsedError; + String? get address => throw _privateConstructorUsedError; + String? get email => throw _privateConstructorUsedError; + String? get phone => throw _privateConstructorUsedError; + String? get logo => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this CompanyModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CompanyModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CompanyModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CompanyModelCopyWith<$Res> { + factory $CompanyModelCopyWith( + CompanyModel value, + $Res Function(CompanyModel) then, + ) = _$CompanyModelCopyWithImpl<$Res, CompanyModel>; + @useResult + $Res call({ + String id, + String name, + @JsonKey(name: 'company_code') String companyCode, + @JsonKey(name: 'gst_number') String? gstNumber, + String? address, + String? email, + String? phone, + String? logo, + String status, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$CompanyModelCopyWithImpl<$Res, $Val extends CompanyModel> + implements $CompanyModelCopyWith<$Res> { + _$CompanyModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CompanyModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? companyCode = null, + Object? gstNumber = freezed, + Object? address = freezed, + Object? email = freezed, + Object? phone = freezed, + Object? logo = freezed, + Object? status = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + companyCode: null == companyCode + ? _value.companyCode + : companyCode // ignore: cast_nullable_to_non_nullable + as String, + gstNumber: freezed == gstNumber + ? _value.gstNumber + : gstNumber // ignore: cast_nullable_to_non_nullable + as String?, + address: freezed == address + ? _value.address + : address // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + phone: freezed == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String?, + logo: freezed == logo + ? _value.logo + : logo // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CompanyModelImplCopyWith<$Res> + implements $CompanyModelCopyWith<$Res> { + factory _$$CompanyModelImplCopyWith( + _$CompanyModelImpl value, + $Res Function(_$CompanyModelImpl) then, + ) = __$$CompanyModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + @JsonKey(name: 'company_code') String companyCode, + @JsonKey(name: 'gst_number') String? gstNumber, + String? address, + String? email, + String? phone, + String? logo, + String status, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class __$$CompanyModelImplCopyWithImpl<$Res> + extends _$CompanyModelCopyWithImpl<$Res, _$CompanyModelImpl> + implements _$$CompanyModelImplCopyWith<$Res> { + __$$CompanyModelImplCopyWithImpl( + _$CompanyModelImpl _value, + $Res Function(_$CompanyModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CompanyModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? companyCode = null, + Object? gstNumber = freezed, + Object? address = freezed, + Object? email = freezed, + Object? phone = freezed, + Object? logo = freezed, + Object? status = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$CompanyModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + companyCode: null == companyCode + ? _value.companyCode + : companyCode // ignore: cast_nullable_to_non_nullable + as String, + gstNumber: freezed == gstNumber + ? _value.gstNumber + : gstNumber // ignore: cast_nullable_to_non_nullable + as String?, + address: freezed == address + ? _value.address + : address // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + phone: freezed == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String?, + logo: freezed == logo + ? _value.logo + : logo // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CompanyModelImpl implements _CompanyModel { + const _$CompanyModelImpl({ + required this.id, + required this.name, + @JsonKey(name: 'company_code') required this.companyCode, + @JsonKey(name: 'gst_number') this.gstNumber, + this.address, + this.email, + this.phone, + this.logo, + this.status = 'active', + this.createdAt, + this.updatedAt, + }); + + factory _$CompanyModelImpl.fromJson(Map json) => + _$$CompanyModelImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + @JsonKey(name: 'company_code') + final String companyCode; + @override + @JsonKey(name: 'gst_number') + final String? gstNumber; + @override + final String? address; + @override + final String? email; + @override + final String? phone; + @override + final String? logo; + @override + @JsonKey() + final String status; + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'CompanyModel(id: $id, name: $name, companyCode: $companyCode, gstNumber: $gstNumber, address: $address, email: $email, phone: $phone, logo: $logo, status: $status, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CompanyModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.companyCode, companyCode) || + other.companyCode == companyCode) && + (identical(other.gstNumber, gstNumber) || + other.gstNumber == gstNumber) && + (identical(other.address, address) || other.address == address) && + (identical(other.email, email) || other.email == email) && + (identical(other.phone, phone) || other.phone == phone) && + (identical(other.logo, logo) || other.logo == logo) && + (identical(other.status, status) || other.status == status) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + companyCode, + gstNumber, + address, + email, + phone, + logo, + status, + createdAt, + updatedAt, + ); + + /// Create a copy of CompanyModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CompanyModelImplCopyWith<_$CompanyModelImpl> get copyWith => + __$$CompanyModelImplCopyWithImpl<_$CompanyModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$CompanyModelImplToJson(this); + } +} + +abstract class _CompanyModel implements CompanyModel { + const factory _CompanyModel({ + required final String id, + required final String name, + @JsonKey(name: 'company_code') required final String companyCode, + @JsonKey(name: 'gst_number') final String? gstNumber, + final String? address, + final String? email, + final String? phone, + final String? logo, + final String status, + final DateTime? createdAt, + final DateTime? updatedAt, + }) = _$CompanyModelImpl; + + factory _CompanyModel.fromJson(Map json) = + _$CompanyModelImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + @JsonKey(name: 'company_code') + String get companyCode; + @override + @JsonKey(name: 'gst_number') + String? get gstNumber; + @override + String? get address; + @override + String? get email; + @override + String? get phone; + @override + String? get logo; + @override + String get status; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of CompanyModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CompanyModelImplCopyWith<_$CompanyModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +CompanySettingsModel _$CompanySettingsModelFromJson(Map json) { + return _CompanySettingsModel.fromJson(json); +} + +/// @nodoc +mixin _$CompanySettingsModel { + @JsonKey(name: 'primary_color') + String? get primaryColor => throw _privateConstructorUsedError; + @JsonKey(name: 'secondary_color') + String? get secondaryColor => throw _privateConstructorUsedError; + String? get logo => throw _privateConstructorUsedError; + @JsonKey(name: 'company_name') + String? get companyName => throw _privateConstructorUsedError; + + /// Serializes this CompanySettingsModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CompanySettingsModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CompanySettingsModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CompanySettingsModelCopyWith<$Res> { + factory $CompanySettingsModelCopyWith( + CompanySettingsModel value, + $Res Function(CompanySettingsModel) then, + ) = _$CompanySettingsModelCopyWithImpl<$Res, CompanySettingsModel>; + @useResult + $Res call({ + @JsonKey(name: 'primary_color') String? primaryColor, + @JsonKey(name: 'secondary_color') String? secondaryColor, + String? logo, + @JsonKey(name: 'company_name') String? companyName, + }); +} + +/// @nodoc +class _$CompanySettingsModelCopyWithImpl< + $Res, + $Val extends CompanySettingsModel +> + implements $CompanySettingsModelCopyWith<$Res> { + _$CompanySettingsModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CompanySettingsModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? primaryColor = freezed, + Object? secondaryColor = freezed, + Object? logo = freezed, + Object? companyName = freezed, + }) { + return _then( + _value.copyWith( + primaryColor: freezed == primaryColor + ? _value.primaryColor + : primaryColor // ignore: cast_nullable_to_non_nullable + as String?, + secondaryColor: freezed == secondaryColor + ? _value.secondaryColor + : secondaryColor // ignore: cast_nullable_to_non_nullable + as String?, + logo: freezed == logo + ? _value.logo + : logo // ignore: cast_nullable_to_non_nullable + as String?, + companyName: freezed == companyName + ? _value.companyName + : companyName // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CompanySettingsModelImplCopyWith<$Res> + implements $CompanySettingsModelCopyWith<$Res> { + factory _$$CompanySettingsModelImplCopyWith( + _$CompanySettingsModelImpl value, + $Res Function(_$CompanySettingsModelImpl) then, + ) = __$$CompanySettingsModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'primary_color') String? primaryColor, + @JsonKey(name: 'secondary_color') String? secondaryColor, + String? logo, + @JsonKey(name: 'company_name') String? companyName, + }); +} + +/// @nodoc +class __$$CompanySettingsModelImplCopyWithImpl<$Res> + extends _$CompanySettingsModelCopyWithImpl<$Res, _$CompanySettingsModelImpl> + implements _$$CompanySettingsModelImplCopyWith<$Res> { + __$$CompanySettingsModelImplCopyWithImpl( + _$CompanySettingsModelImpl _value, + $Res Function(_$CompanySettingsModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CompanySettingsModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? primaryColor = freezed, + Object? secondaryColor = freezed, + Object? logo = freezed, + Object? companyName = freezed, + }) { + return _then( + _$CompanySettingsModelImpl( + primaryColor: freezed == primaryColor + ? _value.primaryColor + : primaryColor // ignore: cast_nullable_to_non_nullable + as String?, + secondaryColor: freezed == secondaryColor + ? _value.secondaryColor + : secondaryColor // ignore: cast_nullable_to_non_nullable + as String?, + logo: freezed == logo + ? _value.logo + : logo // ignore: cast_nullable_to_non_nullable + as String?, + companyName: freezed == companyName + ? _value.companyName + : companyName // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CompanySettingsModelImpl implements _CompanySettingsModel { + const _$CompanySettingsModelImpl({ + @JsonKey(name: 'primary_color') this.primaryColor, + @JsonKey(name: 'secondary_color') this.secondaryColor, + this.logo, + @JsonKey(name: 'company_name') this.companyName, + }); + + factory _$CompanySettingsModelImpl.fromJson(Map json) => + _$$CompanySettingsModelImplFromJson(json); + + @override + @JsonKey(name: 'primary_color') + final String? primaryColor; + @override + @JsonKey(name: 'secondary_color') + final String? secondaryColor; + @override + final String? logo; + @override + @JsonKey(name: 'company_name') + final String? companyName; + + @override + String toString() { + return 'CompanySettingsModel(primaryColor: $primaryColor, secondaryColor: $secondaryColor, logo: $logo, companyName: $companyName)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CompanySettingsModelImpl && + (identical(other.primaryColor, primaryColor) || + other.primaryColor == primaryColor) && + (identical(other.secondaryColor, secondaryColor) || + other.secondaryColor == secondaryColor) && + (identical(other.logo, logo) || other.logo == logo) && + (identical(other.companyName, companyName) || + other.companyName == companyName)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, primaryColor, secondaryColor, logo, companyName); + + /// Create a copy of CompanySettingsModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CompanySettingsModelImplCopyWith<_$CompanySettingsModelImpl> + get copyWith => + __$$CompanySettingsModelImplCopyWithImpl<_$CompanySettingsModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$CompanySettingsModelImplToJson(this); + } +} + +abstract class _CompanySettingsModel implements CompanySettingsModel { + const factory _CompanySettingsModel({ + @JsonKey(name: 'primary_color') final String? primaryColor, + @JsonKey(name: 'secondary_color') final String? secondaryColor, + final String? logo, + @JsonKey(name: 'company_name') final String? companyName, + }) = _$CompanySettingsModelImpl; + + factory _CompanySettingsModel.fromJson(Map json) = + _$CompanySettingsModelImpl.fromJson; + + @override + @JsonKey(name: 'primary_color') + String? get primaryColor; + @override + @JsonKey(name: 'secondary_color') + String? get secondaryColor; + @override + String? get logo; + @override + @JsonKey(name: 'company_name') + String? get companyName; + + /// Create a copy of CompanySettingsModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CompanySettingsModelImplCopyWith<_$CompanySettingsModelImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/company_model.g.dart b/lib/shared/models/company_model.g.dart new file mode 100644 index 0000000..4f98da4 --- /dev/null +++ b/lib/shared/models/company_model.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'company_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$CompanyModelImpl _$$CompanyModelImplFromJson(Map json) => + _$CompanyModelImpl( + id: json['id'] as String, + name: json['name'] as String, + companyCode: json['company_code'] as String, + gstNumber: json['gst_number'] as String?, + address: json['address'] as String?, + email: json['email'] as String?, + phone: json['phone'] as String?, + logo: json['logo'] as String?, + status: json['status'] as String? ?? 'active', + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + ); + +Map _$$CompanyModelImplToJson(_$CompanyModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'company_code': instance.companyCode, + 'gst_number': instance.gstNumber, + 'address': instance.address, + 'email': instance.email, + 'phone': instance.phone, + 'logo': instance.logo, + 'status': instance.status, + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), + }; + +_$CompanySettingsModelImpl _$$CompanySettingsModelImplFromJson( + Map json, +) => _$CompanySettingsModelImpl( + primaryColor: json['primary_color'] as String?, + secondaryColor: json['secondary_color'] as String?, + logo: json['logo'] as String?, + companyName: json['company_name'] as String?, +); + +Map _$$CompanySettingsModelImplToJson( + _$CompanySettingsModelImpl instance, +) => { + 'primary_color': instance.primaryColor, + 'secondary_color': instance.secondaryColor, + 'logo': instance.logo, + 'company_name': instance.companyName, +}; diff --git a/lib/shared/models/dashboard_model.dart b/lib/shared/models/dashboard_model.dart new file mode 100644 index 0000000..7ae3096 --- /dev/null +++ b/lib/shared/models/dashboard_model.dart @@ -0,0 +1,60 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'dashboard_model.freezed.dart'; +part 'dashboard_model.g.dart'; + +@freezed +class DashboardKpis with _$DashboardKpis { + const factory DashboardKpis({ + @JsonKey(name: 'total_assets') @Default(0) int totalAssets, + @JsonKey(name: 'allocated_assets') @Default(0) int allocatedAssets, + @JsonKey(name: 'available_assets') @Default(0) int availableAssets, + @JsonKey(name: 'maintenance_assets') @Default(0) int maintenanceAssets, + @JsonKey(name: 'disposed_assets') @Default(0) int disposedAssets, + @JsonKey(name: 'warranty_expiring') @Default(0) int warrantyExpiring, + }) = _DashboardKpis; + + factory DashboardKpis.fromJson(Map json) => + _$DashboardKpisFromJson(json); +} + +@freezed +class ChartDataPoint with _$ChartDataPoint { + const factory ChartDataPoint({ + required String label, + required double value, + }) = _ChartDataPoint; + + factory ChartDataPoint.fromJson(Map json) => + _$ChartDataPointFromJson(json); +} + +@freezed +class DashboardCharts with _$DashboardCharts { + const factory DashboardCharts({ + @JsonKey(name: 'assets_by_category') @Default([]) List assetsByCategory, + @JsonKey(name: 'assets_by_branch') @Default([]) List assetsByBranch, + @JsonKey(name: 'allocation_trend') @Default([]) List allocationTrend, + }) = _DashboardCharts; + + factory DashboardCharts.fromJson(Map json) => + _$DashboardChartsFromJson(json); +} + +@freezed +class AuditLogModel with _$AuditLogModel { + const factory AuditLogModel({ + required String id, + required String action, + required String module, + @JsonKey(name: 'entity_id') String? entityId, + @JsonKey(name: 'user_id') required String userId, + @JsonKey(name: 'user_name') String? userName, + Map? changes, + @JsonKey(name: 'ip_address') String? ipAddress, + DateTime? createdAt, + }) = _AuditLogModel; + + factory AuditLogModel.fromJson(Map json) => + _$AuditLogModelFromJson(json); +} diff --git a/lib/shared/models/dashboard_model.freezed.dart b/lib/shared/models/dashboard_model.freezed.dart new file mode 100644 index 0000000..b5f24f1 --- /dev/null +++ b/lib/shared/models/dashboard_model.freezed.dart @@ -0,0 +1,1097 @@ +// 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 'dashboard_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +DashboardKpis _$DashboardKpisFromJson(Map json) { + return _DashboardKpis.fromJson(json); +} + +/// @nodoc +mixin _$DashboardKpis { + @JsonKey(name: 'total_assets') + int get totalAssets => throw _privateConstructorUsedError; + @JsonKey(name: 'allocated_assets') + int get allocatedAssets => throw _privateConstructorUsedError; + @JsonKey(name: 'available_assets') + int get availableAssets => throw _privateConstructorUsedError; + @JsonKey(name: 'maintenance_assets') + int get maintenanceAssets => throw _privateConstructorUsedError; + @JsonKey(name: 'disposed_assets') + int get disposedAssets => throw _privateConstructorUsedError; + @JsonKey(name: 'warranty_expiring') + int get warrantyExpiring => throw _privateConstructorUsedError; + + /// Serializes this DashboardKpis to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of DashboardKpis + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DashboardKpisCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DashboardKpisCopyWith<$Res> { + factory $DashboardKpisCopyWith( + DashboardKpis value, + $Res Function(DashboardKpis) then, + ) = _$DashboardKpisCopyWithImpl<$Res, DashboardKpis>; + @useResult + $Res call({ + @JsonKey(name: 'total_assets') int totalAssets, + @JsonKey(name: 'allocated_assets') int allocatedAssets, + @JsonKey(name: 'available_assets') int availableAssets, + @JsonKey(name: 'maintenance_assets') int maintenanceAssets, + @JsonKey(name: 'disposed_assets') int disposedAssets, + @JsonKey(name: 'warranty_expiring') int warrantyExpiring, + }); +} + +/// @nodoc +class _$DashboardKpisCopyWithImpl<$Res, $Val extends DashboardKpis> + implements $DashboardKpisCopyWith<$Res> { + _$DashboardKpisCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DashboardKpis + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totalAssets = null, + Object? allocatedAssets = null, + Object? availableAssets = null, + Object? maintenanceAssets = null, + Object? disposedAssets = null, + Object? warrantyExpiring = null, + }) { + return _then( + _value.copyWith( + totalAssets: null == totalAssets + ? _value.totalAssets + : totalAssets // ignore: cast_nullable_to_non_nullable + as int, + allocatedAssets: null == allocatedAssets + ? _value.allocatedAssets + : allocatedAssets // ignore: cast_nullable_to_non_nullable + as int, + availableAssets: null == availableAssets + ? _value.availableAssets + : availableAssets // ignore: cast_nullable_to_non_nullable + as int, + maintenanceAssets: null == maintenanceAssets + ? _value.maintenanceAssets + : maintenanceAssets // ignore: cast_nullable_to_non_nullable + as int, + disposedAssets: null == disposedAssets + ? _value.disposedAssets + : disposedAssets // ignore: cast_nullable_to_non_nullable + as int, + warrantyExpiring: null == warrantyExpiring + ? _value.warrantyExpiring + : warrantyExpiring // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$DashboardKpisImplCopyWith<$Res> + implements $DashboardKpisCopyWith<$Res> { + factory _$$DashboardKpisImplCopyWith( + _$DashboardKpisImpl value, + $Res Function(_$DashboardKpisImpl) then, + ) = __$$DashboardKpisImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'total_assets') int totalAssets, + @JsonKey(name: 'allocated_assets') int allocatedAssets, + @JsonKey(name: 'available_assets') int availableAssets, + @JsonKey(name: 'maintenance_assets') int maintenanceAssets, + @JsonKey(name: 'disposed_assets') int disposedAssets, + @JsonKey(name: 'warranty_expiring') int warrantyExpiring, + }); +} + +/// @nodoc +class __$$DashboardKpisImplCopyWithImpl<$Res> + extends _$DashboardKpisCopyWithImpl<$Res, _$DashboardKpisImpl> + implements _$$DashboardKpisImplCopyWith<$Res> { + __$$DashboardKpisImplCopyWithImpl( + _$DashboardKpisImpl _value, + $Res Function(_$DashboardKpisImpl) _then, + ) : super(_value, _then); + + /// Create a copy of DashboardKpis + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totalAssets = null, + Object? allocatedAssets = null, + Object? availableAssets = null, + Object? maintenanceAssets = null, + Object? disposedAssets = null, + Object? warrantyExpiring = null, + }) { + return _then( + _$DashboardKpisImpl( + totalAssets: null == totalAssets + ? _value.totalAssets + : totalAssets // ignore: cast_nullable_to_non_nullable + as int, + allocatedAssets: null == allocatedAssets + ? _value.allocatedAssets + : allocatedAssets // ignore: cast_nullable_to_non_nullable + as int, + availableAssets: null == availableAssets + ? _value.availableAssets + : availableAssets // ignore: cast_nullable_to_non_nullable + as int, + maintenanceAssets: null == maintenanceAssets + ? _value.maintenanceAssets + : maintenanceAssets // ignore: cast_nullable_to_non_nullable + as int, + disposedAssets: null == disposedAssets + ? _value.disposedAssets + : disposedAssets // ignore: cast_nullable_to_non_nullable + as int, + warrantyExpiring: null == warrantyExpiring + ? _value.warrantyExpiring + : warrantyExpiring // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$DashboardKpisImpl implements _DashboardKpis { + const _$DashboardKpisImpl({ + @JsonKey(name: 'total_assets') this.totalAssets = 0, + @JsonKey(name: 'allocated_assets') this.allocatedAssets = 0, + @JsonKey(name: 'available_assets') this.availableAssets = 0, + @JsonKey(name: 'maintenance_assets') this.maintenanceAssets = 0, + @JsonKey(name: 'disposed_assets') this.disposedAssets = 0, + @JsonKey(name: 'warranty_expiring') this.warrantyExpiring = 0, + }); + + factory _$DashboardKpisImpl.fromJson(Map json) => + _$$DashboardKpisImplFromJson(json); + + @override + @JsonKey(name: 'total_assets') + final int totalAssets; + @override + @JsonKey(name: 'allocated_assets') + final int allocatedAssets; + @override + @JsonKey(name: 'available_assets') + final int availableAssets; + @override + @JsonKey(name: 'maintenance_assets') + final int maintenanceAssets; + @override + @JsonKey(name: 'disposed_assets') + final int disposedAssets; + @override + @JsonKey(name: 'warranty_expiring') + final int warrantyExpiring; + + @override + String toString() { + return 'DashboardKpis(totalAssets: $totalAssets, allocatedAssets: $allocatedAssets, availableAssets: $availableAssets, maintenanceAssets: $maintenanceAssets, disposedAssets: $disposedAssets, warrantyExpiring: $warrantyExpiring)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DashboardKpisImpl && + (identical(other.totalAssets, totalAssets) || + other.totalAssets == totalAssets) && + (identical(other.allocatedAssets, allocatedAssets) || + other.allocatedAssets == allocatedAssets) && + (identical(other.availableAssets, availableAssets) || + other.availableAssets == availableAssets) && + (identical(other.maintenanceAssets, maintenanceAssets) || + other.maintenanceAssets == maintenanceAssets) && + (identical(other.disposedAssets, disposedAssets) || + other.disposedAssets == disposedAssets) && + (identical(other.warrantyExpiring, warrantyExpiring) || + other.warrantyExpiring == warrantyExpiring)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + totalAssets, + allocatedAssets, + availableAssets, + maintenanceAssets, + disposedAssets, + warrantyExpiring, + ); + + /// Create a copy of DashboardKpis + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DashboardKpisImplCopyWith<_$DashboardKpisImpl> get copyWith => + __$$DashboardKpisImplCopyWithImpl<_$DashboardKpisImpl>(this, _$identity); + + @override + Map toJson() { + return _$$DashboardKpisImplToJson(this); + } +} + +abstract class _DashboardKpis implements DashboardKpis { + const factory _DashboardKpis({ + @JsonKey(name: 'total_assets') final int totalAssets, + @JsonKey(name: 'allocated_assets') final int allocatedAssets, + @JsonKey(name: 'available_assets') final int availableAssets, + @JsonKey(name: 'maintenance_assets') final int maintenanceAssets, + @JsonKey(name: 'disposed_assets') final int disposedAssets, + @JsonKey(name: 'warranty_expiring') final int warrantyExpiring, + }) = _$DashboardKpisImpl; + + factory _DashboardKpis.fromJson(Map json) = + _$DashboardKpisImpl.fromJson; + + @override + @JsonKey(name: 'total_assets') + int get totalAssets; + @override + @JsonKey(name: 'allocated_assets') + int get allocatedAssets; + @override + @JsonKey(name: 'available_assets') + int get availableAssets; + @override + @JsonKey(name: 'maintenance_assets') + int get maintenanceAssets; + @override + @JsonKey(name: 'disposed_assets') + int get disposedAssets; + @override + @JsonKey(name: 'warranty_expiring') + int get warrantyExpiring; + + /// Create a copy of DashboardKpis + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DashboardKpisImplCopyWith<_$DashboardKpisImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ChartDataPoint _$ChartDataPointFromJson(Map json) { + return _ChartDataPoint.fromJson(json); +} + +/// @nodoc +mixin _$ChartDataPoint { + String get label => throw _privateConstructorUsedError; + double get value => throw _privateConstructorUsedError; + + /// Serializes this ChartDataPoint to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ChartDataPoint + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ChartDataPointCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChartDataPointCopyWith<$Res> { + factory $ChartDataPointCopyWith( + ChartDataPoint value, + $Res Function(ChartDataPoint) then, + ) = _$ChartDataPointCopyWithImpl<$Res, ChartDataPoint>; + @useResult + $Res call({String label, double value}); +} + +/// @nodoc +class _$ChartDataPointCopyWithImpl<$Res, $Val extends ChartDataPoint> + implements $ChartDataPointCopyWith<$Res> { + _$ChartDataPointCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ChartDataPoint + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? label = null, Object? value = null}) { + return _then( + _value.copyWith( + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + value: null == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as double, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ChartDataPointImplCopyWith<$Res> + implements $ChartDataPointCopyWith<$Res> { + factory _$$ChartDataPointImplCopyWith( + _$ChartDataPointImpl value, + $Res Function(_$ChartDataPointImpl) then, + ) = __$$ChartDataPointImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String label, double value}); +} + +/// @nodoc +class __$$ChartDataPointImplCopyWithImpl<$Res> + extends _$ChartDataPointCopyWithImpl<$Res, _$ChartDataPointImpl> + implements _$$ChartDataPointImplCopyWith<$Res> { + __$$ChartDataPointImplCopyWithImpl( + _$ChartDataPointImpl _value, + $Res Function(_$ChartDataPointImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ChartDataPoint + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? label = null, Object? value = null}) { + return _then( + _$ChartDataPointImpl( + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + value: null == value + ? _value.value + : value // ignore: cast_nullable_to_non_nullable + as double, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChartDataPointImpl implements _ChartDataPoint { + const _$ChartDataPointImpl({required this.label, required this.value}); + + factory _$ChartDataPointImpl.fromJson(Map json) => + _$$ChartDataPointImplFromJson(json); + + @override + final String label; + @override + final double value; + + @override + String toString() { + return 'ChartDataPoint(label: $label, value: $value)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChartDataPointImpl && + (identical(other.label, label) || other.label == label) && + (identical(other.value, value) || other.value == value)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, label, value); + + /// Create a copy of ChartDataPoint + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ChartDataPointImplCopyWith<_$ChartDataPointImpl> get copyWith => + __$$ChartDataPointImplCopyWithImpl<_$ChartDataPointImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ChartDataPointImplToJson(this); + } +} + +abstract class _ChartDataPoint implements ChartDataPoint { + const factory _ChartDataPoint({ + required final String label, + required final double value, + }) = _$ChartDataPointImpl; + + factory _ChartDataPoint.fromJson(Map json) = + _$ChartDataPointImpl.fromJson; + + @override + String get label; + @override + double get value; + + /// Create a copy of ChartDataPoint + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ChartDataPointImplCopyWith<_$ChartDataPointImpl> get copyWith => + throw _privateConstructorUsedError; +} + +DashboardCharts _$DashboardChartsFromJson(Map json) { + return _DashboardCharts.fromJson(json); +} + +/// @nodoc +mixin _$DashboardCharts { + @JsonKey(name: 'assets_by_category') + List get assetsByCategory => + throw _privateConstructorUsedError; + @JsonKey(name: 'assets_by_branch') + List get assetsByBranch => throw _privateConstructorUsedError; + @JsonKey(name: 'allocation_trend') + List get allocationTrend => + throw _privateConstructorUsedError; + + /// Serializes this DashboardCharts to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of DashboardCharts + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DashboardChartsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DashboardChartsCopyWith<$Res> { + factory $DashboardChartsCopyWith( + DashboardCharts value, + $Res Function(DashboardCharts) then, + ) = _$DashboardChartsCopyWithImpl<$Res, DashboardCharts>; + @useResult + $Res call({ + @JsonKey(name: 'assets_by_category') List assetsByCategory, + @JsonKey(name: 'assets_by_branch') List assetsByBranch, + @JsonKey(name: 'allocation_trend') List allocationTrend, + }); +} + +/// @nodoc +class _$DashboardChartsCopyWithImpl<$Res, $Val extends DashboardCharts> + implements $DashboardChartsCopyWith<$Res> { + _$DashboardChartsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DashboardCharts + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? assetsByCategory = null, + Object? assetsByBranch = null, + Object? allocationTrend = null, + }) { + return _then( + _value.copyWith( + assetsByCategory: null == assetsByCategory + ? _value.assetsByCategory + : assetsByCategory // ignore: cast_nullable_to_non_nullable + as List, + assetsByBranch: null == assetsByBranch + ? _value.assetsByBranch + : assetsByBranch // ignore: cast_nullable_to_non_nullable + as List, + allocationTrend: null == allocationTrend + ? _value.allocationTrend + : allocationTrend // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$DashboardChartsImplCopyWith<$Res> + implements $DashboardChartsCopyWith<$Res> { + factory _$$DashboardChartsImplCopyWith( + _$DashboardChartsImpl value, + $Res Function(_$DashboardChartsImpl) then, + ) = __$$DashboardChartsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'assets_by_category') List assetsByCategory, + @JsonKey(name: 'assets_by_branch') List assetsByBranch, + @JsonKey(name: 'allocation_trend') List allocationTrend, + }); +} + +/// @nodoc +class __$$DashboardChartsImplCopyWithImpl<$Res> + extends _$DashboardChartsCopyWithImpl<$Res, _$DashboardChartsImpl> + implements _$$DashboardChartsImplCopyWith<$Res> { + __$$DashboardChartsImplCopyWithImpl( + _$DashboardChartsImpl _value, + $Res Function(_$DashboardChartsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of DashboardCharts + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? assetsByCategory = null, + Object? assetsByBranch = null, + Object? allocationTrend = null, + }) { + return _then( + _$DashboardChartsImpl( + assetsByCategory: null == assetsByCategory + ? _value._assetsByCategory + : assetsByCategory // ignore: cast_nullable_to_non_nullable + as List, + assetsByBranch: null == assetsByBranch + ? _value._assetsByBranch + : assetsByBranch // ignore: cast_nullable_to_non_nullable + as List, + allocationTrend: null == allocationTrend + ? _value._allocationTrend + : allocationTrend // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$DashboardChartsImpl implements _DashboardCharts { + const _$DashboardChartsImpl({ + @JsonKey(name: 'assets_by_category') + final List assetsByCategory = const [], + @JsonKey(name: 'assets_by_branch') + final List assetsByBranch = const [], + @JsonKey(name: 'allocation_trend') + final List allocationTrend = const [], + }) : _assetsByCategory = assetsByCategory, + _assetsByBranch = assetsByBranch, + _allocationTrend = allocationTrend; + + factory _$DashboardChartsImpl.fromJson(Map json) => + _$$DashboardChartsImplFromJson(json); + + final List _assetsByCategory; + @override + @JsonKey(name: 'assets_by_category') + List get assetsByCategory { + if (_assetsByCategory is EqualUnmodifiableListView) + return _assetsByCategory; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_assetsByCategory); + } + + final List _assetsByBranch; + @override + @JsonKey(name: 'assets_by_branch') + List get assetsByBranch { + if (_assetsByBranch is EqualUnmodifiableListView) return _assetsByBranch; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_assetsByBranch); + } + + final List _allocationTrend; + @override + @JsonKey(name: 'allocation_trend') + List get allocationTrend { + if (_allocationTrend is EqualUnmodifiableListView) return _allocationTrend; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_allocationTrend); + } + + @override + String toString() { + return 'DashboardCharts(assetsByCategory: $assetsByCategory, assetsByBranch: $assetsByBranch, allocationTrend: $allocationTrend)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DashboardChartsImpl && + const DeepCollectionEquality().equals( + other._assetsByCategory, + _assetsByCategory, + ) && + const DeepCollectionEquality().equals( + other._assetsByBranch, + _assetsByBranch, + ) && + const DeepCollectionEquality().equals( + other._allocationTrend, + _allocationTrend, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_assetsByCategory), + const DeepCollectionEquality().hash(_assetsByBranch), + const DeepCollectionEquality().hash(_allocationTrend), + ); + + /// Create a copy of DashboardCharts + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DashboardChartsImplCopyWith<_$DashboardChartsImpl> get copyWith => + __$$DashboardChartsImplCopyWithImpl<_$DashboardChartsImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$DashboardChartsImplToJson(this); + } +} + +abstract class _DashboardCharts implements DashboardCharts { + const factory _DashboardCharts({ + @JsonKey(name: 'assets_by_category') + final List assetsByCategory, + @JsonKey(name: 'assets_by_branch') + final List assetsByBranch, + @JsonKey(name: 'allocation_trend') + final List allocationTrend, + }) = _$DashboardChartsImpl; + + factory _DashboardCharts.fromJson(Map json) = + _$DashboardChartsImpl.fromJson; + + @override + @JsonKey(name: 'assets_by_category') + List get assetsByCategory; + @override + @JsonKey(name: 'assets_by_branch') + List get assetsByBranch; + @override + @JsonKey(name: 'allocation_trend') + List get allocationTrend; + + /// Create a copy of DashboardCharts + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DashboardChartsImplCopyWith<_$DashboardChartsImpl> get copyWith => + throw _privateConstructorUsedError; +} + +AuditLogModel _$AuditLogModelFromJson(Map json) { + return _AuditLogModel.fromJson(json); +} + +/// @nodoc +mixin _$AuditLogModel { + String get id => throw _privateConstructorUsedError; + String get action => throw _privateConstructorUsedError; + String get module => throw _privateConstructorUsedError; + @JsonKey(name: 'entity_id') + String? get entityId => throw _privateConstructorUsedError; + @JsonKey(name: 'user_id') + String get userId => throw _privateConstructorUsedError; + @JsonKey(name: 'user_name') + String? get userName => throw _privateConstructorUsedError; + Map? get changes => throw _privateConstructorUsedError; + @JsonKey(name: 'ip_address') + String? get ipAddress => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + + /// Serializes this AuditLogModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuditLogModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuditLogModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuditLogModelCopyWith<$Res> { + factory $AuditLogModelCopyWith( + AuditLogModel value, + $Res Function(AuditLogModel) then, + ) = _$AuditLogModelCopyWithImpl<$Res, AuditLogModel>; + @useResult + $Res call({ + String id, + String action, + String module, + @JsonKey(name: 'entity_id') String? entityId, + @JsonKey(name: 'user_id') String userId, + @JsonKey(name: 'user_name') String? userName, + Map? changes, + @JsonKey(name: 'ip_address') String? ipAddress, + DateTime? createdAt, + }); +} + +/// @nodoc +class _$AuditLogModelCopyWithImpl<$Res, $Val extends AuditLogModel> + implements $AuditLogModelCopyWith<$Res> { + _$AuditLogModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuditLogModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? action = null, + Object? module = null, + Object? entityId = freezed, + Object? userId = null, + Object? userName = freezed, + Object? changes = freezed, + Object? ipAddress = freezed, + Object? createdAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + action: null == action + ? _value.action + : action // ignore: cast_nullable_to_non_nullable + as String, + module: null == module + ? _value.module + : module // ignore: cast_nullable_to_non_nullable + as String, + entityId: freezed == entityId + ? _value.entityId + : entityId // ignore: cast_nullable_to_non_nullable + as String?, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + userName: freezed == userName + ? _value.userName + : userName // ignore: cast_nullable_to_non_nullable + as String?, + changes: freezed == changes + ? _value.changes + : changes // ignore: cast_nullable_to_non_nullable + as Map?, + ipAddress: freezed == ipAddress + ? _value.ipAddress + : ipAddress // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AuditLogModelImplCopyWith<$Res> + implements $AuditLogModelCopyWith<$Res> { + factory _$$AuditLogModelImplCopyWith( + _$AuditLogModelImpl value, + $Res Function(_$AuditLogModelImpl) then, + ) = __$$AuditLogModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String action, + String module, + @JsonKey(name: 'entity_id') String? entityId, + @JsonKey(name: 'user_id') String userId, + @JsonKey(name: 'user_name') String? userName, + Map? changes, + @JsonKey(name: 'ip_address') String? ipAddress, + DateTime? createdAt, + }); +} + +/// @nodoc +class __$$AuditLogModelImplCopyWithImpl<$Res> + extends _$AuditLogModelCopyWithImpl<$Res, _$AuditLogModelImpl> + implements _$$AuditLogModelImplCopyWith<$Res> { + __$$AuditLogModelImplCopyWithImpl( + _$AuditLogModelImpl _value, + $Res Function(_$AuditLogModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuditLogModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? action = null, + Object? module = null, + Object? entityId = freezed, + Object? userId = null, + Object? userName = freezed, + Object? changes = freezed, + Object? ipAddress = freezed, + Object? createdAt = freezed, + }) { + return _then( + _$AuditLogModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + action: null == action + ? _value.action + : action // ignore: cast_nullable_to_non_nullable + as String, + module: null == module + ? _value.module + : module // ignore: cast_nullable_to_non_nullable + as String, + entityId: freezed == entityId + ? _value.entityId + : entityId // ignore: cast_nullable_to_non_nullable + as String?, + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + userName: freezed == userName + ? _value.userName + : userName // ignore: cast_nullable_to_non_nullable + as String?, + changes: freezed == changes + ? _value._changes + : changes // ignore: cast_nullable_to_non_nullable + as Map?, + ipAddress: freezed == ipAddress + ? _value.ipAddress + : ipAddress // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuditLogModelImpl implements _AuditLogModel { + const _$AuditLogModelImpl({ + required this.id, + required this.action, + required this.module, + @JsonKey(name: 'entity_id') this.entityId, + @JsonKey(name: 'user_id') required this.userId, + @JsonKey(name: 'user_name') this.userName, + final Map? changes, + @JsonKey(name: 'ip_address') this.ipAddress, + this.createdAt, + }) : _changes = changes; + + factory _$AuditLogModelImpl.fromJson(Map json) => + _$$AuditLogModelImplFromJson(json); + + @override + final String id; + @override + final String action; + @override + final String module; + @override + @JsonKey(name: 'entity_id') + final String? entityId; + @override + @JsonKey(name: 'user_id') + final String userId; + @override + @JsonKey(name: 'user_name') + final String? userName; + final Map? _changes; + @override + Map? get changes { + final value = _changes; + if (value == null) return null; + if (_changes is EqualUnmodifiableMapView) return _changes; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + @override + @JsonKey(name: 'ip_address') + final String? ipAddress; + @override + final DateTime? createdAt; + + @override + String toString() { + return 'AuditLogModel(id: $id, action: $action, module: $module, entityId: $entityId, userId: $userId, userName: $userName, changes: $changes, ipAddress: $ipAddress, createdAt: $createdAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuditLogModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.action, action) || other.action == action) && + (identical(other.module, module) || other.module == module) && + (identical(other.entityId, entityId) || + other.entityId == entityId) && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.userName, userName) || + other.userName == userName) && + const DeepCollectionEquality().equals(other._changes, _changes) && + (identical(other.ipAddress, ipAddress) || + other.ipAddress == ipAddress) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + action, + module, + entityId, + userId, + userName, + const DeepCollectionEquality().hash(_changes), + ipAddress, + createdAt, + ); + + /// Create a copy of AuditLogModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuditLogModelImplCopyWith<_$AuditLogModelImpl> get copyWith => + __$$AuditLogModelImplCopyWithImpl<_$AuditLogModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AuditLogModelImplToJson(this); + } +} + +abstract class _AuditLogModel implements AuditLogModel { + const factory _AuditLogModel({ + required final String id, + required final String action, + required final String module, + @JsonKey(name: 'entity_id') final String? entityId, + @JsonKey(name: 'user_id') required final String userId, + @JsonKey(name: 'user_name') final String? userName, + final Map? changes, + @JsonKey(name: 'ip_address') final String? ipAddress, + final DateTime? createdAt, + }) = _$AuditLogModelImpl; + + factory _AuditLogModel.fromJson(Map json) = + _$AuditLogModelImpl.fromJson; + + @override + String get id; + @override + String get action; + @override + String get module; + @override + @JsonKey(name: 'entity_id') + String? get entityId; + @override + @JsonKey(name: 'user_id') + String get userId; + @override + @JsonKey(name: 'user_name') + String? get userName; + @override + Map? get changes; + @override + @JsonKey(name: 'ip_address') + String? get ipAddress; + @override + DateTime? get createdAt; + + /// Create a copy of AuditLogModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuditLogModelImplCopyWith<_$AuditLogModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/dashboard_model.g.dart b/lib/shared/models/dashboard_model.g.dart new file mode 100644 index 0000000..a38a86a --- /dev/null +++ b/lib/shared/models/dashboard_model.g.dart @@ -0,0 +1,93 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dashboard_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$DashboardKpisImpl _$$DashboardKpisImplFromJson(Map json) => + _$DashboardKpisImpl( + totalAssets: (json['total_assets'] as num?)?.toInt() ?? 0, + allocatedAssets: (json['allocated_assets'] as num?)?.toInt() ?? 0, + availableAssets: (json['available_assets'] as num?)?.toInt() ?? 0, + maintenanceAssets: (json['maintenance_assets'] as num?)?.toInt() ?? 0, + disposedAssets: (json['disposed_assets'] as num?)?.toInt() ?? 0, + warrantyExpiring: (json['warranty_expiring'] as num?)?.toInt() ?? 0, + ); + +Map _$$DashboardKpisImplToJson(_$DashboardKpisImpl instance) => + { + 'total_assets': instance.totalAssets, + 'allocated_assets': instance.allocatedAssets, + 'available_assets': instance.availableAssets, + 'maintenance_assets': instance.maintenanceAssets, + 'disposed_assets': instance.disposedAssets, + 'warranty_expiring': instance.warrantyExpiring, + }; + +_$ChartDataPointImpl _$$ChartDataPointImplFromJson(Map json) => + _$ChartDataPointImpl( + label: json['label'] as String, + value: (json['value'] as num).toDouble(), + ); + +Map _$$ChartDataPointImplToJson( + _$ChartDataPointImpl instance, +) => {'label': instance.label, 'value': instance.value}; + +_$DashboardChartsImpl _$$DashboardChartsImplFromJson( + Map json, +) => _$DashboardChartsImpl( + assetsByCategory: + (json['assets_by_category'] as List?) + ?.map((e) => ChartDataPoint.fromJson(e as Map)) + .toList() ?? + const [], + assetsByBranch: + (json['assets_by_branch'] as List?) + ?.map((e) => ChartDataPoint.fromJson(e as Map)) + .toList() ?? + const [], + allocationTrend: + (json['allocation_trend'] as List?) + ?.map((e) => ChartDataPoint.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$$DashboardChartsImplToJson( + _$DashboardChartsImpl instance, +) => { + 'assets_by_category': instance.assetsByCategory, + 'assets_by_branch': instance.assetsByBranch, + 'allocation_trend': instance.allocationTrend, +}; + +_$AuditLogModelImpl _$$AuditLogModelImplFromJson(Map json) => + _$AuditLogModelImpl( + id: json['id'] as String, + action: json['action'] as String, + module: json['module'] as String, + entityId: json['entity_id'] as String?, + userId: json['user_id'] as String, + userName: json['user_name'] as String?, + changes: json['changes'] as Map?, + ipAddress: json['ip_address'] as String?, + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + ); + +Map _$$AuditLogModelImplToJson(_$AuditLogModelImpl instance) => + { + 'id': instance.id, + 'action': instance.action, + 'module': instance.module, + 'entity_id': instance.entityId, + 'user_id': instance.userId, + 'user_name': instance.userName, + 'changes': instance.changes, + 'ip_address': instance.ipAddress, + 'createdAt': instance.createdAt?.toIso8601String(), + }; diff --git a/lib/shared/models/role_model.dart b/lib/shared/models/role_model.dart new file mode 100644 index 0000000..8ae70ab --- /dev/null +++ b/lib/shared/models/role_model.dart @@ -0,0 +1,36 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'role_model.freezed.dart'; +part 'role_model.g.dart'; + +@freezed +class RoleModel with _$RoleModel { + const factory RoleModel({ + required String id, + required String name, + required String slug, + String? description, + @Default([]) List permissions, + DateTime? createdAt, + DateTime? updatedAt, + }) = _RoleModel; + + factory RoleModel.fromJson(Map json) => _$RoleModelFromJson(json); +} + +@freezed +class PermissionModel with _$PermissionModel { + const factory PermissionModel({ + required String id, + required String module, + required String action, + String? description, + }) = _PermissionModel; + + factory PermissionModel.fromJson(Map json) => + _$PermissionModelFromJson(json); +} + +extension PermissionModelX on PermissionModel { + String get key => '$module.$action'; +} diff --git a/lib/shared/models/role_model.freezed.dart b/lib/shared/models/role_model.freezed.dart new file mode 100644 index 0000000..879f420 --- /dev/null +++ b/lib/shared/models/role_model.freezed.dart @@ -0,0 +1,536 @@ +// 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 'role_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +RoleModel _$RoleModelFromJson(Map json) { + return _RoleModel.fromJson(json); +} + +/// @nodoc +mixin _$RoleModel { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get slug => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + List get permissions => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this RoleModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RoleModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RoleModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RoleModelCopyWith<$Res> { + factory $RoleModelCopyWith(RoleModel value, $Res Function(RoleModel) then) = + _$RoleModelCopyWithImpl<$Res, RoleModel>; + @useResult + $Res call({ + String id, + String name, + String slug, + String? description, + List permissions, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$RoleModelCopyWithImpl<$Res, $Val extends RoleModel> + implements $RoleModelCopyWith<$Res> { + _$RoleModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RoleModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? slug = null, + Object? description = freezed, + Object? permissions = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + slug: null == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + permissions: null == permissions + ? _value.permissions + : permissions // ignore: cast_nullable_to_non_nullable + as List, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$RoleModelImplCopyWith<$Res> + implements $RoleModelCopyWith<$Res> { + factory _$$RoleModelImplCopyWith( + _$RoleModelImpl value, + $Res Function(_$RoleModelImpl) then, + ) = __$$RoleModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + String name, + String slug, + String? description, + List permissions, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class __$$RoleModelImplCopyWithImpl<$Res> + extends _$RoleModelCopyWithImpl<$Res, _$RoleModelImpl> + implements _$$RoleModelImplCopyWith<$Res> { + __$$RoleModelImplCopyWithImpl( + _$RoleModelImpl _value, + $Res Function(_$RoleModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of RoleModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? slug = null, + Object? description = freezed, + Object? permissions = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$RoleModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + slug: null == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + permissions: null == permissions + ? _value._permissions + : permissions // ignore: cast_nullable_to_non_nullable + as List, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$RoleModelImpl implements _RoleModel { + const _$RoleModelImpl({ + required this.id, + required this.name, + required this.slug, + this.description, + final List permissions = const [], + this.createdAt, + this.updatedAt, + }) : _permissions = permissions; + + factory _$RoleModelImpl.fromJson(Map json) => + _$$RoleModelImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String slug; + @override + final String? description; + final List _permissions; + @override + @JsonKey() + List get permissions { + if (_permissions is EqualUnmodifiableListView) return _permissions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_permissions); + } + + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'RoleModel(id: $id, name: $name, slug: $slug, description: $description, permissions: $permissions, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RoleModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.slug, slug) || other.slug == slug) && + (identical(other.description, description) || + other.description == description) && + const DeepCollectionEquality().equals( + other._permissions, + _permissions, + ) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + slug, + description, + const DeepCollectionEquality().hash(_permissions), + createdAt, + updatedAt, + ); + + /// Create a copy of RoleModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RoleModelImplCopyWith<_$RoleModelImpl> get copyWith => + __$$RoleModelImplCopyWithImpl<_$RoleModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RoleModelImplToJson(this); + } +} + +abstract class _RoleModel implements RoleModel { + const factory _RoleModel({ + required final String id, + required final String name, + required final String slug, + final String? description, + final List permissions, + final DateTime? createdAt, + final DateTime? updatedAt, + }) = _$RoleModelImpl; + + factory _RoleModel.fromJson(Map json) = + _$RoleModelImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String get slug; + @override + String? get description; + @override + List get permissions; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of RoleModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RoleModelImplCopyWith<_$RoleModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +PermissionModel _$PermissionModelFromJson(Map json) { + return _PermissionModel.fromJson(json); +} + +/// @nodoc +mixin _$PermissionModel { + String get id => throw _privateConstructorUsedError; + String get module => throw _privateConstructorUsedError; + String get action => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + + /// Serializes this PermissionModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PermissionModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PermissionModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PermissionModelCopyWith<$Res> { + factory $PermissionModelCopyWith( + PermissionModel value, + $Res Function(PermissionModel) then, + ) = _$PermissionModelCopyWithImpl<$Res, PermissionModel>; + @useResult + $Res call({String id, String module, String action, String? description}); +} + +/// @nodoc +class _$PermissionModelCopyWithImpl<$Res, $Val extends PermissionModel> + implements $PermissionModelCopyWith<$Res> { + _$PermissionModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PermissionModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? module = null, + Object? action = null, + Object? description = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + module: null == module + ? _value.module + : module // ignore: cast_nullable_to_non_nullable + as String, + action: null == action + ? _value.action + : action // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PermissionModelImplCopyWith<$Res> + implements $PermissionModelCopyWith<$Res> { + factory _$$PermissionModelImplCopyWith( + _$PermissionModelImpl value, + $Res Function(_$PermissionModelImpl) then, + ) = __$$PermissionModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, String module, String action, String? description}); +} + +/// @nodoc +class __$$PermissionModelImplCopyWithImpl<$Res> + extends _$PermissionModelCopyWithImpl<$Res, _$PermissionModelImpl> + implements _$$PermissionModelImplCopyWith<$Res> { + __$$PermissionModelImplCopyWithImpl( + _$PermissionModelImpl _value, + $Res Function(_$PermissionModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PermissionModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? module = null, + Object? action = null, + Object? description = freezed, + }) { + return _then( + _$PermissionModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + module: null == module + ? _value.module + : module // ignore: cast_nullable_to_non_nullable + as String, + action: null == action + ? _value.action + : action // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PermissionModelImpl implements _PermissionModel { + const _$PermissionModelImpl({ + required this.id, + required this.module, + required this.action, + this.description, + }); + + factory _$PermissionModelImpl.fromJson(Map json) => + _$$PermissionModelImplFromJson(json); + + @override + final String id; + @override + final String module; + @override + final String action; + @override + final String? description; + + @override + String toString() { + return 'PermissionModel(id: $id, module: $module, action: $action, description: $description)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PermissionModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.module, module) || other.module == module) && + (identical(other.action, action) || other.action == action) && + (identical(other.description, description) || + other.description == description)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, module, action, description); + + /// Create a copy of PermissionModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PermissionModelImplCopyWith<_$PermissionModelImpl> get copyWith => + __$$PermissionModelImplCopyWithImpl<_$PermissionModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$PermissionModelImplToJson(this); + } +} + +abstract class _PermissionModel implements PermissionModel { + const factory _PermissionModel({ + required final String id, + required final String module, + required final String action, + final String? description, + }) = _$PermissionModelImpl; + + factory _PermissionModel.fromJson(Map json) = + _$PermissionModelImpl.fromJson; + + @override + String get id; + @override + String get module; + @override + String get action; + @override + String? get description; + + /// Create a copy of PermissionModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PermissionModelImplCopyWith<_$PermissionModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/role_model.g.dart b/lib/shared/models/role_model.g.dart new file mode 100644 index 0000000..e321dfc --- /dev/null +++ b/lib/shared/models/role_model.g.dart @@ -0,0 +1,55 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'role_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$RoleModelImpl _$$RoleModelImplFromJson(Map json) => + _$RoleModelImpl( + id: json['id'] as String, + name: json['name'] as String, + slug: json['slug'] as String, + description: json['description'] as String?, + permissions: + (json['permissions'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + ); + +Map _$$RoleModelImplToJson(_$RoleModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'slug': instance.slug, + 'description': instance.description, + 'permissions': instance.permissions, + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), + }; + +_$PermissionModelImpl _$$PermissionModelImplFromJson( + Map json, +) => _$PermissionModelImpl( + id: json['id'] as String, + module: json['module'] as String, + action: json['action'] as String, + description: json['description'] as String?, +); + +Map _$$PermissionModelImplToJson( + _$PermissionModelImpl instance, +) => { + 'id': instance.id, + 'module': instance.module, + 'action': instance.action, + 'description': instance.description, +}; diff --git a/lib/shared/models/user_management_models.dart b/lib/shared/models/user_management_models.dart new file mode 100644 index 0000000..04dec36 --- /dev/null +++ b/lib/shared/models/user_management_models.dart @@ -0,0 +1,348 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user_management_models.freezed.dart'; +part 'user_management_models.g.dart'; + +Object? _readId(Map json, String key) => json[key]; + +String _idFromJson(Object? value) => value?.toString() ?? ''; + +String? _idFromJsonNullable(Object? value) => + value == null ? null : value.toString(); + +Object? _readEmployeeCode(Map json, String key) => + json['employee_code'] ?? json['employee_id']; + +Object? _readFullName(Map json, String key) { + final fullName = json['full_name'] ?? json['name']; + if (fullName != null) return fullName; + final first = json['first_name'] as String?; + final last = json['last_name'] as String?; + if (first != null || last != null) { + return [first, last].where((e) => e != null && e.isNotEmpty).join(' '); + } + return null; +} + +Object? _readRoleName(Map json, String key) { + final roleName = json['role_name']; + if (roleName is String) return roleName; + final role = json['role']; + if (role is Map) return role['name']; + if (role is String) return role; + return null; +} + +Object? _readDepartmentName(Map json, String key) { + final name = json['department_name']; + if (name is String) return name; + final department = json['department']; + if (department is Map) return department['name']; + return null; +} + +Object? _readPlantName(Map json, String key) { + final name = json['plant_name']; + if (name is String) return name; + final plant = json['plant']; + if (plant is Map) return plant['name']; + if (plant is String) return plant; + return null; +} + +Object? _readRoleId(Map json, String key) { + final roleId = json['role_id']; + if (roleId != null) return roleId; + final role = json['role']; + if (role is Map) return role['id']; + return null; +} + +@freezed +class UserSummaryModel with _$UserSummaryModel { + const factory UserSummaryModel({ + @JsonKey(name: 'total_users') @Default(0) int totalUsers, + @JsonKey(name: 'active_users') @Default(0) int activeUsers, + @JsonKey(name: 'inactive_users') @Default(0) int inactiveUsers, + @JsonKey(name: 'locked_users') @Default(0) int lockedUsers, + @JsonKey(name: 'roles_count') @Default(0) int rolesCount, + }) = _UserSummaryModel; + + factory UserSummaryModel.fromJson(Map json) => + _$UserSummaryModelFromJson(json); +} + +@freezed +class FilterOptionModel with _$FilterOptionModel { + const factory FilterOptionModel({ + @JsonKey(fromJson: _idFromJson) required String id, + required String name, + String? slug, + }) = _FilterOptionModel; + + factory FilterOptionModel.fromJson(Map json) => + _$FilterOptionModelFromJson(json); +} + +@freezed +class UserFiltersModel with _$UserFiltersModel { + const factory UserFiltersModel({ + @Default([]) List roles, + @Default([]) List departments, + @Default([]) List statuses, + }) = _UserFiltersModel; + + factory UserFiltersModel.fromJson(Map json) => + _$UserFiltersModelFromJson(json); +} + +@freezed +class ManagedUserModel with _$ManagedUserModel { + const factory ManagedUserModel({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) required String id, + @JsonKey(name: 'employee_code', readValue: _readEmployeeCode) + required String employeeCode, + @JsonKey(name: 'full_name', readValue: _readFullName) required String fullName, + @JsonKey(name: 'first_name') String? firstName, + @JsonKey(name: 'last_name') String? lastName, + required String email, + @Default('') String mobile, + @JsonKey(fromJson: _idFromJsonNullable, name: 'role_id', readValue: _readRoleId) + String? roleId, + @JsonKey(name: 'role_name', readValue: _readRoleName) String? roleName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'department_id') + String? departmentId, + @JsonKey(name: 'department_name', readValue: _readDepartmentName) + String? departmentName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'designation_id') + String? designationId, + @JsonKey(name: 'designation_name') String? designationName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'plant_id') String? plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) String? plantName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'reporting_to') + String? reportingTo, + @JsonKey(name: 'reporting_to_name') String? reportingToName, + @JsonKey(name: 'last_login_at') DateTime? lastLoginAt, + String? initials, + @Default('active') String status, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + @JsonKey(name: 'avatar_url') String? avatarUrl, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, + }) = _ManagedUserModel; + + factory ManagedUserModel.fromJson(Map json) => + _$ManagedUserModelFromJson(json); +} + +extension ManagedUserModelX on ManagedUserModel { + String get displayName => fullName; + String get roleLabel => roleName ?? '—'; + String get departmentLabel => departmentName ?? '—'; + String get plantLabel => plantName ?? '—'; + + String get initialsDisplay { + if (initials != null && initials!.trim().isNotEmpty) { + return initials!.trim().toUpperCase(); + } + final parts = fullName.trim().split(RegExp(r'\s+')); + if (parts.length >= 2) { + return '${parts.first[0]}${parts[1][0]}'.toUpperCase(); + } + return fullName.isNotEmpty ? fullName[0].toUpperCase() : 'U'; + } +} + +@freezed +class CreateUserRequest with _$CreateUserRequest { + const factory CreateUserRequest({ + @JsonKey(name: 'employee_code') required String employeeCode, + @JsonKey(name: 'full_name') required String fullName, + required String email, + required String password, + String? mobile, + @JsonKey(name: 'role_id') required int roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'designation_id') int? designationId, + @JsonKey(name: 'plant_id') int? plantId, + @JsonKey(name: 'reporting_to') int? reportingTo, + @Default('active') String status, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + }) = _CreateUserRequest; + + factory CreateUserRequest.fromJson(Map json) => + _$CreateUserRequestFromJson(json); +} + +@freezed +class UpdateUserRequest with _$UpdateUserRequest { + const factory UpdateUserRequest({ + @JsonKey(name: 'employee_code') String? employeeCode, + @JsonKey(name: 'full_name') String? fullName, + String? email, + String? password, + String? mobile, + @JsonKey(name: 'role_id') int? roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'designation_id') int? designationId, + @JsonKey(name: 'plant_id') int? plantId, + @JsonKey(name: 'reporting_to') int? reportingTo, + String? status, + @JsonKey(name: 'is_active') bool? isActive, + }) = _UpdateUserRequest; + + factory UpdateUserRequest.fromJson(Map json) => + _$UpdateUserRequestFromJson(json); +} + +@freezed +class UserListQuery with _$UserListQuery { + const factory UserListQuery({ + @Default(1) int page, + @Default(20) int limit, + String? search, + String? status, + @JsonKey(name: 'role_id') int? roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'sort_by') String? sortBy, + @JsonKey(name: 'sort_order') @Default('asc') String sortOrder, + @JsonKey(name: 'is_active') bool? isActive, + }) = _UserListQuery; +} + +@freezed +class RoleCardModel with _$RoleCardModel { + const factory RoleCardModel({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) required String id, + required String name, + String? description, + @JsonKey(name: 'user_count') @Default(0) int userCount, + @JsonKey(name: 'permission_count') @Default(0) int permissionCount, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + }) = _RoleCardModel; + + factory RoleCardModel.fromJson(Map json) => + _$RoleCardModelFromJson(json); +} + +@freezed +class CreateRoleRequest with _$CreateRoleRequest { + const factory CreateRoleRequest({ + required String name, + String? description, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + }) = _CreateRoleRequest; + + factory CreateRoleRequest.fromJson(Map json) => + _$CreateRoleRequestFromJson(json); +} + +@freezed +class UpdateRoleRequest with _$UpdateRoleRequest { + const factory UpdateRoleRequest({ + String? name, + String? description, + @JsonKey(name: 'is_active') bool? isActive, + }) = _UpdateRoleRequest; + + factory UpdateRoleRequest.fromJson(Map json) => + _$UpdateRoleRequestFromJson(json); +} + +@freezed +class PermissionCatalogModel with _$PermissionCatalogModel { + const factory PermissionCatalogModel({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) required String id, + required String module, + required String action, + String? description, + @JsonKey(name: 'module_id', fromJson: _idFromJsonNullable) + String? moduleId, + }) = _PermissionCatalogModel; + + factory PermissionCatalogModel.fromJson(Map json) => + _$PermissionCatalogModelFromJson(json); +} + +@freezed +class PermissionMatrixActions with _$PermissionMatrixActions { + const factory PermissionMatrixActions({ + @Default(false) bool view, + @Default(false) bool edit, + @Default(false) bool approve, + @Default(false) bool export, + }) = _PermissionMatrixActions; + + factory PermissionMatrixActions.fromJson(Map json) => + _$PermissionMatrixActionsFromJson(json); +} + +@freezed +class PermissionMatrixRow with _$PermissionMatrixRow { + const factory PermissionMatrixRow({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) required String moduleId, + required String module, + required PermissionMatrixActions actions, + }) = _PermissionMatrixRow; + + factory PermissionMatrixRow.fromJson(Map json) => + _$PermissionMatrixRowFromJson(json); +} + +@freezed +class PermissionMatrixModel with _$PermissionMatrixModel { + const factory PermissionMatrixModel({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) required String roleId, + required String roleName, + @Default([]) List matrix, + }) = _PermissionMatrixModel; + + factory PermissionMatrixModel.fromJson(Map json) => + _$PermissionMatrixModelFromJson(json); +} + +@freezed +class PermissionMatrixSaveRequest with _$PermissionMatrixSaveRequest { + const factory PermissionMatrixSaveRequest({ + required List matrix, + }) = _PermissionMatrixSaveRequest; + + factory PermissionMatrixSaveRequest.fromJson(Map json) => + _$PermissionMatrixSaveRequestFromJson(json); +} + +extension PermissionMatrixSaveRequestX on PermissionMatrixSaveRequest { + Map toApiJson() => { + 'matrix': matrix + .map( + (row) => { + 'module_id': int.tryParse(row.moduleId) ?? row.moduleId, + 'actions': row.actions.toJson(), + }, + ) + .toList(), + }; +} + +@freezed +class PermissionMatrixSaveRow with _$PermissionMatrixSaveRow { + const factory PermissionMatrixSaveRow({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) required String moduleId, + required PermissionMatrixActions actions, + }) = _PermissionMatrixSaveRow; + + factory PermissionMatrixSaveRow.fromJson(Map json) => + _$PermissionMatrixSaveRowFromJson(json); +} + +@freezed +class UpdateProfileRequest with _$UpdateProfileRequest { + const factory UpdateProfileRequest({ + @JsonKey(name: 'full_name') String? fullName, + String? mobile, + @JsonKey(name: 'avatar_url') String? avatarUrl, + }) = _UpdateProfileRequest; + + factory UpdateProfileRequest.fromJson(Map json) => + _$UpdateProfileRequestFromJson(json); +} diff --git a/lib/shared/models/user_management_models.freezed.dart b/lib/shared/models/user_management_models.freezed.dart new file mode 100644 index 0000000..3d046cd --- /dev/null +++ b/lib/shared/models/user_management_models.freezed.dart @@ -0,0 +1,4980 @@ +// 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 'user_management_models.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +UserSummaryModel _$UserSummaryModelFromJson(Map json) { + return _UserSummaryModel.fromJson(json); +} + +/// @nodoc +mixin _$UserSummaryModel { + @JsonKey(name: 'total_users') + int get totalUsers => throw _privateConstructorUsedError; + @JsonKey(name: 'active_users') + int get activeUsers => throw _privateConstructorUsedError; + @JsonKey(name: 'inactive_users') + int get inactiveUsers => throw _privateConstructorUsedError; + @JsonKey(name: 'locked_users') + int get lockedUsers => throw _privateConstructorUsedError; + @JsonKey(name: 'roles_count') + int get rolesCount => throw _privateConstructorUsedError; + + /// Serializes this UserSummaryModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UserSummaryModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserSummaryModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserSummaryModelCopyWith<$Res> { + factory $UserSummaryModelCopyWith( + UserSummaryModel value, + $Res Function(UserSummaryModel) then, + ) = _$UserSummaryModelCopyWithImpl<$Res, UserSummaryModel>; + @useResult + $Res call({ + @JsonKey(name: 'total_users') int totalUsers, + @JsonKey(name: 'active_users') int activeUsers, + @JsonKey(name: 'inactive_users') int inactiveUsers, + @JsonKey(name: 'locked_users') int lockedUsers, + @JsonKey(name: 'roles_count') int rolesCount, + }); +} + +/// @nodoc +class _$UserSummaryModelCopyWithImpl<$Res, $Val extends UserSummaryModel> + implements $UserSummaryModelCopyWith<$Res> { + _$UserSummaryModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserSummaryModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totalUsers = null, + Object? activeUsers = null, + Object? inactiveUsers = null, + Object? lockedUsers = null, + Object? rolesCount = null, + }) { + return _then( + _value.copyWith( + totalUsers: null == totalUsers + ? _value.totalUsers + : totalUsers // ignore: cast_nullable_to_non_nullable + as int, + activeUsers: null == activeUsers + ? _value.activeUsers + : activeUsers // ignore: cast_nullable_to_non_nullable + as int, + inactiveUsers: null == inactiveUsers + ? _value.inactiveUsers + : inactiveUsers // ignore: cast_nullable_to_non_nullable + as int, + lockedUsers: null == lockedUsers + ? _value.lockedUsers + : lockedUsers // ignore: cast_nullable_to_non_nullable + as int, + rolesCount: null == rolesCount + ? _value.rolesCount + : rolesCount // ignore: cast_nullable_to_non_nullable + as int, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UserSummaryModelImplCopyWith<$Res> + implements $UserSummaryModelCopyWith<$Res> { + factory _$$UserSummaryModelImplCopyWith( + _$UserSummaryModelImpl value, + $Res Function(_$UserSummaryModelImpl) then, + ) = __$$UserSummaryModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'total_users') int totalUsers, + @JsonKey(name: 'active_users') int activeUsers, + @JsonKey(name: 'inactive_users') int inactiveUsers, + @JsonKey(name: 'locked_users') int lockedUsers, + @JsonKey(name: 'roles_count') int rolesCount, + }); +} + +/// @nodoc +class __$$UserSummaryModelImplCopyWithImpl<$Res> + extends _$UserSummaryModelCopyWithImpl<$Res, _$UserSummaryModelImpl> + implements _$$UserSummaryModelImplCopyWith<$Res> { + __$$UserSummaryModelImplCopyWithImpl( + _$UserSummaryModelImpl _value, + $Res Function(_$UserSummaryModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UserSummaryModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? totalUsers = null, + Object? activeUsers = null, + Object? inactiveUsers = null, + Object? lockedUsers = null, + Object? rolesCount = null, + }) { + return _then( + _$UserSummaryModelImpl( + totalUsers: null == totalUsers + ? _value.totalUsers + : totalUsers // ignore: cast_nullable_to_non_nullable + as int, + activeUsers: null == activeUsers + ? _value.activeUsers + : activeUsers // ignore: cast_nullable_to_non_nullable + as int, + inactiveUsers: null == inactiveUsers + ? _value.inactiveUsers + : inactiveUsers // ignore: cast_nullable_to_non_nullable + as int, + lockedUsers: null == lockedUsers + ? _value.lockedUsers + : lockedUsers // ignore: cast_nullable_to_non_nullable + as int, + rolesCount: null == rolesCount + ? _value.rolesCount + : rolesCount // ignore: cast_nullable_to_non_nullable + as int, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserSummaryModelImpl implements _UserSummaryModel { + const _$UserSummaryModelImpl({ + @JsonKey(name: 'total_users') this.totalUsers = 0, + @JsonKey(name: 'active_users') this.activeUsers = 0, + @JsonKey(name: 'inactive_users') this.inactiveUsers = 0, + @JsonKey(name: 'locked_users') this.lockedUsers = 0, + @JsonKey(name: 'roles_count') this.rolesCount = 0, + }); + + factory _$UserSummaryModelImpl.fromJson(Map json) => + _$$UserSummaryModelImplFromJson(json); + + @override + @JsonKey(name: 'total_users') + final int totalUsers; + @override + @JsonKey(name: 'active_users') + final int activeUsers; + @override + @JsonKey(name: 'inactive_users') + final int inactiveUsers; + @override + @JsonKey(name: 'locked_users') + final int lockedUsers; + @override + @JsonKey(name: 'roles_count') + final int rolesCount; + + @override + String toString() { + return 'UserSummaryModel(totalUsers: $totalUsers, activeUsers: $activeUsers, inactiveUsers: $inactiveUsers, lockedUsers: $lockedUsers, rolesCount: $rolesCount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserSummaryModelImpl && + (identical(other.totalUsers, totalUsers) || + other.totalUsers == totalUsers) && + (identical(other.activeUsers, activeUsers) || + other.activeUsers == activeUsers) && + (identical(other.inactiveUsers, inactiveUsers) || + other.inactiveUsers == inactiveUsers) && + (identical(other.lockedUsers, lockedUsers) || + other.lockedUsers == lockedUsers) && + (identical(other.rolesCount, rolesCount) || + other.rolesCount == rolesCount)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + totalUsers, + activeUsers, + inactiveUsers, + lockedUsers, + rolesCount, + ); + + /// Create a copy of UserSummaryModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserSummaryModelImplCopyWith<_$UserSummaryModelImpl> get copyWith => + __$$UserSummaryModelImplCopyWithImpl<_$UserSummaryModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$UserSummaryModelImplToJson(this); + } +} + +abstract class _UserSummaryModel implements UserSummaryModel { + const factory _UserSummaryModel({ + @JsonKey(name: 'total_users') final int totalUsers, + @JsonKey(name: 'active_users') final int activeUsers, + @JsonKey(name: 'inactive_users') final int inactiveUsers, + @JsonKey(name: 'locked_users') final int lockedUsers, + @JsonKey(name: 'roles_count') final int rolesCount, + }) = _$UserSummaryModelImpl; + + factory _UserSummaryModel.fromJson(Map json) = + _$UserSummaryModelImpl.fromJson; + + @override + @JsonKey(name: 'total_users') + int get totalUsers; + @override + @JsonKey(name: 'active_users') + int get activeUsers; + @override + @JsonKey(name: 'inactive_users') + int get inactiveUsers; + @override + @JsonKey(name: 'locked_users') + int get lockedUsers; + @override + @JsonKey(name: 'roles_count') + int get rolesCount; + + /// Create a copy of UserSummaryModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserSummaryModelImplCopyWith<_$UserSummaryModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +FilterOptionModel _$FilterOptionModelFromJson(Map json) { + return _FilterOptionModel.fromJson(json); +} + +/// @nodoc +mixin _$FilterOptionModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String? get slug => throw _privateConstructorUsedError; + + /// Serializes this FilterOptionModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of FilterOptionModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $FilterOptionModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FilterOptionModelCopyWith<$Res> { + factory $FilterOptionModelCopyWith( + FilterOptionModel value, + $Res Function(FilterOptionModel) then, + ) = _$FilterOptionModelCopyWithImpl<$Res, FilterOptionModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + String name, + String? slug, + }); +} + +/// @nodoc +class _$FilterOptionModelCopyWithImpl<$Res, $Val extends FilterOptionModel> + implements $FilterOptionModelCopyWith<$Res> { + _$FilterOptionModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of FilterOptionModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? id = null, Object? name = null, Object? slug = freezed}) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + slug: freezed == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$FilterOptionModelImplCopyWith<$Res> + implements $FilterOptionModelCopyWith<$Res> { + factory _$$FilterOptionModelImplCopyWith( + _$FilterOptionModelImpl value, + $Res Function(_$FilterOptionModelImpl) then, + ) = __$$FilterOptionModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + String name, + String? slug, + }); +} + +/// @nodoc +class __$$FilterOptionModelImplCopyWithImpl<$Res> + extends _$FilterOptionModelCopyWithImpl<$Res, _$FilterOptionModelImpl> + implements _$$FilterOptionModelImplCopyWith<$Res> { + __$$FilterOptionModelImplCopyWithImpl( + _$FilterOptionModelImpl _value, + $Res Function(_$FilterOptionModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of FilterOptionModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? id = null, Object? name = null, Object? slug = freezed}) { + return _then( + _$FilterOptionModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + slug: freezed == slug + ? _value.slug + : slug // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$FilterOptionModelImpl implements _FilterOptionModel { + const _$FilterOptionModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + required this.name, + this.slug, + }); + + factory _$FilterOptionModelImpl.fromJson(Map json) => + _$$FilterOptionModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + final String name; + @override + final String? slug; + + @override + String toString() { + return 'FilterOptionModel(id: $id, name: $name, slug: $slug)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FilterOptionModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.slug, slug) || other.slug == slug)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, name, slug); + + /// Create a copy of FilterOptionModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$FilterOptionModelImplCopyWith<_$FilterOptionModelImpl> get copyWith => + __$$FilterOptionModelImplCopyWithImpl<_$FilterOptionModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$FilterOptionModelImplToJson(this); + } +} + +abstract class _FilterOptionModel implements FilterOptionModel { + const factory _FilterOptionModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + required final String name, + final String? slug, + }) = _$FilterOptionModelImpl; + + factory _FilterOptionModel.fromJson(Map json) = + _$FilterOptionModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + String get name; + @override + String? get slug; + + /// Create a copy of FilterOptionModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$FilterOptionModelImplCopyWith<_$FilterOptionModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +UserFiltersModel _$UserFiltersModelFromJson(Map json) { + return _UserFiltersModel.fromJson(json); +} + +/// @nodoc +mixin _$UserFiltersModel { + List get roles => throw _privateConstructorUsedError; + List get departments => throw _privateConstructorUsedError; + List get statuses => throw _privateConstructorUsedError; + + /// Serializes this UserFiltersModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UserFiltersModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserFiltersModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserFiltersModelCopyWith<$Res> { + factory $UserFiltersModelCopyWith( + UserFiltersModel value, + $Res Function(UserFiltersModel) then, + ) = _$UserFiltersModelCopyWithImpl<$Res, UserFiltersModel>; + @useResult + $Res call({ + List roles, + List departments, + List statuses, + }); +} + +/// @nodoc +class _$UserFiltersModelCopyWithImpl<$Res, $Val extends UserFiltersModel> + implements $UserFiltersModelCopyWith<$Res> { + _$UserFiltersModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserFiltersModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? roles = null, + Object? departments = null, + Object? statuses = null, + }) { + return _then( + _value.copyWith( + roles: null == roles + ? _value.roles + : roles // ignore: cast_nullable_to_non_nullable + as List, + departments: null == departments + ? _value.departments + : departments // ignore: cast_nullable_to_non_nullable + as List, + statuses: null == statuses + ? _value.statuses + : statuses // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UserFiltersModelImplCopyWith<$Res> + implements $UserFiltersModelCopyWith<$Res> { + factory _$$UserFiltersModelImplCopyWith( + _$UserFiltersModelImpl value, + $Res Function(_$UserFiltersModelImpl) then, + ) = __$$UserFiltersModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + List roles, + List departments, + List statuses, + }); +} + +/// @nodoc +class __$$UserFiltersModelImplCopyWithImpl<$Res> + extends _$UserFiltersModelCopyWithImpl<$Res, _$UserFiltersModelImpl> + implements _$$UserFiltersModelImplCopyWith<$Res> { + __$$UserFiltersModelImplCopyWithImpl( + _$UserFiltersModelImpl _value, + $Res Function(_$UserFiltersModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UserFiltersModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? roles = null, + Object? departments = null, + Object? statuses = null, + }) { + return _then( + _$UserFiltersModelImpl( + roles: null == roles + ? _value._roles + : roles // ignore: cast_nullable_to_non_nullable + as List, + departments: null == departments + ? _value._departments + : departments // ignore: cast_nullable_to_non_nullable + as List, + statuses: null == statuses + ? _value._statuses + : statuses // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserFiltersModelImpl implements _UserFiltersModel { + const _$UserFiltersModelImpl({ + final List roles = const [], + final List departments = const [], + final List statuses = const [], + }) : _roles = roles, + _departments = departments, + _statuses = statuses; + + factory _$UserFiltersModelImpl.fromJson(Map json) => + _$$UserFiltersModelImplFromJson(json); + + final List _roles; + @override + @JsonKey() + List get roles { + if (_roles is EqualUnmodifiableListView) return _roles; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_roles); + } + + final List _departments; + @override + @JsonKey() + List get departments { + if (_departments is EqualUnmodifiableListView) return _departments; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_departments); + } + + final List _statuses; + @override + @JsonKey() + List get statuses { + if (_statuses is EqualUnmodifiableListView) return _statuses; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_statuses); + } + + @override + String toString() { + return 'UserFiltersModel(roles: $roles, departments: $departments, statuses: $statuses)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserFiltersModelImpl && + const DeepCollectionEquality().equals(other._roles, _roles) && + const DeepCollectionEquality().equals( + other._departments, + _departments, + ) && + const DeepCollectionEquality().equals(other._statuses, _statuses)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_roles), + const DeepCollectionEquality().hash(_departments), + const DeepCollectionEquality().hash(_statuses), + ); + + /// Create a copy of UserFiltersModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserFiltersModelImplCopyWith<_$UserFiltersModelImpl> get copyWith => + __$$UserFiltersModelImplCopyWithImpl<_$UserFiltersModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$UserFiltersModelImplToJson(this); + } +} + +abstract class _UserFiltersModel implements UserFiltersModel { + const factory _UserFiltersModel({ + final List roles, + final List departments, + final List statuses, + }) = _$UserFiltersModelImpl; + + factory _UserFiltersModel.fromJson(Map json) = + _$UserFiltersModelImpl.fromJson; + + @override + List get roles; + @override + List get departments; + @override + List get statuses; + + /// Create a copy of UserFiltersModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserFiltersModelImplCopyWith<_$UserFiltersModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ManagedUserModel _$ManagedUserModelFromJson(Map json) { + return _ManagedUserModel.fromJson(json); +} + +/// @nodoc +mixin _$ManagedUserModel { + @JsonKey(fromJson: _idFromJson, readValue: _readId) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'employee_code', readValue: _readEmployeeCode) + String get employeeCode => throw _privateConstructorUsedError; + @JsonKey(name: 'full_name', readValue: _readFullName) + String get fullName => throw _privateConstructorUsedError; + @JsonKey(name: 'first_name') + String? get firstName => throw _privateConstructorUsedError; + @JsonKey(name: 'last_name') + String? get lastName => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + String get mobile => throw _privateConstructorUsedError; + @JsonKey( + fromJson: _idFromJsonNullable, + name: 'role_id', + readValue: _readRoleId, + ) + String? get roleId => throw _privateConstructorUsedError; + @JsonKey(name: 'role_name', readValue: _readRoleName) + String? get roleName => throw _privateConstructorUsedError; + @JsonKey(fromJson: _idFromJsonNullable, name: 'department_id') + String? get departmentId => throw _privateConstructorUsedError; + @JsonKey(name: 'department_name', readValue: _readDepartmentName) + String? get departmentName => throw _privateConstructorUsedError; + @JsonKey(fromJson: _idFromJsonNullable, name: 'designation_id') + String? get designationId => throw _privateConstructorUsedError; + @JsonKey(name: 'designation_name') + String? get designationName => throw _privateConstructorUsedError; + @JsonKey(fromJson: _idFromJsonNullable, name: 'plant_id') + String? get plantId => throw _privateConstructorUsedError; + @JsonKey(name: 'plant_name', readValue: _readPlantName) + String? get plantName => throw _privateConstructorUsedError; + @JsonKey(fromJson: _idFromJsonNullable, name: 'reporting_to') + String? get reportingTo => throw _privateConstructorUsedError; + @JsonKey(name: 'reporting_to_name') + String? get reportingToName => throw _privateConstructorUsedError; + @JsonKey(name: 'last_login_at') + DateTime? get lastLoginAt => throw _privateConstructorUsedError; + String? get initials => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + @JsonKey(name: 'avatar_url') + String? get avatarUrl => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at') + DateTime? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at') + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this ManagedUserModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ManagedUserModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ManagedUserModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ManagedUserModelCopyWith<$Res> { + factory $ManagedUserModelCopyWith( + ManagedUserModel value, + $Res Function(ManagedUserModel) then, + ) = _$ManagedUserModelCopyWithImpl<$Res, ManagedUserModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) String id, + @JsonKey(name: 'employee_code', readValue: _readEmployeeCode) + String employeeCode, + @JsonKey(name: 'full_name', readValue: _readFullName) String fullName, + @JsonKey(name: 'first_name') String? firstName, + @JsonKey(name: 'last_name') String? lastName, + String email, + String mobile, + @JsonKey( + fromJson: _idFromJsonNullable, + name: 'role_id', + readValue: _readRoleId, + ) + String? roleId, + @JsonKey(name: 'role_name', readValue: _readRoleName) String? roleName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'department_id') + String? departmentId, + @JsonKey(name: 'department_name', readValue: _readDepartmentName) + String? departmentName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'designation_id') + String? designationId, + @JsonKey(name: 'designation_name') String? designationName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'plant_id') String? plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) String? plantName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'reporting_to') + String? reportingTo, + @JsonKey(name: 'reporting_to_name') String? reportingToName, + @JsonKey(name: 'last_login_at') DateTime? lastLoginAt, + String? initials, + String status, + @JsonKey(name: 'is_active') bool isActive, + @JsonKey(name: 'avatar_url') String? avatarUrl, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, + }); +} + +/// @nodoc +class _$ManagedUserModelCopyWithImpl<$Res, $Val extends ManagedUserModel> + implements $ManagedUserModelCopyWith<$Res> { + _$ManagedUserModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ManagedUserModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? employeeCode = null, + Object? fullName = null, + Object? firstName = freezed, + Object? lastName = freezed, + Object? email = null, + Object? mobile = null, + Object? roleId = freezed, + Object? roleName = freezed, + Object? departmentId = freezed, + Object? departmentName = freezed, + Object? designationId = freezed, + Object? designationName = freezed, + Object? plantId = freezed, + Object? plantName = freezed, + Object? reportingTo = freezed, + Object? reportingToName = freezed, + Object? lastLoginAt = freezed, + Object? initials = freezed, + Object? status = null, + Object? isActive = null, + Object? avatarUrl = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + employeeCode: null == employeeCode + ? _value.employeeCode + : employeeCode // ignore: cast_nullable_to_non_nullable + as String, + fullName: null == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String, + firstName: freezed == firstName + ? _value.firstName + : firstName // ignore: cast_nullable_to_non_nullable + as String?, + lastName: freezed == lastName + ? _value.lastName + : lastName // ignore: cast_nullable_to_non_nullable + as String?, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + mobile: null == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String, + roleId: freezed == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as String?, + roleName: freezed == roleName + ? _value.roleName + : roleName // ignore: cast_nullable_to_non_nullable + as String?, + departmentId: freezed == departmentId + ? _value.departmentId + : departmentId // ignore: cast_nullable_to_non_nullable + as String?, + departmentName: freezed == departmentName + ? _value.departmentName + : departmentName // ignore: cast_nullable_to_non_nullable + as String?, + designationId: freezed == designationId + ? _value.designationId + : designationId // ignore: cast_nullable_to_non_nullable + as String?, + designationName: freezed == designationName + ? _value.designationName + : designationName // ignore: cast_nullable_to_non_nullable + as String?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as String?, + plantName: freezed == plantName + ? _value.plantName + : plantName // ignore: cast_nullable_to_non_nullable + as String?, + reportingTo: freezed == reportingTo + ? _value.reportingTo + : reportingTo // ignore: cast_nullable_to_non_nullable + as String?, + reportingToName: freezed == reportingToName + ? _value.reportingToName + : reportingToName // ignore: cast_nullable_to_non_nullable + as String?, + lastLoginAt: freezed == lastLoginAt + ? _value.lastLoginAt + : lastLoginAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + initials: freezed == initials + ? _value.initials + : initials // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + avatarUrl: freezed == avatarUrl + ? _value.avatarUrl + : avatarUrl // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ManagedUserModelImplCopyWith<$Res> + implements $ManagedUserModelCopyWith<$Res> { + factory _$$ManagedUserModelImplCopyWith( + _$ManagedUserModelImpl value, + $Res Function(_$ManagedUserModelImpl) then, + ) = __$$ManagedUserModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) String id, + @JsonKey(name: 'employee_code', readValue: _readEmployeeCode) + String employeeCode, + @JsonKey(name: 'full_name', readValue: _readFullName) String fullName, + @JsonKey(name: 'first_name') String? firstName, + @JsonKey(name: 'last_name') String? lastName, + String email, + String mobile, + @JsonKey( + fromJson: _idFromJsonNullable, + name: 'role_id', + readValue: _readRoleId, + ) + String? roleId, + @JsonKey(name: 'role_name', readValue: _readRoleName) String? roleName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'department_id') + String? departmentId, + @JsonKey(name: 'department_name', readValue: _readDepartmentName) + String? departmentName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'designation_id') + String? designationId, + @JsonKey(name: 'designation_name') String? designationName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'plant_id') String? plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) String? plantName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'reporting_to') + String? reportingTo, + @JsonKey(name: 'reporting_to_name') String? reportingToName, + @JsonKey(name: 'last_login_at') DateTime? lastLoginAt, + String? initials, + String status, + @JsonKey(name: 'is_active') bool isActive, + @JsonKey(name: 'avatar_url') String? avatarUrl, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, + }); +} + +/// @nodoc +class __$$ManagedUserModelImplCopyWithImpl<$Res> + extends _$ManagedUserModelCopyWithImpl<$Res, _$ManagedUserModelImpl> + implements _$$ManagedUserModelImplCopyWith<$Res> { + __$$ManagedUserModelImplCopyWithImpl( + _$ManagedUserModelImpl _value, + $Res Function(_$ManagedUserModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ManagedUserModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? employeeCode = null, + Object? fullName = null, + Object? firstName = freezed, + Object? lastName = freezed, + Object? email = null, + Object? mobile = null, + Object? roleId = freezed, + Object? roleName = freezed, + Object? departmentId = freezed, + Object? departmentName = freezed, + Object? designationId = freezed, + Object? designationName = freezed, + Object? plantId = freezed, + Object? plantName = freezed, + Object? reportingTo = freezed, + Object? reportingToName = freezed, + Object? lastLoginAt = freezed, + Object? initials = freezed, + Object? status = null, + Object? isActive = null, + Object? avatarUrl = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$ManagedUserModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + employeeCode: null == employeeCode + ? _value.employeeCode + : employeeCode // ignore: cast_nullable_to_non_nullable + as String, + fullName: null == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String, + firstName: freezed == firstName + ? _value.firstName + : firstName // ignore: cast_nullable_to_non_nullable + as String?, + lastName: freezed == lastName + ? _value.lastName + : lastName // ignore: cast_nullable_to_non_nullable + as String?, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + mobile: null == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String, + roleId: freezed == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as String?, + roleName: freezed == roleName + ? _value.roleName + : roleName // ignore: cast_nullable_to_non_nullable + as String?, + departmentId: freezed == departmentId + ? _value.departmentId + : departmentId // ignore: cast_nullable_to_non_nullable + as String?, + departmentName: freezed == departmentName + ? _value.departmentName + : departmentName // ignore: cast_nullable_to_non_nullable + as String?, + designationId: freezed == designationId + ? _value.designationId + : designationId // ignore: cast_nullable_to_non_nullable + as String?, + designationName: freezed == designationName + ? _value.designationName + : designationName // ignore: cast_nullable_to_non_nullable + as String?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as String?, + plantName: freezed == plantName + ? _value.plantName + : plantName // ignore: cast_nullable_to_non_nullable + as String?, + reportingTo: freezed == reportingTo + ? _value.reportingTo + : reportingTo // ignore: cast_nullable_to_non_nullable + as String?, + reportingToName: freezed == reportingToName + ? _value.reportingToName + : reportingToName // ignore: cast_nullable_to_non_nullable + as String?, + lastLoginAt: freezed == lastLoginAt + ? _value.lastLoginAt + : lastLoginAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + initials: freezed == initials + ? _value.initials + : initials // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + avatarUrl: freezed == avatarUrl + ? _value.avatarUrl + : avatarUrl // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ManagedUserModelImpl implements _ManagedUserModel { + const _$ManagedUserModelImpl({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) required this.id, + @JsonKey(name: 'employee_code', readValue: _readEmployeeCode) + required this.employeeCode, + @JsonKey(name: 'full_name', readValue: _readFullName) + required this.fullName, + @JsonKey(name: 'first_name') this.firstName, + @JsonKey(name: 'last_name') this.lastName, + required this.email, + this.mobile = '', + @JsonKey( + fromJson: _idFromJsonNullable, + name: 'role_id', + readValue: _readRoleId, + ) + this.roleId, + @JsonKey(name: 'role_name', readValue: _readRoleName) this.roleName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'department_id') + this.departmentId, + @JsonKey(name: 'department_name', readValue: _readDepartmentName) + this.departmentName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'designation_id') + this.designationId, + @JsonKey(name: 'designation_name') this.designationName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'plant_id') this.plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) this.plantName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'reporting_to') + this.reportingTo, + @JsonKey(name: 'reporting_to_name') this.reportingToName, + @JsonKey(name: 'last_login_at') this.lastLoginAt, + this.initials, + this.status = 'active', + @JsonKey(name: 'is_active') this.isActive = true, + @JsonKey(name: 'avatar_url') this.avatarUrl, + @JsonKey(name: 'created_at') this.createdAt, + @JsonKey(name: 'updated_at') this.updatedAt, + }); + + factory _$ManagedUserModelImpl.fromJson(Map json) => + _$$ManagedUserModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson, readValue: _readId) + final String id; + @override + @JsonKey(name: 'employee_code', readValue: _readEmployeeCode) + final String employeeCode; + @override + @JsonKey(name: 'full_name', readValue: _readFullName) + final String fullName; + @override + @JsonKey(name: 'first_name') + final String? firstName; + @override + @JsonKey(name: 'last_name') + final String? lastName; + @override + final String email; + @override + @JsonKey() + final String mobile; + @override + @JsonKey( + fromJson: _idFromJsonNullable, + name: 'role_id', + readValue: _readRoleId, + ) + final String? roleId; + @override + @JsonKey(name: 'role_name', readValue: _readRoleName) + final String? roleName; + @override + @JsonKey(fromJson: _idFromJsonNullable, name: 'department_id') + final String? departmentId; + @override + @JsonKey(name: 'department_name', readValue: _readDepartmentName) + final String? departmentName; + @override + @JsonKey(fromJson: _idFromJsonNullable, name: 'designation_id') + final String? designationId; + @override + @JsonKey(name: 'designation_name') + final String? designationName; + @override + @JsonKey(fromJson: _idFromJsonNullable, name: 'plant_id') + final String? plantId; + @override + @JsonKey(name: 'plant_name', readValue: _readPlantName) + final String? plantName; + @override + @JsonKey(fromJson: _idFromJsonNullable, name: 'reporting_to') + final String? reportingTo; + @override + @JsonKey(name: 'reporting_to_name') + final String? reportingToName; + @override + @JsonKey(name: 'last_login_at') + final DateTime? lastLoginAt; + @override + final String? initials; + @override + @JsonKey() + final String status; + @override + @JsonKey(name: 'is_active') + final bool isActive; + @override + @JsonKey(name: 'avatar_url') + final String? avatarUrl; + @override + @JsonKey(name: 'created_at') + final DateTime? createdAt; + @override + @JsonKey(name: 'updated_at') + final DateTime? updatedAt; + + @override + String toString() { + return 'ManagedUserModel(id: $id, employeeCode: $employeeCode, fullName: $fullName, firstName: $firstName, lastName: $lastName, email: $email, mobile: $mobile, roleId: $roleId, roleName: $roleName, departmentId: $departmentId, departmentName: $departmentName, designationId: $designationId, designationName: $designationName, plantId: $plantId, plantName: $plantName, reportingTo: $reportingTo, reportingToName: $reportingToName, lastLoginAt: $lastLoginAt, initials: $initials, status: $status, isActive: $isActive, avatarUrl: $avatarUrl, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ManagedUserModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.employeeCode, employeeCode) || + other.employeeCode == employeeCode) && + (identical(other.fullName, fullName) || + other.fullName == fullName) && + (identical(other.firstName, firstName) || + other.firstName == firstName) && + (identical(other.lastName, lastName) || + other.lastName == lastName) && + (identical(other.email, email) || other.email == email) && + (identical(other.mobile, mobile) || other.mobile == mobile) && + (identical(other.roleId, roleId) || other.roleId == roleId) && + (identical(other.roleName, roleName) || + other.roleName == roleName) && + (identical(other.departmentId, departmentId) || + other.departmentId == departmentId) && + (identical(other.departmentName, departmentName) || + other.departmentName == departmentName) && + (identical(other.designationId, designationId) || + other.designationId == designationId) && + (identical(other.designationName, designationName) || + other.designationName == designationName) && + (identical(other.plantId, plantId) || other.plantId == plantId) && + (identical(other.plantName, plantName) || + other.plantName == plantName) && + (identical(other.reportingTo, reportingTo) || + other.reportingTo == reportingTo) && + (identical(other.reportingToName, reportingToName) || + other.reportingToName == reportingToName) && + (identical(other.lastLoginAt, lastLoginAt) || + other.lastLoginAt == lastLoginAt) && + (identical(other.initials, initials) || + other.initials == initials) && + (identical(other.status, status) || other.status == status) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + (identical(other.avatarUrl, avatarUrl) || + other.avatarUrl == avatarUrl) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hashAll([ + runtimeType, + id, + employeeCode, + fullName, + firstName, + lastName, + email, + mobile, + roleId, + roleName, + departmentId, + departmentName, + designationId, + designationName, + plantId, + plantName, + reportingTo, + reportingToName, + lastLoginAt, + initials, + status, + isActive, + avatarUrl, + createdAt, + updatedAt, + ]); + + /// Create a copy of ManagedUserModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ManagedUserModelImplCopyWith<_$ManagedUserModelImpl> get copyWith => + __$$ManagedUserModelImplCopyWithImpl<_$ManagedUserModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ManagedUserModelImplToJson(this); + } +} + +abstract class _ManagedUserModel implements ManagedUserModel { + const factory _ManagedUserModel({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) + required final String id, + @JsonKey(name: 'employee_code', readValue: _readEmployeeCode) + required final String employeeCode, + @JsonKey(name: 'full_name', readValue: _readFullName) + required final String fullName, + @JsonKey(name: 'first_name') final String? firstName, + @JsonKey(name: 'last_name') final String? lastName, + required final String email, + final String mobile, + @JsonKey( + fromJson: _idFromJsonNullable, + name: 'role_id', + readValue: _readRoleId, + ) + final String? roleId, + @JsonKey(name: 'role_name', readValue: _readRoleName) + final String? roleName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'department_id') + final String? departmentId, + @JsonKey(name: 'department_name', readValue: _readDepartmentName) + final String? departmentName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'designation_id') + final String? designationId, + @JsonKey(name: 'designation_name') final String? designationName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'plant_id') + final String? plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) + final String? plantName, + @JsonKey(fromJson: _idFromJsonNullable, name: 'reporting_to') + final String? reportingTo, + @JsonKey(name: 'reporting_to_name') final String? reportingToName, + @JsonKey(name: 'last_login_at') final DateTime? lastLoginAt, + final String? initials, + final String status, + @JsonKey(name: 'is_active') final bool isActive, + @JsonKey(name: 'avatar_url') final String? avatarUrl, + @JsonKey(name: 'created_at') final DateTime? createdAt, + @JsonKey(name: 'updated_at') final DateTime? updatedAt, + }) = _$ManagedUserModelImpl; + + factory _ManagedUserModel.fromJson(Map json) = + _$ManagedUserModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson, readValue: _readId) + String get id; + @override + @JsonKey(name: 'employee_code', readValue: _readEmployeeCode) + String get employeeCode; + @override + @JsonKey(name: 'full_name', readValue: _readFullName) + String get fullName; + @override + @JsonKey(name: 'first_name') + String? get firstName; + @override + @JsonKey(name: 'last_name') + String? get lastName; + @override + String get email; + @override + String get mobile; + @override + @JsonKey( + fromJson: _idFromJsonNullable, + name: 'role_id', + readValue: _readRoleId, + ) + String? get roleId; + @override + @JsonKey(name: 'role_name', readValue: _readRoleName) + String? get roleName; + @override + @JsonKey(fromJson: _idFromJsonNullable, name: 'department_id') + String? get departmentId; + @override + @JsonKey(name: 'department_name', readValue: _readDepartmentName) + String? get departmentName; + @override + @JsonKey(fromJson: _idFromJsonNullable, name: 'designation_id') + String? get designationId; + @override + @JsonKey(name: 'designation_name') + String? get designationName; + @override + @JsonKey(fromJson: _idFromJsonNullable, name: 'plant_id') + String? get plantId; + @override + @JsonKey(name: 'plant_name', readValue: _readPlantName) + String? get plantName; + @override + @JsonKey(fromJson: _idFromJsonNullable, name: 'reporting_to') + String? get reportingTo; + @override + @JsonKey(name: 'reporting_to_name') + String? get reportingToName; + @override + @JsonKey(name: 'last_login_at') + DateTime? get lastLoginAt; + @override + String? get initials; + @override + String get status; + @override + @JsonKey(name: 'is_active') + bool get isActive; + @override + @JsonKey(name: 'avatar_url') + String? get avatarUrl; + @override + @JsonKey(name: 'created_at') + DateTime? get createdAt; + @override + @JsonKey(name: 'updated_at') + DateTime? get updatedAt; + + /// Create a copy of ManagedUserModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ManagedUserModelImplCopyWith<_$ManagedUserModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +CreateUserRequest _$CreateUserRequestFromJson(Map json) { + return _CreateUserRequest.fromJson(json); +} + +/// @nodoc +mixin _$CreateUserRequest { + @JsonKey(name: 'employee_code') + String get employeeCode => throw _privateConstructorUsedError; + @JsonKey(name: 'full_name') + String get fullName => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + String? get mobile => throw _privateConstructorUsedError; + @JsonKey(name: 'role_id') + int get roleId => throw _privateConstructorUsedError; + @JsonKey(name: 'department_id') + int? get departmentId => throw _privateConstructorUsedError; + @JsonKey(name: 'designation_id') + int? get designationId => throw _privateConstructorUsedError; + @JsonKey(name: 'plant_id') + int? get plantId => throw _privateConstructorUsedError; + @JsonKey(name: 'reporting_to') + int? get reportingTo => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + + /// Serializes this CreateUserRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CreateUserRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CreateUserRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CreateUserRequestCopyWith<$Res> { + factory $CreateUserRequestCopyWith( + CreateUserRequest value, + $Res Function(CreateUserRequest) then, + ) = _$CreateUserRequestCopyWithImpl<$Res, CreateUserRequest>; + @useResult + $Res call({ + @JsonKey(name: 'employee_code') String employeeCode, + @JsonKey(name: 'full_name') String fullName, + String email, + String password, + String? mobile, + @JsonKey(name: 'role_id') int roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'designation_id') int? designationId, + @JsonKey(name: 'plant_id') int? plantId, + @JsonKey(name: 'reporting_to') int? reportingTo, + String status, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class _$CreateUserRequestCopyWithImpl<$Res, $Val extends CreateUserRequest> + implements $CreateUserRequestCopyWith<$Res> { + _$CreateUserRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CreateUserRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? employeeCode = null, + Object? fullName = null, + Object? email = null, + Object? password = null, + Object? mobile = freezed, + Object? roleId = null, + Object? departmentId = freezed, + Object? designationId = freezed, + Object? plantId = freezed, + Object? reportingTo = freezed, + Object? status = null, + Object? isActive = null, + }) { + return _then( + _value.copyWith( + employeeCode: null == employeeCode + ? _value.employeeCode + : employeeCode // ignore: cast_nullable_to_non_nullable + as String, + fullName: null == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + mobile: freezed == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String?, + roleId: null == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as int, + departmentId: freezed == departmentId + ? _value.departmentId + : departmentId // ignore: cast_nullable_to_non_nullable + as int?, + designationId: freezed == designationId + ? _value.designationId + : designationId // ignore: cast_nullable_to_non_nullable + as int?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as int?, + reportingTo: freezed == reportingTo + ? _value.reportingTo + : reportingTo // ignore: cast_nullable_to_non_nullable + as int?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CreateUserRequestImplCopyWith<$Res> + implements $CreateUserRequestCopyWith<$Res> { + factory _$$CreateUserRequestImplCopyWith( + _$CreateUserRequestImpl value, + $Res Function(_$CreateUserRequestImpl) then, + ) = __$$CreateUserRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'employee_code') String employeeCode, + @JsonKey(name: 'full_name') String fullName, + String email, + String password, + String? mobile, + @JsonKey(name: 'role_id') int roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'designation_id') int? designationId, + @JsonKey(name: 'plant_id') int? plantId, + @JsonKey(name: 'reporting_to') int? reportingTo, + String status, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class __$$CreateUserRequestImplCopyWithImpl<$Res> + extends _$CreateUserRequestCopyWithImpl<$Res, _$CreateUserRequestImpl> + implements _$$CreateUserRequestImplCopyWith<$Res> { + __$$CreateUserRequestImplCopyWithImpl( + _$CreateUserRequestImpl _value, + $Res Function(_$CreateUserRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CreateUserRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? employeeCode = null, + Object? fullName = null, + Object? email = null, + Object? password = null, + Object? mobile = freezed, + Object? roleId = null, + Object? departmentId = freezed, + Object? designationId = freezed, + Object? plantId = freezed, + Object? reportingTo = freezed, + Object? status = null, + Object? isActive = null, + }) { + return _then( + _$CreateUserRequestImpl( + employeeCode: null == employeeCode + ? _value.employeeCode + : employeeCode // ignore: cast_nullable_to_non_nullable + as String, + fullName: null == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + mobile: freezed == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String?, + roleId: null == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as int, + departmentId: freezed == departmentId + ? _value.departmentId + : departmentId // ignore: cast_nullable_to_non_nullable + as int?, + designationId: freezed == designationId + ? _value.designationId + : designationId // ignore: cast_nullable_to_non_nullable + as int?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as int?, + reportingTo: freezed == reportingTo + ? _value.reportingTo + : reportingTo // ignore: cast_nullable_to_non_nullable + as int?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CreateUserRequestImpl implements _CreateUserRequest { + const _$CreateUserRequestImpl({ + @JsonKey(name: 'employee_code') required this.employeeCode, + @JsonKey(name: 'full_name') required this.fullName, + required this.email, + required this.password, + this.mobile, + @JsonKey(name: 'role_id') required this.roleId, + @JsonKey(name: 'department_id') this.departmentId, + @JsonKey(name: 'designation_id') this.designationId, + @JsonKey(name: 'plant_id') this.plantId, + @JsonKey(name: 'reporting_to') this.reportingTo, + this.status = 'active', + @JsonKey(name: 'is_active') this.isActive = true, + }); + + factory _$CreateUserRequestImpl.fromJson(Map json) => + _$$CreateUserRequestImplFromJson(json); + + @override + @JsonKey(name: 'employee_code') + final String employeeCode; + @override + @JsonKey(name: 'full_name') + final String fullName; + @override + final String email; + @override + final String password; + @override + final String? mobile; + @override + @JsonKey(name: 'role_id') + final int roleId; + @override + @JsonKey(name: 'department_id') + final int? departmentId; + @override + @JsonKey(name: 'designation_id') + final int? designationId; + @override + @JsonKey(name: 'plant_id') + final int? plantId; + @override + @JsonKey(name: 'reporting_to') + final int? reportingTo; + @override + @JsonKey() + final String status; + @override + @JsonKey(name: 'is_active') + final bool isActive; + + @override + String toString() { + return 'CreateUserRequest(employeeCode: $employeeCode, fullName: $fullName, email: $email, password: $password, mobile: $mobile, roleId: $roleId, departmentId: $departmentId, designationId: $designationId, plantId: $plantId, reportingTo: $reportingTo, status: $status, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateUserRequestImpl && + (identical(other.employeeCode, employeeCode) || + other.employeeCode == employeeCode) && + (identical(other.fullName, fullName) || + other.fullName == fullName) && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password) && + (identical(other.mobile, mobile) || other.mobile == mobile) && + (identical(other.roleId, roleId) || other.roleId == roleId) && + (identical(other.departmentId, departmentId) || + other.departmentId == departmentId) && + (identical(other.designationId, designationId) || + other.designationId == designationId) && + (identical(other.plantId, plantId) || other.plantId == plantId) && + (identical(other.reportingTo, reportingTo) || + other.reportingTo == reportingTo) && + (identical(other.status, status) || other.status == status) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + employeeCode, + fullName, + email, + password, + mobile, + roleId, + departmentId, + designationId, + plantId, + reportingTo, + status, + isActive, + ); + + /// Create a copy of CreateUserRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreateUserRequestImplCopyWith<_$CreateUserRequestImpl> get copyWith => + __$$CreateUserRequestImplCopyWithImpl<_$CreateUserRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$CreateUserRequestImplToJson(this); + } +} + +abstract class _CreateUserRequest implements CreateUserRequest { + const factory _CreateUserRequest({ + @JsonKey(name: 'employee_code') required final String employeeCode, + @JsonKey(name: 'full_name') required final String fullName, + required final String email, + required final String password, + final String? mobile, + @JsonKey(name: 'role_id') required final int roleId, + @JsonKey(name: 'department_id') final int? departmentId, + @JsonKey(name: 'designation_id') final int? designationId, + @JsonKey(name: 'plant_id') final int? plantId, + @JsonKey(name: 'reporting_to') final int? reportingTo, + final String status, + @JsonKey(name: 'is_active') final bool isActive, + }) = _$CreateUserRequestImpl; + + factory _CreateUserRequest.fromJson(Map json) = + _$CreateUserRequestImpl.fromJson; + + @override + @JsonKey(name: 'employee_code') + String get employeeCode; + @override + @JsonKey(name: 'full_name') + String get fullName; + @override + String get email; + @override + String get password; + @override + String? get mobile; + @override + @JsonKey(name: 'role_id') + int get roleId; + @override + @JsonKey(name: 'department_id') + int? get departmentId; + @override + @JsonKey(name: 'designation_id') + int? get designationId; + @override + @JsonKey(name: 'plant_id') + int? get plantId; + @override + @JsonKey(name: 'reporting_to') + int? get reportingTo; + @override + String get status; + @override + @JsonKey(name: 'is_active') + bool get isActive; + + /// Create a copy of CreateUserRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreateUserRequestImplCopyWith<_$CreateUserRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} + +UpdateUserRequest _$UpdateUserRequestFromJson(Map json) { + return _UpdateUserRequest.fromJson(json); +} + +/// @nodoc +mixin _$UpdateUserRequest { + @JsonKey(name: 'employee_code') + String? get employeeCode => throw _privateConstructorUsedError; + @JsonKey(name: 'full_name') + String? get fullName => throw _privateConstructorUsedError; + String? get email => throw _privateConstructorUsedError; + String? get password => throw _privateConstructorUsedError; + String? get mobile => throw _privateConstructorUsedError; + @JsonKey(name: 'role_id') + int? get roleId => throw _privateConstructorUsedError; + @JsonKey(name: 'department_id') + int? get departmentId => throw _privateConstructorUsedError; + @JsonKey(name: 'designation_id') + int? get designationId => throw _privateConstructorUsedError; + @JsonKey(name: 'plant_id') + int? get plantId => throw _privateConstructorUsedError; + @JsonKey(name: 'reporting_to') + int? get reportingTo => throw _privateConstructorUsedError; + String? get status => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool? get isActive => throw _privateConstructorUsedError; + + /// Serializes this UpdateUserRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UpdateUserRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UpdateUserRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateUserRequestCopyWith<$Res> { + factory $UpdateUserRequestCopyWith( + UpdateUserRequest value, + $Res Function(UpdateUserRequest) then, + ) = _$UpdateUserRequestCopyWithImpl<$Res, UpdateUserRequest>; + @useResult + $Res call({ + @JsonKey(name: 'employee_code') String? employeeCode, + @JsonKey(name: 'full_name') String? fullName, + String? email, + String? password, + String? mobile, + @JsonKey(name: 'role_id') int? roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'designation_id') int? designationId, + @JsonKey(name: 'plant_id') int? plantId, + @JsonKey(name: 'reporting_to') int? reportingTo, + String? status, + @JsonKey(name: 'is_active') bool? isActive, + }); +} + +/// @nodoc +class _$UpdateUserRequestCopyWithImpl<$Res, $Val extends UpdateUserRequest> + implements $UpdateUserRequestCopyWith<$Res> { + _$UpdateUserRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UpdateUserRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? employeeCode = freezed, + Object? fullName = freezed, + Object? email = freezed, + Object? password = freezed, + Object? mobile = freezed, + Object? roleId = freezed, + Object? departmentId = freezed, + Object? designationId = freezed, + Object? plantId = freezed, + Object? reportingTo = freezed, + Object? status = freezed, + Object? isActive = freezed, + }) { + return _then( + _value.copyWith( + employeeCode: freezed == employeeCode + ? _value.employeeCode + : employeeCode // ignore: cast_nullable_to_non_nullable + as String?, + fullName: freezed == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + password: freezed == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String?, + mobile: freezed == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String?, + roleId: freezed == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as int?, + departmentId: freezed == departmentId + ? _value.departmentId + : departmentId // ignore: cast_nullable_to_non_nullable + as int?, + designationId: freezed == designationId + ? _value.designationId + : designationId // ignore: cast_nullable_to_non_nullable + as int?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as int?, + reportingTo: freezed == reportingTo + ? _value.reportingTo + : reportingTo // ignore: cast_nullable_to_non_nullable + as int?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UpdateUserRequestImplCopyWith<$Res> + implements $UpdateUserRequestCopyWith<$Res> { + factory _$$UpdateUserRequestImplCopyWith( + _$UpdateUserRequestImpl value, + $Res Function(_$UpdateUserRequestImpl) then, + ) = __$$UpdateUserRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'employee_code') String? employeeCode, + @JsonKey(name: 'full_name') String? fullName, + String? email, + String? password, + String? mobile, + @JsonKey(name: 'role_id') int? roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'designation_id') int? designationId, + @JsonKey(name: 'plant_id') int? plantId, + @JsonKey(name: 'reporting_to') int? reportingTo, + String? status, + @JsonKey(name: 'is_active') bool? isActive, + }); +} + +/// @nodoc +class __$$UpdateUserRequestImplCopyWithImpl<$Res> + extends _$UpdateUserRequestCopyWithImpl<$Res, _$UpdateUserRequestImpl> + implements _$$UpdateUserRequestImplCopyWith<$Res> { + __$$UpdateUserRequestImplCopyWithImpl( + _$UpdateUserRequestImpl _value, + $Res Function(_$UpdateUserRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UpdateUserRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? employeeCode = freezed, + Object? fullName = freezed, + Object? email = freezed, + Object? password = freezed, + Object? mobile = freezed, + Object? roleId = freezed, + Object? departmentId = freezed, + Object? designationId = freezed, + Object? plantId = freezed, + Object? reportingTo = freezed, + Object? status = freezed, + Object? isActive = freezed, + }) { + return _then( + _$UpdateUserRequestImpl( + employeeCode: freezed == employeeCode + ? _value.employeeCode + : employeeCode // ignore: cast_nullable_to_non_nullable + as String?, + fullName: freezed == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + password: freezed == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String?, + mobile: freezed == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String?, + roleId: freezed == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as int?, + departmentId: freezed == departmentId + ? _value.departmentId + : departmentId // ignore: cast_nullable_to_non_nullable + as int?, + designationId: freezed == designationId + ? _value.designationId + : designationId // ignore: cast_nullable_to_non_nullable + as int?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as int?, + reportingTo: freezed == reportingTo + ? _value.reportingTo + : reportingTo // ignore: cast_nullable_to_non_nullable + as int?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UpdateUserRequestImpl implements _UpdateUserRequest { + const _$UpdateUserRequestImpl({ + @JsonKey(name: 'employee_code') this.employeeCode, + @JsonKey(name: 'full_name') this.fullName, + this.email, + this.password, + this.mobile, + @JsonKey(name: 'role_id') this.roleId, + @JsonKey(name: 'department_id') this.departmentId, + @JsonKey(name: 'designation_id') this.designationId, + @JsonKey(name: 'plant_id') this.plantId, + @JsonKey(name: 'reporting_to') this.reportingTo, + this.status, + @JsonKey(name: 'is_active') this.isActive, + }); + + factory _$UpdateUserRequestImpl.fromJson(Map json) => + _$$UpdateUserRequestImplFromJson(json); + + @override + @JsonKey(name: 'employee_code') + final String? employeeCode; + @override + @JsonKey(name: 'full_name') + final String? fullName; + @override + final String? email; + @override + final String? password; + @override + final String? mobile; + @override + @JsonKey(name: 'role_id') + final int? roleId; + @override + @JsonKey(name: 'department_id') + final int? departmentId; + @override + @JsonKey(name: 'designation_id') + final int? designationId; + @override + @JsonKey(name: 'plant_id') + final int? plantId; + @override + @JsonKey(name: 'reporting_to') + final int? reportingTo; + @override + final String? status; + @override + @JsonKey(name: 'is_active') + final bool? isActive; + + @override + String toString() { + return 'UpdateUserRequest(employeeCode: $employeeCode, fullName: $fullName, email: $email, password: $password, mobile: $mobile, roleId: $roleId, departmentId: $departmentId, designationId: $designationId, plantId: $plantId, reportingTo: $reportingTo, status: $status, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateUserRequestImpl && + (identical(other.employeeCode, employeeCode) || + other.employeeCode == employeeCode) && + (identical(other.fullName, fullName) || + other.fullName == fullName) && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password) && + (identical(other.mobile, mobile) || other.mobile == mobile) && + (identical(other.roleId, roleId) || other.roleId == roleId) && + (identical(other.departmentId, departmentId) || + other.departmentId == departmentId) && + (identical(other.designationId, designationId) || + other.designationId == designationId) && + (identical(other.plantId, plantId) || other.plantId == plantId) && + (identical(other.reportingTo, reportingTo) || + other.reportingTo == reportingTo) && + (identical(other.status, status) || other.status == status) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + employeeCode, + fullName, + email, + password, + mobile, + roleId, + departmentId, + designationId, + plantId, + reportingTo, + status, + isActive, + ); + + /// Create a copy of UpdateUserRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UpdateUserRequestImplCopyWith<_$UpdateUserRequestImpl> get copyWith => + __$$UpdateUserRequestImplCopyWithImpl<_$UpdateUserRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$UpdateUserRequestImplToJson(this); + } +} + +abstract class _UpdateUserRequest implements UpdateUserRequest { + const factory _UpdateUserRequest({ + @JsonKey(name: 'employee_code') final String? employeeCode, + @JsonKey(name: 'full_name') final String? fullName, + final String? email, + final String? password, + final String? mobile, + @JsonKey(name: 'role_id') final int? roleId, + @JsonKey(name: 'department_id') final int? departmentId, + @JsonKey(name: 'designation_id') final int? designationId, + @JsonKey(name: 'plant_id') final int? plantId, + @JsonKey(name: 'reporting_to') final int? reportingTo, + final String? status, + @JsonKey(name: 'is_active') final bool? isActive, + }) = _$UpdateUserRequestImpl; + + factory _UpdateUserRequest.fromJson(Map json) = + _$UpdateUserRequestImpl.fromJson; + + @override + @JsonKey(name: 'employee_code') + String? get employeeCode; + @override + @JsonKey(name: 'full_name') + String? get fullName; + @override + String? get email; + @override + String? get password; + @override + String? get mobile; + @override + @JsonKey(name: 'role_id') + int? get roleId; + @override + @JsonKey(name: 'department_id') + int? get departmentId; + @override + @JsonKey(name: 'designation_id') + int? get designationId; + @override + @JsonKey(name: 'plant_id') + int? get plantId; + @override + @JsonKey(name: 'reporting_to') + int? get reportingTo; + @override + String? get status; + @override + @JsonKey(name: 'is_active') + bool? get isActive; + + /// Create a copy of UpdateUserRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UpdateUserRequestImplCopyWith<_$UpdateUserRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$UserListQuery { + int get page => throw _privateConstructorUsedError; + int get limit => throw _privateConstructorUsedError; + String? get search => throw _privateConstructorUsedError; + String? get status => throw _privateConstructorUsedError; + @JsonKey(name: 'role_id') + int? get roleId => throw _privateConstructorUsedError; + @JsonKey(name: 'department_id') + int? get departmentId => throw _privateConstructorUsedError; + @JsonKey(name: 'sort_by') + String? get sortBy => throw _privateConstructorUsedError; + @JsonKey(name: 'sort_order') + String get sortOrder => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool? get isActive => throw _privateConstructorUsedError; + + /// Create a copy of UserListQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserListQueryCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserListQueryCopyWith<$Res> { + factory $UserListQueryCopyWith( + UserListQuery value, + $Res Function(UserListQuery) then, + ) = _$UserListQueryCopyWithImpl<$Res, UserListQuery>; + @useResult + $Res call({ + int page, + int limit, + String? search, + String? status, + @JsonKey(name: 'role_id') int? roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'sort_by') String? sortBy, + @JsonKey(name: 'sort_order') String sortOrder, + @JsonKey(name: 'is_active') bool? isActive, + }); +} + +/// @nodoc +class _$UserListQueryCopyWithImpl<$Res, $Val extends UserListQuery> + implements $UserListQueryCopyWith<$Res> { + _$UserListQueryCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserListQuery + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? page = null, + Object? limit = null, + Object? search = freezed, + Object? status = freezed, + Object? roleId = freezed, + Object? departmentId = freezed, + Object? sortBy = freezed, + Object? sortOrder = null, + Object? isActive = freezed, + }) { + return _then( + _value.copyWith( + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as int, + search: freezed == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String?, + roleId: freezed == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as int?, + departmentId: freezed == departmentId + ? _value.departmentId + : departmentId // ignore: cast_nullable_to_non_nullable + as int?, + sortBy: freezed == sortBy + ? _value.sortBy + : sortBy // ignore: cast_nullable_to_non_nullable + as String?, + sortOrder: null == sortOrder + ? _value.sortOrder + : sortOrder // ignore: cast_nullable_to_non_nullable + as String, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UserListQueryImplCopyWith<$Res> + implements $UserListQueryCopyWith<$Res> { + factory _$$UserListQueryImplCopyWith( + _$UserListQueryImpl value, + $Res Function(_$UserListQueryImpl) then, + ) = __$$UserListQueryImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int page, + int limit, + String? search, + String? status, + @JsonKey(name: 'role_id') int? roleId, + @JsonKey(name: 'department_id') int? departmentId, + @JsonKey(name: 'sort_by') String? sortBy, + @JsonKey(name: 'sort_order') String sortOrder, + @JsonKey(name: 'is_active') bool? isActive, + }); +} + +/// @nodoc +class __$$UserListQueryImplCopyWithImpl<$Res> + extends _$UserListQueryCopyWithImpl<$Res, _$UserListQueryImpl> + implements _$$UserListQueryImplCopyWith<$Res> { + __$$UserListQueryImplCopyWithImpl( + _$UserListQueryImpl _value, + $Res Function(_$UserListQueryImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UserListQuery + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? page = null, + Object? limit = null, + Object? search = freezed, + Object? status = freezed, + Object? roleId = freezed, + Object? departmentId = freezed, + Object? sortBy = freezed, + Object? sortOrder = null, + Object? isActive = freezed, + }) { + return _then( + _$UserListQueryImpl( + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as int, + limit: null == limit + ? _value.limit + : limit // ignore: cast_nullable_to_non_nullable + as int, + search: freezed == search + ? _value.search + : search // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String?, + roleId: freezed == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as int?, + departmentId: freezed == departmentId + ? _value.departmentId + : departmentId // ignore: cast_nullable_to_non_nullable + as int?, + sortBy: freezed == sortBy + ? _value.sortBy + : sortBy // ignore: cast_nullable_to_non_nullable + as String?, + sortOrder: null == sortOrder + ? _value.sortOrder + : sortOrder // ignore: cast_nullable_to_non_nullable + as String, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + ), + ); + } +} + +/// @nodoc + +class _$UserListQueryImpl implements _UserListQuery { + const _$UserListQueryImpl({ + this.page = 1, + this.limit = 20, + this.search, + this.status, + @JsonKey(name: 'role_id') this.roleId, + @JsonKey(name: 'department_id') this.departmentId, + @JsonKey(name: 'sort_by') this.sortBy, + @JsonKey(name: 'sort_order') this.sortOrder = 'asc', + @JsonKey(name: 'is_active') this.isActive, + }); + + @override + @JsonKey() + final int page; + @override + @JsonKey() + final int limit; + @override + final String? search; + @override + final String? status; + @override + @JsonKey(name: 'role_id') + final int? roleId; + @override + @JsonKey(name: 'department_id') + final int? departmentId; + @override + @JsonKey(name: 'sort_by') + final String? sortBy; + @override + @JsonKey(name: 'sort_order') + final String sortOrder; + @override + @JsonKey(name: 'is_active') + final bool? isActive; + + @override + String toString() { + return 'UserListQuery(page: $page, limit: $limit, search: $search, status: $status, roleId: $roleId, departmentId: $departmentId, sortBy: $sortBy, sortOrder: $sortOrder, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserListQueryImpl && + (identical(other.page, page) || other.page == page) && + (identical(other.limit, limit) || other.limit == limit) && + (identical(other.search, search) || other.search == search) && + (identical(other.status, status) || other.status == status) && + (identical(other.roleId, roleId) || other.roleId == roleId) && + (identical(other.departmentId, departmentId) || + other.departmentId == departmentId) && + (identical(other.sortBy, sortBy) || other.sortBy == sortBy) && + (identical(other.sortOrder, sortOrder) || + other.sortOrder == sortOrder) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + page, + limit, + search, + status, + roleId, + departmentId, + sortBy, + sortOrder, + isActive, + ); + + /// Create a copy of UserListQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserListQueryImplCopyWith<_$UserListQueryImpl> get copyWith => + __$$UserListQueryImplCopyWithImpl<_$UserListQueryImpl>(this, _$identity); +} + +abstract class _UserListQuery implements UserListQuery { + const factory _UserListQuery({ + final int page, + final int limit, + final String? search, + final String? status, + @JsonKey(name: 'role_id') final int? roleId, + @JsonKey(name: 'department_id') final int? departmentId, + @JsonKey(name: 'sort_by') final String? sortBy, + @JsonKey(name: 'sort_order') final String sortOrder, + @JsonKey(name: 'is_active') final bool? isActive, + }) = _$UserListQueryImpl; + + @override + int get page; + @override + int get limit; + @override + String? get search; + @override + String? get status; + @override + @JsonKey(name: 'role_id') + int? get roleId; + @override + @JsonKey(name: 'department_id') + int? get departmentId; + @override + @JsonKey(name: 'sort_by') + String? get sortBy; + @override + @JsonKey(name: 'sort_order') + String get sortOrder; + @override + @JsonKey(name: 'is_active') + bool? get isActive; + + /// Create a copy of UserListQuery + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserListQueryImplCopyWith<_$UserListQueryImpl> get copyWith => + throw _privateConstructorUsedError; +} + +RoleCardModel _$RoleCardModelFromJson(Map json) { + return _RoleCardModel.fromJson(json); +} + +/// @nodoc +mixin _$RoleCardModel { + @JsonKey(fromJson: _idFromJson, readValue: _readId) + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + @JsonKey(name: 'user_count') + int get userCount => throw _privateConstructorUsedError; + @JsonKey(name: 'permission_count') + int get permissionCount => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + + /// Serializes this RoleCardModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of RoleCardModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $RoleCardModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $RoleCardModelCopyWith<$Res> { + factory $RoleCardModelCopyWith( + RoleCardModel value, + $Res Function(RoleCardModel) then, + ) = _$RoleCardModelCopyWithImpl<$Res, RoleCardModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) String id, + String name, + String? description, + @JsonKey(name: 'user_count') int userCount, + @JsonKey(name: 'permission_count') int permissionCount, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class _$RoleCardModelCopyWithImpl<$Res, $Val extends RoleCardModel> + implements $RoleCardModelCopyWith<$Res> { + _$RoleCardModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of RoleCardModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? description = freezed, + Object? userCount = null, + Object? permissionCount = null, + Object? isActive = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + userCount: null == userCount + ? _value.userCount + : userCount // ignore: cast_nullable_to_non_nullable + as int, + permissionCount: null == permissionCount + ? _value.permissionCount + : permissionCount // ignore: cast_nullable_to_non_nullable + as int, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$RoleCardModelImplCopyWith<$Res> + implements $RoleCardModelCopyWith<$Res> { + factory _$$RoleCardModelImplCopyWith( + _$RoleCardModelImpl value, + $Res Function(_$RoleCardModelImpl) then, + ) = __$$RoleCardModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) String id, + String name, + String? description, + @JsonKey(name: 'user_count') int userCount, + @JsonKey(name: 'permission_count') int permissionCount, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class __$$RoleCardModelImplCopyWithImpl<$Res> + extends _$RoleCardModelCopyWithImpl<$Res, _$RoleCardModelImpl> + implements _$$RoleCardModelImplCopyWith<$Res> { + __$$RoleCardModelImplCopyWithImpl( + _$RoleCardModelImpl _value, + $Res Function(_$RoleCardModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of RoleCardModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? description = freezed, + Object? userCount = null, + Object? permissionCount = null, + Object? isActive = null, + }) { + return _then( + _$RoleCardModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + userCount: null == userCount + ? _value.userCount + : userCount // ignore: cast_nullable_to_non_nullable + as int, + permissionCount: null == permissionCount + ? _value.permissionCount + : permissionCount // ignore: cast_nullable_to_non_nullable + as int, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$RoleCardModelImpl implements _RoleCardModel { + const _$RoleCardModelImpl({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) required this.id, + required this.name, + this.description, + @JsonKey(name: 'user_count') this.userCount = 0, + @JsonKey(name: 'permission_count') this.permissionCount = 0, + @JsonKey(name: 'is_active') this.isActive = true, + }); + + factory _$RoleCardModelImpl.fromJson(Map json) => + _$$RoleCardModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson, readValue: _readId) + final String id; + @override + final String name; + @override + final String? description; + @override + @JsonKey(name: 'user_count') + final int userCount; + @override + @JsonKey(name: 'permission_count') + final int permissionCount; + @override + @JsonKey(name: 'is_active') + final bool isActive; + + @override + String toString() { + return 'RoleCardModel(id: $id, name: $name, description: $description, userCount: $userCount, permissionCount: $permissionCount, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$RoleCardModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + (identical(other.userCount, userCount) || + other.userCount == userCount) && + (identical(other.permissionCount, permissionCount) || + other.permissionCount == permissionCount) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + description, + userCount, + permissionCount, + isActive, + ); + + /// Create a copy of RoleCardModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$RoleCardModelImplCopyWith<_$RoleCardModelImpl> get copyWith => + __$$RoleCardModelImplCopyWithImpl<_$RoleCardModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$RoleCardModelImplToJson(this); + } +} + +abstract class _RoleCardModel implements RoleCardModel { + const factory _RoleCardModel({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) + required final String id, + required final String name, + final String? description, + @JsonKey(name: 'user_count') final int userCount, + @JsonKey(name: 'permission_count') final int permissionCount, + @JsonKey(name: 'is_active') final bool isActive, + }) = _$RoleCardModelImpl; + + factory _RoleCardModel.fromJson(Map json) = + _$RoleCardModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson, readValue: _readId) + String get id; + @override + String get name; + @override + String? get description; + @override + @JsonKey(name: 'user_count') + int get userCount; + @override + @JsonKey(name: 'permission_count') + int get permissionCount; + @override + @JsonKey(name: 'is_active') + bool get isActive; + + /// Create a copy of RoleCardModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$RoleCardModelImplCopyWith<_$RoleCardModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +CreateRoleRequest _$CreateRoleRequestFromJson(Map json) { + return _CreateRoleRequest.fromJson(json); +} + +/// @nodoc +mixin _$CreateRoleRequest { + String get name => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + + /// Serializes this CreateRoleRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CreateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CreateRoleRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CreateRoleRequestCopyWith<$Res> { + factory $CreateRoleRequestCopyWith( + CreateRoleRequest value, + $Res Function(CreateRoleRequest) then, + ) = _$CreateRoleRequestCopyWithImpl<$Res, CreateRoleRequest>; + @useResult + $Res call({ + String name, + String? description, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class _$CreateRoleRequestCopyWithImpl<$Res, $Val extends CreateRoleRequest> + implements $CreateRoleRequestCopyWith<$Res> { + _$CreateRoleRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CreateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? description = freezed, + Object? isActive = null, + }) { + return _then( + _value.copyWith( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CreateRoleRequestImplCopyWith<$Res> + implements $CreateRoleRequestCopyWith<$Res> { + factory _$$CreateRoleRequestImplCopyWith( + _$CreateRoleRequestImpl value, + $Res Function(_$CreateRoleRequestImpl) then, + ) = __$$CreateRoleRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String name, + String? description, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class __$$CreateRoleRequestImplCopyWithImpl<$Res> + extends _$CreateRoleRequestCopyWithImpl<$Res, _$CreateRoleRequestImpl> + implements _$$CreateRoleRequestImplCopyWith<$Res> { + __$$CreateRoleRequestImplCopyWithImpl( + _$CreateRoleRequestImpl _value, + $Res Function(_$CreateRoleRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CreateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = null, + Object? description = freezed, + Object? isActive = null, + }) { + return _then( + _$CreateRoleRequestImpl( + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CreateRoleRequestImpl implements _CreateRoleRequest { + const _$CreateRoleRequestImpl({ + required this.name, + this.description, + @JsonKey(name: 'is_active') this.isActive = true, + }); + + factory _$CreateRoleRequestImpl.fromJson(Map json) => + _$$CreateRoleRequestImplFromJson(json); + + @override + final String name; + @override + final String? description; + @override + @JsonKey(name: 'is_active') + final bool isActive; + + @override + String toString() { + return 'CreateRoleRequest(name: $name, description: $description, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CreateRoleRequestImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name, description, isActive); + + /// Create a copy of CreateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CreateRoleRequestImplCopyWith<_$CreateRoleRequestImpl> get copyWith => + __$$CreateRoleRequestImplCopyWithImpl<_$CreateRoleRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$CreateRoleRequestImplToJson(this); + } +} + +abstract class _CreateRoleRequest implements CreateRoleRequest { + const factory _CreateRoleRequest({ + required final String name, + final String? description, + @JsonKey(name: 'is_active') final bool isActive, + }) = _$CreateRoleRequestImpl; + + factory _CreateRoleRequest.fromJson(Map json) = + _$CreateRoleRequestImpl.fromJson; + + @override + String get name; + @override + String? get description; + @override + @JsonKey(name: 'is_active') + bool get isActive; + + /// Create a copy of CreateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CreateRoleRequestImplCopyWith<_$CreateRoleRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} + +UpdateRoleRequest _$UpdateRoleRequestFromJson(Map json) { + return _UpdateRoleRequest.fromJson(json); +} + +/// @nodoc +mixin _$UpdateRoleRequest { + String? get name => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool? get isActive => throw _privateConstructorUsedError; + + /// Serializes this UpdateRoleRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UpdateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UpdateRoleRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateRoleRequestCopyWith<$Res> { + factory $UpdateRoleRequestCopyWith( + UpdateRoleRequest value, + $Res Function(UpdateRoleRequest) then, + ) = _$UpdateRoleRequestCopyWithImpl<$Res, UpdateRoleRequest>; + @useResult + $Res call({ + String? name, + String? description, + @JsonKey(name: 'is_active') bool? isActive, + }); +} + +/// @nodoc +class _$UpdateRoleRequestCopyWithImpl<$Res, $Val extends UpdateRoleRequest> + implements $UpdateRoleRequestCopyWith<$Res> { + _$UpdateRoleRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UpdateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = freezed, + Object? description = freezed, + Object? isActive = freezed, + }) { + return _then( + _value.copyWith( + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UpdateRoleRequestImplCopyWith<$Res> + implements $UpdateRoleRequestCopyWith<$Res> { + factory _$$UpdateRoleRequestImplCopyWith( + _$UpdateRoleRequestImpl value, + $Res Function(_$UpdateRoleRequestImpl) then, + ) = __$$UpdateRoleRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String? name, + String? description, + @JsonKey(name: 'is_active') bool? isActive, + }); +} + +/// @nodoc +class __$$UpdateRoleRequestImplCopyWithImpl<$Res> + extends _$UpdateRoleRequestCopyWithImpl<$Res, _$UpdateRoleRequestImpl> + implements _$$UpdateRoleRequestImplCopyWith<$Res> { + __$$UpdateRoleRequestImplCopyWithImpl( + _$UpdateRoleRequestImpl _value, + $Res Function(_$UpdateRoleRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UpdateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? name = freezed, + Object? description = freezed, + Object? isActive = freezed, + }) { + return _then( + _$UpdateRoleRequestImpl( + name: freezed == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String?, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UpdateRoleRequestImpl implements _UpdateRoleRequest { + const _$UpdateRoleRequestImpl({ + this.name, + this.description, + @JsonKey(name: 'is_active') this.isActive, + }); + + factory _$UpdateRoleRequestImpl.fromJson(Map json) => + _$$UpdateRoleRequestImplFromJson(json); + + @override + final String? name; + @override + final String? description; + @override + @JsonKey(name: 'is_active') + final bool? isActive; + + @override + String toString() { + return 'UpdateRoleRequest(name: $name, description: $description, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateRoleRequestImpl && + (identical(other.name, name) || other.name == name) && + (identical(other.description, description) || + other.description == description) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, name, description, isActive); + + /// Create a copy of UpdateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UpdateRoleRequestImplCopyWith<_$UpdateRoleRequestImpl> get copyWith => + __$$UpdateRoleRequestImplCopyWithImpl<_$UpdateRoleRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$UpdateRoleRequestImplToJson(this); + } +} + +abstract class _UpdateRoleRequest implements UpdateRoleRequest { + const factory _UpdateRoleRequest({ + final String? name, + final String? description, + @JsonKey(name: 'is_active') final bool? isActive, + }) = _$UpdateRoleRequestImpl; + + factory _UpdateRoleRequest.fromJson(Map json) = + _$UpdateRoleRequestImpl.fromJson; + + @override + String? get name; + @override + String? get description; + @override + @JsonKey(name: 'is_active') + bool? get isActive; + + /// Create a copy of UpdateRoleRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UpdateRoleRequestImplCopyWith<_$UpdateRoleRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} + +PermissionCatalogModel _$PermissionCatalogModelFromJson( + Map json, +) { + return _PermissionCatalogModel.fromJson(json); +} + +/// @nodoc +mixin _$PermissionCatalogModel { + @JsonKey(fromJson: _idFromJson, readValue: _readId) + String get id => throw _privateConstructorUsedError; + String get module => throw _privateConstructorUsedError; + String get action => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + @JsonKey(name: 'module_id', fromJson: _idFromJsonNullable) + String? get moduleId => throw _privateConstructorUsedError; + + /// Serializes this PermissionCatalogModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PermissionCatalogModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PermissionCatalogModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PermissionCatalogModelCopyWith<$Res> { + factory $PermissionCatalogModelCopyWith( + PermissionCatalogModel value, + $Res Function(PermissionCatalogModel) then, + ) = _$PermissionCatalogModelCopyWithImpl<$Res, PermissionCatalogModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) String id, + String module, + String action, + String? description, + @JsonKey(name: 'module_id', fromJson: _idFromJsonNullable) String? moduleId, + }); +} + +/// @nodoc +class _$PermissionCatalogModelCopyWithImpl< + $Res, + $Val extends PermissionCatalogModel +> + implements $PermissionCatalogModelCopyWith<$Res> { + _$PermissionCatalogModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PermissionCatalogModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? module = null, + Object? action = null, + Object? description = freezed, + Object? moduleId = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + module: null == module + ? _value.module + : module // ignore: cast_nullable_to_non_nullable + as String, + action: null == action + ? _value.action + : action // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + moduleId: freezed == moduleId + ? _value.moduleId + : moduleId // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PermissionCatalogModelImplCopyWith<$Res> + implements $PermissionCatalogModelCopyWith<$Res> { + factory _$$PermissionCatalogModelImplCopyWith( + _$PermissionCatalogModelImpl value, + $Res Function(_$PermissionCatalogModelImpl) then, + ) = __$$PermissionCatalogModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) String id, + String module, + String action, + String? description, + @JsonKey(name: 'module_id', fromJson: _idFromJsonNullable) String? moduleId, + }); +} + +/// @nodoc +class __$$PermissionCatalogModelImplCopyWithImpl<$Res> + extends + _$PermissionCatalogModelCopyWithImpl<$Res, _$PermissionCatalogModelImpl> + implements _$$PermissionCatalogModelImplCopyWith<$Res> { + __$$PermissionCatalogModelImplCopyWithImpl( + _$PermissionCatalogModelImpl _value, + $Res Function(_$PermissionCatalogModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PermissionCatalogModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? module = null, + Object? action = null, + Object? description = freezed, + Object? moduleId = freezed, + }) { + return _then( + _$PermissionCatalogModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + module: null == module + ? _value.module + : module // ignore: cast_nullable_to_non_nullable + as String, + action: null == action + ? _value.action + : action // ignore: cast_nullable_to_non_nullable + as String, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + moduleId: freezed == moduleId + ? _value.moduleId + : moduleId // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PermissionCatalogModelImpl implements _PermissionCatalogModel { + const _$PermissionCatalogModelImpl({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) required this.id, + required this.module, + required this.action, + this.description, + @JsonKey(name: 'module_id', fromJson: _idFromJsonNullable) this.moduleId, + }); + + factory _$PermissionCatalogModelImpl.fromJson(Map json) => + _$$PermissionCatalogModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson, readValue: _readId) + final String id; + @override + final String module; + @override + final String action; + @override + final String? description; + @override + @JsonKey(name: 'module_id', fromJson: _idFromJsonNullable) + final String? moduleId; + + @override + String toString() { + return 'PermissionCatalogModel(id: $id, module: $module, action: $action, description: $description, moduleId: $moduleId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PermissionCatalogModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.module, module) || other.module == module) && + (identical(other.action, action) || other.action == action) && + (identical(other.description, description) || + other.description == description) && + (identical(other.moduleId, moduleId) || + other.moduleId == moduleId)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, module, action, description, moduleId); + + /// Create a copy of PermissionCatalogModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PermissionCatalogModelImplCopyWith<_$PermissionCatalogModelImpl> + get copyWith => + __$$PermissionCatalogModelImplCopyWithImpl<_$PermissionCatalogModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$PermissionCatalogModelImplToJson(this); + } +} + +abstract class _PermissionCatalogModel implements PermissionCatalogModel { + const factory _PermissionCatalogModel({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) + required final String id, + required final String module, + required final String action, + final String? description, + @JsonKey(name: 'module_id', fromJson: _idFromJsonNullable) + final String? moduleId, + }) = _$PermissionCatalogModelImpl; + + factory _PermissionCatalogModel.fromJson(Map json) = + _$PermissionCatalogModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson, readValue: _readId) + String get id; + @override + String get module; + @override + String get action; + @override + String? get description; + @override + @JsonKey(name: 'module_id', fromJson: _idFromJsonNullable) + String? get moduleId; + + /// Create a copy of PermissionCatalogModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PermissionCatalogModelImplCopyWith<_$PermissionCatalogModelImpl> + get copyWith => throw _privateConstructorUsedError; +} + +PermissionMatrixActions _$PermissionMatrixActionsFromJson( + Map json, +) { + return _PermissionMatrixActions.fromJson(json); +} + +/// @nodoc +mixin _$PermissionMatrixActions { + bool get view => throw _privateConstructorUsedError; + bool get edit => throw _privateConstructorUsedError; + bool get approve => throw _privateConstructorUsedError; + bool get export => throw _privateConstructorUsedError; + + /// Serializes this PermissionMatrixActions to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PermissionMatrixActions + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PermissionMatrixActionsCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PermissionMatrixActionsCopyWith<$Res> { + factory $PermissionMatrixActionsCopyWith( + PermissionMatrixActions value, + $Res Function(PermissionMatrixActions) then, + ) = _$PermissionMatrixActionsCopyWithImpl<$Res, PermissionMatrixActions>; + @useResult + $Res call({bool view, bool edit, bool approve, bool export}); +} + +/// @nodoc +class _$PermissionMatrixActionsCopyWithImpl< + $Res, + $Val extends PermissionMatrixActions +> + implements $PermissionMatrixActionsCopyWith<$Res> { + _$PermissionMatrixActionsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PermissionMatrixActions + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? view = null, + Object? edit = null, + Object? approve = null, + Object? export = null, + }) { + return _then( + _value.copyWith( + view: null == view + ? _value.view + : view // ignore: cast_nullable_to_non_nullable + as bool, + edit: null == edit + ? _value.edit + : edit // ignore: cast_nullable_to_non_nullable + as bool, + approve: null == approve + ? _value.approve + : approve // ignore: cast_nullable_to_non_nullable + as bool, + export: null == export + ? _value.export + : export // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PermissionMatrixActionsImplCopyWith<$Res> + implements $PermissionMatrixActionsCopyWith<$Res> { + factory _$$PermissionMatrixActionsImplCopyWith( + _$PermissionMatrixActionsImpl value, + $Res Function(_$PermissionMatrixActionsImpl) then, + ) = __$$PermissionMatrixActionsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool view, bool edit, bool approve, bool export}); +} + +/// @nodoc +class __$$PermissionMatrixActionsImplCopyWithImpl<$Res> + extends + _$PermissionMatrixActionsCopyWithImpl< + $Res, + _$PermissionMatrixActionsImpl + > + implements _$$PermissionMatrixActionsImplCopyWith<$Res> { + __$$PermissionMatrixActionsImplCopyWithImpl( + _$PermissionMatrixActionsImpl _value, + $Res Function(_$PermissionMatrixActionsImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PermissionMatrixActions + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? view = null, + Object? edit = null, + Object? approve = null, + Object? export = null, + }) { + return _then( + _$PermissionMatrixActionsImpl( + view: null == view + ? _value.view + : view // ignore: cast_nullable_to_non_nullable + as bool, + edit: null == edit + ? _value.edit + : edit // ignore: cast_nullable_to_non_nullable + as bool, + approve: null == approve + ? _value.approve + : approve // ignore: cast_nullable_to_non_nullable + as bool, + export: null == export + ? _value.export + : export // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PermissionMatrixActionsImpl implements _PermissionMatrixActions { + const _$PermissionMatrixActionsImpl({ + this.view = false, + this.edit = false, + this.approve = false, + this.export = false, + }); + + factory _$PermissionMatrixActionsImpl.fromJson(Map json) => + _$$PermissionMatrixActionsImplFromJson(json); + + @override + @JsonKey() + final bool view; + @override + @JsonKey() + final bool edit; + @override + @JsonKey() + final bool approve; + @override + @JsonKey() + final bool export; + + @override + String toString() { + return 'PermissionMatrixActions(view: $view, edit: $edit, approve: $approve, export: $export)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PermissionMatrixActionsImpl && + (identical(other.view, view) || other.view == view) && + (identical(other.edit, edit) || other.edit == edit) && + (identical(other.approve, approve) || other.approve == approve) && + (identical(other.export, export) || other.export == export)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, view, edit, approve, export); + + /// Create a copy of PermissionMatrixActions + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PermissionMatrixActionsImplCopyWith<_$PermissionMatrixActionsImpl> + get copyWith => + __$$PermissionMatrixActionsImplCopyWithImpl< + _$PermissionMatrixActionsImpl + >(this, _$identity); + + @override + Map toJson() { + return _$$PermissionMatrixActionsImplToJson(this); + } +} + +abstract class _PermissionMatrixActions implements PermissionMatrixActions { + const factory _PermissionMatrixActions({ + final bool view, + final bool edit, + final bool approve, + final bool export, + }) = _$PermissionMatrixActionsImpl; + + factory _PermissionMatrixActions.fromJson(Map json) = + _$PermissionMatrixActionsImpl.fromJson; + + @override + bool get view; + @override + bool get edit; + @override + bool get approve; + @override + bool get export; + + /// Create a copy of PermissionMatrixActions + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PermissionMatrixActionsImplCopyWith<_$PermissionMatrixActionsImpl> + get copyWith => throw _privateConstructorUsedError; +} + +PermissionMatrixRow _$PermissionMatrixRowFromJson(Map json) { + return _PermissionMatrixRow.fromJson(json); +} + +/// @nodoc +mixin _$PermissionMatrixRow { + @JsonKey(name: 'module_id', fromJson: _idFromJson) + String get moduleId => throw _privateConstructorUsedError; + String get module => throw _privateConstructorUsedError; + PermissionMatrixActions get actions => throw _privateConstructorUsedError; + + /// Serializes this PermissionMatrixRow to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PermissionMatrixRow + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PermissionMatrixRowCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PermissionMatrixRowCopyWith<$Res> { + factory $PermissionMatrixRowCopyWith( + PermissionMatrixRow value, + $Res Function(PermissionMatrixRow) then, + ) = _$PermissionMatrixRowCopyWithImpl<$Res, PermissionMatrixRow>; + @useResult + $Res call({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) String moduleId, + String module, + PermissionMatrixActions actions, + }); + + $PermissionMatrixActionsCopyWith<$Res> get actions; +} + +/// @nodoc +class _$PermissionMatrixRowCopyWithImpl<$Res, $Val extends PermissionMatrixRow> + implements $PermissionMatrixRowCopyWith<$Res> { + _$PermissionMatrixRowCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PermissionMatrixRow + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? moduleId = null, + Object? module = null, + Object? actions = null, + }) { + return _then( + _value.copyWith( + moduleId: null == moduleId + ? _value.moduleId + : moduleId // ignore: cast_nullable_to_non_nullable + as String, + module: null == module + ? _value.module + : module // ignore: cast_nullable_to_non_nullable + as String, + actions: null == actions + ? _value.actions + : actions // ignore: cast_nullable_to_non_nullable + as PermissionMatrixActions, + ) + as $Val, + ); + } + + /// Create a copy of PermissionMatrixRow + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PermissionMatrixActionsCopyWith<$Res> get actions { + return $PermissionMatrixActionsCopyWith<$Res>(_value.actions, (value) { + return _then(_value.copyWith(actions: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$PermissionMatrixRowImplCopyWith<$Res> + implements $PermissionMatrixRowCopyWith<$Res> { + factory _$$PermissionMatrixRowImplCopyWith( + _$PermissionMatrixRowImpl value, + $Res Function(_$PermissionMatrixRowImpl) then, + ) = __$$PermissionMatrixRowImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) String moduleId, + String module, + PermissionMatrixActions actions, + }); + + @override + $PermissionMatrixActionsCopyWith<$Res> get actions; +} + +/// @nodoc +class __$$PermissionMatrixRowImplCopyWithImpl<$Res> + extends _$PermissionMatrixRowCopyWithImpl<$Res, _$PermissionMatrixRowImpl> + implements _$$PermissionMatrixRowImplCopyWith<$Res> { + __$$PermissionMatrixRowImplCopyWithImpl( + _$PermissionMatrixRowImpl _value, + $Res Function(_$PermissionMatrixRowImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PermissionMatrixRow + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? moduleId = null, + Object? module = null, + Object? actions = null, + }) { + return _then( + _$PermissionMatrixRowImpl( + moduleId: null == moduleId + ? _value.moduleId + : moduleId // ignore: cast_nullable_to_non_nullable + as String, + module: null == module + ? _value.module + : module // ignore: cast_nullable_to_non_nullable + as String, + actions: null == actions + ? _value.actions + : actions // ignore: cast_nullable_to_non_nullable + as PermissionMatrixActions, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PermissionMatrixRowImpl implements _PermissionMatrixRow { + const _$PermissionMatrixRowImpl({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) required this.moduleId, + required this.module, + required this.actions, + }); + + factory _$PermissionMatrixRowImpl.fromJson(Map json) => + _$$PermissionMatrixRowImplFromJson(json); + + @override + @JsonKey(name: 'module_id', fromJson: _idFromJson) + final String moduleId; + @override + final String module; + @override + final PermissionMatrixActions actions; + + @override + String toString() { + return 'PermissionMatrixRow(moduleId: $moduleId, module: $module, actions: $actions)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PermissionMatrixRowImpl && + (identical(other.moduleId, moduleId) || + other.moduleId == moduleId) && + (identical(other.module, module) || other.module == module) && + (identical(other.actions, actions) || other.actions == actions)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, moduleId, module, actions); + + /// Create a copy of PermissionMatrixRow + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PermissionMatrixRowImplCopyWith<_$PermissionMatrixRowImpl> get copyWith => + __$$PermissionMatrixRowImplCopyWithImpl<_$PermissionMatrixRowImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$PermissionMatrixRowImplToJson(this); + } +} + +abstract class _PermissionMatrixRow implements PermissionMatrixRow { + const factory _PermissionMatrixRow({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) + required final String moduleId, + required final String module, + required final PermissionMatrixActions actions, + }) = _$PermissionMatrixRowImpl; + + factory _PermissionMatrixRow.fromJson(Map json) = + _$PermissionMatrixRowImpl.fromJson; + + @override + @JsonKey(name: 'module_id', fromJson: _idFromJson) + String get moduleId; + @override + String get module; + @override + PermissionMatrixActions get actions; + + /// Create a copy of PermissionMatrixRow + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PermissionMatrixRowImplCopyWith<_$PermissionMatrixRowImpl> get copyWith => + throw _privateConstructorUsedError; +} + +PermissionMatrixModel _$PermissionMatrixModelFromJson( + Map json, +) { + return _PermissionMatrixModel.fromJson(json); +} + +/// @nodoc +mixin _$PermissionMatrixModel { + @JsonKey(fromJson: _idFromJson, readValue: _readId) + String get roleId => throw _privateConstructorUsedError; + String get roleName => throw _privateConstructorUsedError; + List get matrix => throw _privateConstructorUsedError; + + /// Serializes this PermissionMatrixModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PermissionMatrixModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PermissionMatrixModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PermissionMatrixModelCopyWith<$Res> { + factory $PermissionMatrixModelCopyWith( + PermissionMatrixModel value, + $Res Function(PermissionMatrixModel) then, + ) = _$PermissionMatrixModelCopyWithImpl<$Res, PermissionMatrixModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) String roleId, + String roleName, + List matrix, + }); +} + +/// @nodoc +class _$PermissionMatrixModelCopyWithImpl< + $Res, + $Val extends PermissionMatrixModel +> + implements $PermissionMatrixModelCopyWith<$Res> { + _$PermissionMatrixModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PermissionMatrixModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? roleId = null, + Object? roleName = null, + Object? matrix = null, + }) { + return _then( + _value.copyWith( + roleId: null == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as String, + roleName: null == roleName + ? _value.roleName + : roleName // ignore: cast_nullable_to_non_nullable + as String, + matrix: null == matrix + ? _value.matrix + : matrix // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PermissionMatrixModelImplCopyWith<$Res> + implements $PermissionMatrixModelCopyWith<$Res> { + factory _$$PermissionMatrixModelImplCopyWith( + _$PermissionMatrixModelImpl value, + $Res Function(_$PermissionMatrixModelImpl) then, + ) = __$$PermissionMatrixModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) String roleId, + String roleName, + List matrix, + }); +} + +/// @nodoc +class __$$PermissionMatrixModelImplCopyWithImpl<$Res> + extends + _$PermissionMatrixModelCopyWithImpl<$Res, _$PermissionMatrixModelImpl> + implements _$$PermissionMatrixModelImplCopyWith<$Res> { + __$$PermissionMatrixModelImplCopyWithImpl( + _$PermissionMatrixModelImpl _value, + $Res Function(_$PermissionMatrixModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PermissionMatrixModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? roleId = null, + Object? roleName = null, + Object? matrix = null, + }) { + return _then( + _$PermissionMatrixModelImpl( + roleId: null == roleId + ? _value.roleId + : roleId // ignore: cast_nullable_to_non_nullable + as String, + roleName: null == roleName + ? _value.roleName + : roleName // ignore: cast_nullable_to_non_nullable + as String, + matrix: null == matrix + ? _value._matrix + : matrix // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PermissionMatrixModelImpl implements _PermissionMatrixModel { + const _$PermissionMatrixModelImpl({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) required this.roleId, + required this.roleName, + final List matrix = const [], + }) : _matrix = matrix; + + factory _$PermissionMatrixModelImpl.fromJson(Map json) => + _$$PermissionMatrixModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson, readValue: _readId) + final String roleId; + @override + final String roleName; + final List _matrix; + @override + @JsonKey() + List get matrix { + if (_matrix is EqualUnmodifiableListView) return _matrix; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_matrix); + } + + @override + String toString() { + return 'PermissionMatrixModel(roleId: $roleId, roleName: $roleName, matrix: $matrix)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PermissionMatrixModelImpl && + (identical(other.roleId, roleId) || other.roleId == roleId) && + (identical(other.roleName, roleName) || + other.roleName == roleName) && + const DeepCollectionEquality().equals(other._matrix, _matrix)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + roleId, + roleName, + const DeepCollectionEquality().hash(_matrix), + ); + + /// Create a copy of PermissionMatrixModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PermissionMatrixModelImplCopyWith<_$PermissionMatrixModelImpl> + get copyWith => + __$$PermissionMatrixModelImplCopyWithImpl<_$PermissionMatrixModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$PermissionMatrixModelImplToJson(this); + } +} + +abstract class _PermissionMatrixModel implements PermissionMatrixModel { + const factory _PermissionMatrixModel({ + @JsonKey(fromJson: _idFromJson, readValue: _readId) + required final String roleId, + required final String roleName, + final List matrix, + }) = _$PermissionMatrixModelImpl; + + factory _PermissionMatrixModel.fromJson(Map json) = + _$PermissionMatrixModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson, readValue: _readId) + String get roleId; + @override + String get roleName; + @override + List get matrix; + + /// Create a copy of PermissionMatrixModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PermissionMatrixModelImplCopyWith<_$PermissionMatrixModelImpl> + get copyWith => throw _privateConstructorUsedError; +} + +PermissionMatrixSaveRequest _$PermissionMatrixSaveRequestFromJson( + Map json, +) { + return _PermissionMatrixSaveRequest.fromJson(json); +} + +/// @nodoc +mixin _$PermissionMatrixSaveRequest { + List get matrix => + throw _privateConstructorUsedError; + + /// Serializes this PermissionMatrixSaveRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PermissionMatrixSaveRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PermissionMatrixSaveRequestCopyWith + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PermissionMatrixSaveRequestCopyWith<$Res> { + factory $PermissionMatrixSaveRequestCopyWith( + PermissionMatrixSaveRequest value, + $Res Function(PermissionMatrixSaveRequest) then, + ) = + _$PermissionMatrixSaveRequestCopyWithImpl< + $Res, + PermissionMatrixSaveRequest + >; + @useResult + $Res call({List matrix}); +} + +/// @nodoc +class _$PermissionMatrixSaveRequestCopyWithImpl< + $Res, + $Val extends PermissionMatrixSaveRequest +> + implements $PermissionMatrixSaveRequestCopyWith<$Res> { + _$PermissionMatrixSaveRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PermissionMatrixSaveRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? matrix = null}) { + return _then( + _value.copyWith( + matrix: null == matrix + ? _value.matrix + : matrix // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PermissionMatrixSaveRequestImplCopyWith<$Res> + implements $PermissionMatrixSaveRequestCopyWith<$Res> { + factory _$$PermissionMatrixSaveRequestImplCopyWith( + _$PermissionMatrixSaveRequestImpl value, + $Res Function(_$PermissionMatrixSaveRequestImpl) then, + ) = __$$PermissionMatrixSaveRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List matrix}); +} + +/// @nodoc +class __$$PermissionMatrixSaveRequestImplCopyWithImpl<$Res> + extends + _$PermissionMatrixSaveRequestCopyWithImpl< + $Res, + _$PermissionMatrixSaveRequestImpl + > + implements _$$PermissionMatrixSaveRequestImplCopyWith<$Res> { + __$$PermissionMatrixSaveRequestImplCopyWithImpl( + _$PermissionMatrixSaveRequestImpl _value, + $Res Function(_$PermissionMatrixSaveRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PermissionMatrixSaveRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? matrix = null}) { + return _then( + _$PermissionMatrixSaveRequestImpl( + matrix: null == matrix + ? _value._matrix + : matrix // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PermissionMatrixSaveRequestImpl + implements _PermissionMatrixSaveRequest { + const _$PermissionMatrixSaveRequestImpl({ + required final List matrix, + }) : _matrix = matrix; + + factory _$PermissionMatrixSaveRequestImpl.fromJson( + Map json, + ) => _$$PermissionMatrixSaveRequestImplFromJson(json); + + final List _matrix; + @override + List get matrix { + if (_matrix is EqualUnmodifiableListView) return _matrix; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_matrix); + } + + @override + String toString() { + return 'PermissionMatrixSaveRequest(matrix: $matrix)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PermissionMatrixSaveRequestImpl && + const DeepCollectionEquality().equals(other._matrix, _matrix)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_matrix)); + + /// Create a copy of PermissionMatrixSaveRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PermissionMatrixSaveRequestImplCopyWith<_$PermissionMatrixSaveRequestImpl> + get copyWith => + __$$PermissionMatrixSaveRequestImplCopyWithImpl< + _$PermissionMatrixSaveRequestImpl + >(this, _$identity); + + @override + Map toJson() { + return _$$PermissionMatrixSaveRequestImplToJson(this); + } +} + +abstract class _PermissionMatrixSaveRequest + implements PermissionMatrixSaveRequest { + const factory _PermissionMatrixSaveRequest({ + required final List matrix, + }) = _$PermissionMatrixSaveRequestImpl; + + factory _PermissionMatrixSaveRequest.fromJson(Map json) = + _$PermissionMatrixSaveRequestImpl.fromJson; + + @override + List get matrix; + + /// Create a copy of PermissionMatrixSaveRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PermissionMatrixSaveRequestImplCopyWith<_$PermissionMatrixSaveRequestImpl> + get copyWith => throw _privateConstructorUsedError; +} + +PermissionMatrixSaveRow _$PermissionMatrixSaveRowFromJson( + Map json, +) { + return _PermissionMatrixSaveRow.fromJson(json); +} + +/// @nodoc +mixin _$PermissionMatrixSaveRow { + @JsonKey(name: 'module_id', fromJson: _idFromJson) + String get moduleId => throw _privateConstructorUsedError; + PermissionMatrixActions get actions => throw _privateConstructorUsedError; + + /// Serializes this PermissionMatrixSaveRow to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PermissionMatrixSaveRow + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PermissionMatrixSaveRowCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PermissionMatrixSaveRowCopyWith<$Res> { + factory $PermissionMatrixSaveRowCopyWith( + PermissionMatrixSaveRow value, + $Res Function(PermissionMatrixSaveRow) then, + ) = _$PermissionMatrixSaveRowCopyWithImpl<$Res, PermissionMatrixSaveRow>; + @useResult + $Res call({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) String moduleId, + PermissionMatrixActions actions, + }); + + $PermissionMatrixActionsCopyWith<$Res> get actions; +} + +/// @nodoc +class _$PermissionMatrixSaveRowCopyWithImpl< + $Res, + $Val extends PermissionMatrixSaveRow +> + implements $PermissionMatrixSaveRowCopyWith<$Res> { + _$PermissionMatrixSaveRowCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PermissionMatrixSaveRow + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? moduleId = null, Object? actions = null}) { + return _then( + _value.copyWith( + moduleId: null == moduleId + ? _value.moduleId + : moduleId // ignore: cast_nullable_to_non_nullable + as String, + actions: null == actions + ? _value.actions + : actions // ignore: cast_nullable_to_non_nullable + as PermissionMatrixActions, + ) + as $Val, + ); + } + + /// Create a copy of PermissionMatrixSaveRow + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PermissionMatrixActionsCopyWith<$Res> get actions { + return $PermissionMatrixActionsCopyWith<$Res>(_value.actions, (value) { + return _then(_value.copyWith(actions: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$PermissionMatrixSaveRowImplCopyWith<$Res> + implements $PermissionMatrixSaveRowCopyWith<$Res> { + factory _$$PermissionMatrixSaveRowImplCopyWith( + _$PermissionMatrixSaveRowImpl value, + $Res Function(_$PermissionMatrixSaveRowImpl) then, + ) = __$$PermissionMatrixSaveRowImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) String moduleId, + PermissionMatrixActions actions, + }); + + @override + $PermissionMatrixActionsCopyWith<$Res> get actions; +} + +/// @nodoc +class __$$PermissionMatrixSaveRowImplCopyWithImpl<$Res> + extends + _$PermissionMatrixSaveRowCopyWithImpl< + $Res, + _$PermissionMatrixSaveRowImpl + > + implements _$$PermissionMatrixSaveRowImplCopyWith<$Res> { + __$$PermissionMatrixSaveRowImplCopyWithImpl( + _$PermissionMatrixSaveRowImpl _value, + $Res Function(_$PermissionMatrixSaveRowImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PermissionMatrixSaveRow + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? moduleId = null, Object? actions = null}) { + return _then( + _$PermissionMatrixSaveRowImpl( + moduleId: null == moduleId + ? _value.moduleId + : moduleId // ignore: cast_nullable_to_non_nullable + as String, + actions: null == actions + ? _value.actions + : actions // ignore: cast_nullable_to_non_nullable + as PermissionMatrixActions, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PermissionMatrixSaveRowImpl implements _PermissionMatrixSaveRow { + const _$PermissionMatrixSaveRowImpl({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) required this.moduleId, + required this.actions, + }); + + factory _$PermissionMatrixSaveRowImpl.fromJson(Map json) => + _$$PermissionMatrixSaveRowImplFromJson(json); + + @override + @JsonKey(name: 'module_id', fromJson: _idFromJson) + final String moduleId; + @override + final PermissionMatrixActions actions; + + @override + String toString() { + return 'PermissionMatrixSaveRow(moduleId: $moduleId, actions: $actions)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PermissionMatrixSaveRowImpl && + (identical(other.moduleId, moduleId) || + other.moduleId == moduleId) && + (identical(other.actions, actions) || other.actions == actions)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, moduleId, actions); + + /// Create a copy of PermissionMatrixSaveRow + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PermissionMatrixSaveRowImplCopyWith<_$PermissionMatrixSaveRowImpl> + get copyWith => + __$$PermissionMatrixSaveRowImplCopyWithImpl< + _$PermissionMatrixSaveRowImpl + >(this, _$identity); + + @override + Map toJson() { + return _$$PermissionMatrixSaveRowImplToJson(this); + } +} + +abstract class _PermissionMatrixSaveRow implements PermissionMatrixSaveRow { + const factory _PermissionMatrixSaveRow({ + @JsonKey(name: 'module_id', fromJson: _idFromJson) + required final String moduleId, + required final PermissionMatrixActions actions, + }) = _$PermissionMatrixSaveRowImpl; + + factory _PermissionMatrixSaveRow.fromJson(Map json) = + _$PermissionMatrixSaveRowImpl.fromJson; + + @override + @JsonKey(name: 'module_id', fromJson: _idFromJson) + String get moduleId; + @override + PermissionMatrixActions get actions; + + /// Create a copy of PermissionMatrixSaveRow + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PermissionMatrixSaveRowImplCopyWith<_$PermissionMatrixSaveRowImpl> + get copyWith => throw _privateConstructorUsedError; +} + +UpdateProfileRequest _$UpdateProfileRequestFromJson(Map json) { + return _UpdateProfileRequest.fromJson(json); +} + +/// @nodoc +mixin _$UpdateProfileRequest { + @JsonKey(name: 'full_name') + String? get fullName => throw _privateConstructorUsedError; + String? get mobile => throw _privateConstructorUsedError; + @JsonKey(name: 'avatar_url') + String? get avatarUrl => throw _privateConstructorUsedError; + + /// Serializes this UpdateProfileRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UpdateProfileRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UpdateProfileRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UpdateProfileRequestCopyWith<$Res> { + factory $UpdateProfileRequestCopyWith( + UpdateProfileRequest value, + $Res Function(UpdateProfileRequest) then, + ) = _$UpdateProfileRequestCopyWithImpl<$Res, UpdateProfileRequest>; + @useResult + $Res call({ + @JsonKey(name: 'full_name') String? fullName, + String? mobile, + @JsonKey(name: 'avatar_url') String? avatarUrl, + }); +} + +/// @nodoc +class _$UpdateProfileRequestCopyWithImpl< + $Res, + $Val extends UpdateProfileRequest +> + implements $UpdateProfileRequestCopyWith<$Res> { + _$UpdateProfileRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UpdateProfileRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? fullName = freezed, + Object? mobile = freezed, + Object? avatarUrl = freezed, + }) { + return _then( + _value.copyWith( + fullName: freezed == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String?, + mobile: freezed == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String?, + avatarUrl: freezed == avatarUrl + ? _value.avatarUrl + : avatarUrl // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UpdateProfileRequestImplCopyWith<$Res> + implements $UpdateProfileRequestCopyWith<$Res> { + factory _$$UpdateProfileRequestImplCopyWith( + _$UpdateProfileRequestImpl value, + $Res Function(_$UpdateProfileRequestImpl) then, + ) = __$$UpdateProfileRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'full_name') String? fullName, + String? mobile, + @JsonKey(name: 'avatar_url') String? avatarUrl, + }); +} + +/// @nodoc +class __$$UpdateProfileRequestImplCopyWithImpl<$Res> + extends _$UpdateProfileRequestCopyWithImpl<$Res, _$UpdateProfileRequestImpl> + implements _$$UpdateProfileRequestImplCopyWith<$Res> { + __$$UpdateProfileRequestImplCopyWithImpl( + _$UpdateProfileRequestImpl _value, + $Res Function(_$UpdateProfileRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UpdateProfileRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? fullName = freezed, + Object? mobile = freezed, + Object? avatarUrl = freezed, + }) { + return _then( + _$UpdateProfileRequestImpl( + fullName: freezed == fullName + ? _value.fullName + : fullName // ignore: cast_nullable_to_non_nullable + as String?, + mobile: freezed == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String?, + avatarUrl: freezed == avatarUrl + ? _value.avatarUrl + : avatarUrl // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UpdateProfileRequestImpl implements _UpdateProfileRequest { + const _$UpdateProfileRequestImpl({ + @JsonKey(name: 'full_name') this.fullName, + this.mobile, + @JsonKey(name: 'avatar_url') this.avatarUrl, + }); + + factory _$UpdateProfileRequestImpl.fromJson(Map json) => + _$$UpdateProfileRequestImplFromJson(json); + + @override + @JsonKey(name: 'full_name') + final String? fullName; + @override + final String? mobile; + @override + @JsonKey(name: 'avatar_url') + final String? avatarUrl; + + @override + String toString() { + return 'UpdateProfileRequest(fullName: $fullName, mobile: $mobile, avatarUrl: $avatarUrl)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UpdateProfileRequestImpl && + (identical(other.fullName, fullName) || + other.fullName == fullName) && + (identical(other.mobile, mobile) || other.mobile == mobile) && + (identical(other.avatarUrl, avatarUrl) || + other.avatarUrl == avatarUrl)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, fullName, mobile, avatarUrl); + + /// Create a copy of UpdateProfileRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UpdateProfileRequestImplCopyWith<_$UpdateProfileRequestImpl> + get copyWith => + __$$UpdateProfileRequestImplCopyWithImpl<_$UpdateProfileRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$UpdateProfileRequestImplToJson(this); + } +} + +abstract class _UpdateProfileRequest implements UpdateProfileRequest { + const factory _UpdateProfileRequest({ + @JsonKey(name: 'full_name') final String? fullName, + final String? mobile, + @JsonKey(name: 'avatar_url') final String? avatarUrl, + }) = _$UpdateProfileRequestImpl; + + factory _UpdateProfileRequest.fromJson(Map json) = + _$UpdateProfileRequestImpl.fromJson; + + @override + @JsonKey(name: 'full_name') + String? get fullName; + @override + String? get mobile; + @override + @JsonKey(name: 'avatar_url') + String? get avatarUrl; + + /// Create a copy of UpdateProfileRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UpdateProfileRequestImplCopyWith<_$UpdateProfileRequestImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/user_management_models.g.dart b/lib/shared/models/user_management_models.g.dart new file mode 100644 index 0000000..92a754d --- /dev/null +++ b/lib/shared/models/user_management_models.g.dart @@ -0,0 +1,375 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_management_models.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$UserSummaryModelImpl _$$UserSummaryModelImplFromJson( + Map json, +) => _$UserSummaryModelImpl( + totalUsers: (json['total_users'] as num?)?.toInt() ?? 0, + activeUsers: (json['active_users'] as num?)?.toInt() ?? 0, + inactiveUsers: (json['inactive_users'] as num?)?.toInt() ?? 0, + lockedUsers: (json['locked_users'] as num?)?.toInt() ?? 0, + rolesCount: (json['roles_count'] as num?)?.toInt() ?? 0, +); + +Map _$$UserSummaryModelImplToJson( + _$UserSummaryModelImpl instance, +) => { + 'total_users': instance.totalUsers, + 'active_users': instance.activeUsers, + 'inactive_users': instance.inactiveUsers, + 'locked_users': instance.lockedUsers, + 'roles_count': instance.rolesCount, +}; + +_$FilterOptionModelImpl _$$FilterOptionModelImplFromJson( + Map json, +) => _$FilterOptionModelImpl( + id: _idFromJson(json['id']), + name: json['name'] as String, + slug: json['slug'] as String?, +); + +Map _$$FilterOptionModelImplToJson( + _$FilterOptionModelImpl instance, +) => { + 'id': instance.id, + 'name': instance.name, + 'slug': instance.slug, +}; + +_$UserFiltersModelImpl _$$UserFiltersModelImplFromJson( + Map json, +) => _$UserFiltersModelImpl( + roles: + (json['roles'] as List?) + ?.map((e) => FilterOptionModel.fromJson(e as Map)) + .toList() ?? + const [], + departments: + (json['departments'] as List?) + ?.map((e) => FilterOptionModel.fromJson(e as Map)) + .toList() ?? + const [], + statuses: + (json['statuses'] as List?) + ?.map((e) => FilterOptionModel.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$$UserFiltersModelImplToJson( + _$UserFiltersModelImpl instance, +) => { + 'roles': instance.roles, + 'departments': instance.departments, + 'statuses': instance.statuses, +}; + +_$ManagedUserModelImpl _$$ManagedUserModelImplFromJson( + Map json, +) => _$ManagedUserModelImpl( + id: _idFromJson(_readId(json, 'id')), + employeeCode: _readEmployeeCode(json, 'employee_code') as String, + fullName: _readFullName(json, 'full_name') as String, + firstName: json['first_name'] as String?, + lastName: json['last_name'] as String?, + email: json['email'] as String, + mobile: json['mobile'] as String? ?? '', + roleId: _idFromJsonNullable(_readRoleId(json, 'role_id')), + roleName: _readRoleName(json, 'role_name') as String?, + departmentId: _idFromJsonNullable(json['department_id']), + departmentName: _readDepartmentName(json, 'department_name') as String?, + designationId: _idFromJsonNullable(json['designation_id']), + designationName: json['designation_name'] as String?, + plantId: _idFromJsonNullable(json['plant_id']), + plantName: _readPlantName(json, 'plant_name') as String?, + reportingTo: _idFromJsonNullable(json['reporting_to']), + reportingToName: json['reporting_to_name'] as String?, + lastLoginAt: json['last_login_at'] == null + ? null + : DateTime.parse(json['last_login_at'] as String), + initials: json['initials'] as String?, + status: json['status'] as String? ?? 'active', + isActive: json['is_active'] as bool? ?? true, + avatarUrl: json['avatar_url'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), +); + +Map _$$ManagedUserModelImplToJson( + _$ManagedUserModelImpl instance, +) => { + 'id': instance.id, + 'employee_code': instance.employeeCode, + 'full_name': instance.fullName, + 'first_name': instance.firstName, + 'last_name': instance.lastName, + 'email': instance.email, + 'mobile': instance.mobile, + 'role_id': instance.roleId, + 'role_name': instance.roleName, + 'department_id': instance.departmentId, + 'department_name': instance.departmentName, + 'designation_id': instance.designationId, + 'designation_name': instance.designationName, + 'plant_id': instance.plantId, + 'plant_name': instance.plantName, + 'reporting_to': instance.reportingTo, + 'reporting_to_name': instance.reportingToName, + 'last_login_at': instance.lastLoginAt?.toIso8601String(), + 'initials': instance.initials, + 'status': instance.status, + 'is_active': instance.isActive, + 'avatar_url': instance.avatarUrl, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), +}; + +_$CreateUserRequestImpl _$$CreateUserRequestImplFromJson( + Map json, +) => _$CreateUserRequestImpl( + employeeCode: json['employee_code'] as String, + fullName: json['full_name'] as String, + email: json['email'] as String, + password: json['password'] as String, + mobile: json['mobile'] as String?, + roleId: (json['role_id'] as num).toInt(), + departmentId: (json['department_id'] as num?)?.toInt(), + designationId: (json['designation_id'] as num?)?.toInt(), + plantId: (json['plant_id'] as num?)?.toInt(), + reportingTo: (json['reporting_to'] as num?)?.toInt(), + status: json['status'] as String? ?? 'active', + isActive: json['is_active'] as bool? ?? true, +); + +Map _$$CreateUserRequestImplToJson( + _$CreateUserRequestImpl instance, +) => { + 'employee_code': instance.employeeCode, + 'full_name': instance.fullName, + 'email': instance.email, + 'password': instance.password, + 'mobile': instance.mobile, + 'role_id': instance.roleId, + 'department_id': instance.departmentId, + 'designation_id': instance.designationId, + 'plant_id': instance.plantId, + 'reporting_to': instance.reportingTo, + 'status': instance.status, + 'is_active': instance.isActive, +}; + +_$UpdateUserRequestImpl _$$UpdateUserRequestImplFromJson( + Map json, +) => _$UpdateUserRequestImpl( + employeeCode: json['employee_code'] as String?, + fullName: json['full_name'] as String?, + email: json['email'] as String?, + password: json['password'] as String?, + mobile: json['mobile'] as String?, + roleId: (json['role_id'] as num?)?.toInt(), + departmentId: (json['department_id'] as num?)?.toInt(), + designationId: (json['designation_id'] as num?)?.toInt(), + plantId: (json['plant_id'] as num?)?.toInt(), + reportingTo: (json['reporting_to'] as num?)?.toInt(), + status: json['status'] as String?, + isActive: json['is_active'] as bool?, +); + +Map _$$UpdateUserRequestImplToJson( + _$UpdateUserRequestImpl instance, +) => { + 'employee_code': instance.employeeCode, + 'full_name': instance.fullName, + 'email': instance.email, + 'password': instance.password, + 'mobile': instance.mobile, + 'role_id': instance.roleId, + 'department_id': instance.departmentId, + 'designation_id': instance.designationId, + 'plant_id': instance.plantId, + 'reporting_to': instance.reportingTo, + 'status': instance.status, + 'is_active': instance.isActive, +}; + +_$RoleCardModelImpl _$$RoleCardModelImplFromJson(Map json) => + _$RoleCardModelImpl( + id: _idFromJson(_readId(json, 'id')), + name: json['name'] as String, + description: json['description'] as String?, + userCount: (json['user_count'] as num?)?.toInt() ?? 0, + permissionCount: (json['permission_count'] as num?)?.toInt() ?? 0, + isActive: json['is_active'] as bool? ?? true, + ); + +Map _$$RoleCardModelImplToJson(_$RoleCardModelImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'description': instance.description, + 'user_count': instance.userCount, + 'permission_count': instance.permissionCount, + 'is_active': instance.isActive, + }; + +_$CreateRoleRequestImpl _$$CreateRoleRequestImplFromJson( + Map json, +) => _$CreateRoleRequestImpl( + name: json['name'] as String, + description: json['description'] as String?, + isActive: json['is_active'] as bool? ?? true, +); + +Map _$$CreateRoleRequestImplToJson( + _$CreateRoleRequestImpl instance, +) => { + 'name': instance.name, + 'description': instance.description, + 'is_active': instance.isActive, +}; + +_$UpdateRoleRequestImpl _$$UpdateRoleRequestImplFromJson( + Map json, +) => _$UpdateRoleRequestImpl( + name: json['name'] as String?, + description: json['description'] as String?, + isActive: json['is_active'] as bool?, +); + +Map _$$UpdateRoleRequestImplToJson( + _$UpdateRoleRequestImpl instance, +) => { + 'name': instance.name, + 'description': instance.description, + 'is_active': instance.isActive, +}; + +_$PermissionCatalogModelImpl _$$PermissionCatalogModelImplFromJson( + Map json, +) => _$PermissionCatalogModelImpl( + id: _idFromJson(_readId(json, 'id')), + module: json['module'] as String, + action: json['action'] as String, + description: json['description'] as String?, + moduleId: _idFromJsonNullable(json['module_id']), +); + +Map _$$PermissionCatalogModelImplToJson( + _$PermissionCatalogModelImpl instance, +) => { + 'id': instance.id, + 'module': instance.module, + 'action': instance.action, + 'description': instance.description, + 'module_id': instance.moduleId, +}; + +_$PermissionMatrixActionsImpl _$$PermissionMatrixActionsImplFromJson( + Map json, +) => _$PermissionMatrixActionsImpl( + view: json['view'] as bool? ?? false, + edit: json['edit'] as bool? ?? false, + approve: json['approve'] as bool? ?? false, + export: json['export'] as bool? ?? false, +); + +Map _$$PermissionMatrixActionsImplToJson( + _$PermissionMatrixActionsImpl instance, +) => { + 'view': instance.view, + 'edit': instance.edit, + 'approve': instance.approve, + 'export': instance.export, +}; + +_$PermissionMatrixRowImpl _$$PermissionMatrixRowImplFromJson( + Map json, +) => _$PermissionMatrixRowImpl( + moduleId: _idFromJson(json['module_id']), + module: json['module'] as String, + actions: PermissionMatrixActions.fromJson( + json['actions'] as Map, + ), +); + +Map _$$PermissionMatrixRowImplToJson( + _$PermissionMatrixRowImpl instance, +) => { + 'module_id': instance.moduleId, + 'module': instance.module, + 'actions': instance.actions, +}; + +_$PermissionMatrixModelImpl _$$PermissionMatrixModelImplFromJson( + Map json, +) => _$PermissionMatrixModelImpl( + roleId: _idFromJson(_readId(json, 'roleId')), + roleName: json['roleName'] as String, + matrix: + (json['matrix'] as List?) + ?.map((e) => PermissionMatrixRow.fromJson(e as Map)) + .toList() ?? + const [], +); + +Map _$$PermissionMatrixModelImplToJson( + _$PermissionMatrixModelImpl instance, +) => { + 'roleId': instance.roleId, + 'roleName': instance.roleName, + 'matrix': instance.matrix, +}; + +_$PermissionMatrixSaveRequestImpl _$$PermissionMatrixSaveRequestImplFromJson( + Map json, +) => _$PermissionMatrixSaveRequestImpl( + matrix: (json['matrix'] as List) + .map((e) => PermissionMatrixSaveRow.fromJson(e as Map)) + .toList(), +); + +Map _$$PermissionMatrixSaveRequestImplToJson( + _$PermissionMatrixSaveRequestImpl instance, +) => {'matrix': instance.matrix}; + +_$PermissionMatrixSaveRowImpl _$$PermissionMatrixSaveRowImplFromJson( + Map json, +) => _$PermissionMatrixSaveRowImpl( + moduleId: _idFromJson(json['module_id']), + actions: PermissionMatrixActions.fromJson( + json['actions'] as Map, + ), +); + +Map _$$PermissionMatrixSaveRowImplToJson( + _$PermissionMatrixSaveRowImpl instance, +) => { + 'module_id': instance.moduleId, + 'actions': instance.actions, +}; + +_$UpdateProfileRequestImpl _$$UpdateProfileRequestImplFromJson( + Map json, +) => _$UpdateProfileRequestImpl( + fullName: json['full_name'] as String?, + mobile: json['mobile'] as String?, + avatarUrl: json['avatar_url'] as String?, +); + +Map _$$UpdateProfileRequestImplToJson( + _$UpdateProfileRequestImpl instance, +) => { + 'full_name': instance.fullName, + 'mobile': instance.mobile, + 'avatar_url': instance.avatarUrl, +}; diff --git a/lib/shared/models/user_model.dart b/lib/shared/models/user_model.dart new file mode 100644 index 0000000..c8cd12b --- /dev/null +++ b/lib/shared/models/user_model.dart @@ -0,0 +1,181 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +import '../../core/constants/enums.dart'; + +part 'user_model.freezed.dart'; +part 'user_model.g.dart'; + +@freezed +class UserModel with _$UserModel { + const factory UserModel({ + required String id, + @JsonKey(name: 'employee_id') required String employeeId, + required String name, + required String email, + required String mobile, + required String role, + String? department, + @Default('active') String status, + @JsonKey(name: 'company_id') String? companyId, + @JsonKey(name: 'branch_id') String? branchId, + String? avatarUrl, + @Default([]) List permissions, + DateTime? createdAt, + DateTime? updatedAt, + }) = _UserModel; + + factory UserModel.fromJson(Map json) => _$UserModelFromJson(json); + + /// Parses user objects returned by auth endpoints (`/auth/login`, `/auth/me`). + factory UserModel.fromLoginJson(Map json) { + final role = json['role_name'] ?? + json['role'] ?? + json['role_slug'] ?? + 'employee'; + final name = json['full_name'] ?? + json['name'] ?? + [ + json['first_name'], + json['last_name'], + ].whereType().where((s) => s.isNotEmpty).join(' '); + + return UserModel( + id: json['id']?.toString() ?? '', + employeeId: (json['employee_code'] ?? json['employee_id'] ?? '') as String, + name: name is String && name.isNotEmpty ? name : (json['email'] as String? ?? ''), + email: json['email'] as String? ?? '', + mobile: json['mobile']?.toString() ?? '', + role: role is String ? role : role.toString(), + department: json['department_name'] as String? ?? json['department'] as String?, + status: json['status'] as String? ?? 'active', + companyId: json['company_id']?.toString(), + branchId: json['branch_id']?.toString(), + avatarUrl: json['avatar_url'] as String? ?? json['avatarUrl'] as String?, + permissions: (json['permissions'] as List?) + ?.map((e) => e.toString()) + .toList() ?? + const [], + createdAt: json['created_at'] != null + ? DateTime.tryParse(json['created_at'] as String) + : null, + updatedAt: json['updated_at'] != null + ? DateTime.tryParse(json['updated_at'] as String) + : null, + ); + } +} + +extension UserModelX on UserModel { + UserRole get userRole => UserRole.fromValue(role); + EntityStatus get entityStatus => EntityStatus.fromValue(status); + bool get isActive => status == EntityStatus.active.value; +} + +@freezed +class AuthTokens with _$AuthTokens { + const factory AuthTokens({ + @JsonKey(name: 'access_token') required String accessToken, + @JsonKey(name: 'refresh_token') required String refreshToken, + @JsonKey(name: 'expires_in') int? expiresIn, + }) = _AuthTokens; + + factory AuthTokens.fromJson(Map json) => _$AuthTokensFromJson(json); +} + +@freezed +class LoginRequest with _$LoginRequest { + const factory LoginRequest({ + required String email, + required String password, + @JsonKey(name: 'company_code') String? companyCode, + }) = _LoginRequest; + + factory LoginRequest.fromJson(Map json) => _$LoginRequestFromJson(json); +} + +@freezed +class LoginResponse with _$LoginResponse { + const factory LoginResponse({ + required AuthTokens tokens, + required UserModel user, + @JsonKey(name: 'requires_otp') @Default(false) bool requiresOtp, + }) = _LoginResponse; + + factory LoginResponse.fromJson(Map json) => _$LoginResponseFromJson(json); + + /// Supports API `data` shapes: + /// - `{ access_token, refresh_token, user? }` + /// - `{ tokens: { access_token, refresh_token }, user }` + factory LoginResponse.fromApiData(Map data) { + final Map tokenSource; + if (data['tokens'] is Map) { + tokenSource = data['tokens'] as Map; + } else { + tokenSource = data; + } + + final accessToken = tokenSource['access_token'] as String? ?? + tokenSource['accessToken'] as String? ?? + tokenSource['token'] as String?; + final refreshToken = tokenSource['refresh_token'] as String? ?? + tokenSource['refreshToken'] as String? ?? + ''; + + if (accessToken == null || accessToken.isEmpty) { + throw StateError('Login response missing access_token'); + } + + final userJson = data['user']; + final user = userJson is Map + ? UserModel.fromLoginJson(userJson) + : UserModel.fromLoginJson({ + if (data['email'] != null) 'email': data['email'], + if (data['full_name'] != null) 'full_name': data['full_name'], + if (data['name'] != null) 'name': data['name'], + if (data['id'] != null) 'id': data['id'], + }); + + return LoginResponse( + tokens: AuthTokens( + accessToken: accessToken, + refreshToken: refreshToken, + expiresIn: (tokenSource['expires_in'] as num?)?.toInt(), + ), + user: user, + requiresOtp: data['requires_otp'] as bool? ?? false, + ); + } +} + +@freezed +class OtpVerifyRequest with _$OtpVerifyRequest { + const factory OtpVerifyRequest({ + required String email, + required String otp, + }) = _OtpVerifyRequest; + + factory OtpVerifyRequest.fromJson(Map json) => + _$OtpVerifyRequestFromJson(json); +} + +@freezed +class ChangePasswordRequest with _$ChangePasswordRequest { + const factory ChangePasswordRequest({ + @JsonKey(name: 'current_password') required String currentPassword, + @JsonKey(name: 'new_password') required String newPassword, + @JsonKey(name: 'confirm_password') required String confirmPassword, + }) = _ChangePasswordRequest; + + factory ChangePasswordRequest.fromJson(Map json) => + _$ChangePasswordRequestFromJson(json); +} + +@freezed +class ForgotPasswordRequest with _$ForgotPasswordRequest { + const factory ForgotPasswordRequest({ + required String email, + }) = _ForgotPasswordRequest; + + factory ForgotPasswordRequest.fromJson(Map json) => + _$ForgotPasswordRequestFromJson(json); +} diff --git a/lib/shared/models/user_model.freezed.dart b/lib/shared/models/user_model.freezed.dart new file mode 100644 index 0000000..6a00ba9 --- /dev/null +++ b/lib/shared/models/user_model.freezed.dart @@ -0,0 +1,1698 @@ +// 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 'user_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(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', +); + +UserModel _$UserModelFromJson(Map json) { + return _UserModel.fromJson(json); +} + +/// @nodoc +mixin _$UserModel { + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'employee_id') + String get employeeId => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + String get mobile => throw _privateConstructorUsedError; + String get role => throw _privateConstructorUsedError; + String? get department => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + @JsonKey(name: 'company_id') + String? get companyId => throw _privateConstructorUsedError; + @JsonKey(name: 'branch_id') + String? get branchId => throw _privateConstructorUsedError; + String? get avatarUrl => throw _privateConstructorUsedError; + List get permissions => throw _privateConstructorUsedError; + DateTime? get createdAt => throw _privateConstructorUsedError; + DateTime? get updatedAt => throw _privateConstructorUsedError; + + /// Serializes this UserModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of UserModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserModelCopyWith<$Res> { + factory $UserModelCopyWith(UserModel value, $Res Function(UserModel) then) = + _$UserModelCopyWithImpl<$Res, UserModel>; + @useResult + $Res call({ + String id, + @JsonKey(name: 'employee_id') String employeeId, + String name, + String email, + String mobile, + String role, + String? department, + String status, + @JsonKey(name: 'company_id') String? companyId, + @JsonKey(name: 'branch_id') String? branchId, + String? avatarUrl, + List permissions, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class _$UserModelCopyWithImpl<$Res, $Val extends UserModel> + implements $UserModelCopyWith<$Res> { + _$UserModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? employeeId = null, + Object? name = null, + Object? email = null, + Object? mobile = null, + Object? role = null, + Object? department = freezed, + Object? status = null, + Object? companyId = freezed, + Object? branchId = freezed, + Object? avatarUrl = freezed, + Object? permissions = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + employeeId: null == employeeId + ? _value.employeeId + : employeeId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + mobile: null == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String, + department: freezed == department + ? _value.department + : department // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + companyId: freezed == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as String?, + branchId: freezed == branchId + ? _value.branchId + : branchId // ignore: cast_nullable_to_non_nullable + as String?, + avatarUrl: freezed == avatarUrl + ? _value.avatarUrl + : avatarUrl // ignore: cast_nullable_to_non_nullable + as String?, + permissions: null == permissions + ? _value.permissions + : permissions // ignore: cast_nullable_to_non_nullable + as List, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UserModelImplCopyWith<$Res> + implements $UserModelCopyWith<$Res> { + factory _$$UserModelImplCopyWith( + _$UserModelImpl value, + $Res Function(_$UserModelImpl) then, + ) = __$$UserModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String id, + @JsonKey(name: 'employee_id') String employeeId, + String name, + String email, + String mobile, + String role, + String? department, + String status, + @JsonKey(name: 'company_id') String? companyId, + @JsonKey(name: 'branch_id') String? branchId, + String? avatarUrl, + List permissions, + DateTime? createdAt, + DateTime? updatedAt, + }); +} + +/// @nodoc +class __$$UserModelImplCopyWithImpl<$Res> + extends _$UserModelCopyWithImpl<$Res, _$UserModelImpl> + implements _$$UserModelImplCopyWith<$Res> { + __$$UserModelImplCopyWithImpl( + _$UserModelImpl _value, + $Res Function(_$UserModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UserModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? employeeId = null, + Object? name = null, + Object? email = null, + Object? mobile = null, + Object? role = null, + Object? department = freezed, + Object? status = null, + Object? companyId = freezed, + Object? branchId = freezed, + Object? avatarUrl = freezed, + Object? permissions = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + }) { + return _then( + _$UserModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + employeeId: null == employeeId + ? _value.employeeId + : employeeId // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + mobile: null == mobile + ? _value.mobile + : mobile // ignore: cast_nullable_to_non_nullable + as String, + role: null == role + ? _value.role + : role // ignore: cast_nullable_to_non_nullable + as String, + department: freezed == department + ? _value.department + : department // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + companyId: freezed == companyId + ? _value.companyId + : companyId // ignore: cast_nullable_to_non_nullable + as String?, + branchId: freezed == branchId + ? _value.branchId + : branchId // ignore: cast_nullable_to_non_nullable + as String?, + avatarUrl: freezed == avatarUrl + ? _value.avatarUrl + : avatarUrl // ignore: cast_nullable_to_non_nullable + as String?, + permissions: null == permissions + ? _value._permissions + : permissions // ignore: cast_nullable_to_non_nullable + as List, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$UserModelImpl implements _UserModel { + const _$UserModelImpl({ + required this.id, + @JsonKey(name: 'employee_id') required this.employeeId, + required this.name, + required this.email, + required this.mobile, + required this.role, + this.department, + this.status = 'active', + @JsonKey(name: 'company_id') this.companyId, + @JsonKey(name: 'branch_id') this.branchId, + this.avatarUrl, + final List permissions = const [], + this.createdAt, + this.updatedAt, + }) : _permissions = permissions; + + factory _$UserModelImpl.fromJson(Map json) => + _$$UserModelImplFromJson(json); + + @override + final String id; + @override + @JsonKey(name: 'employee_id') + final String employeeId; + @override + final String name; + @override + final String email; + @override + final String mobile; + @override + final String role; + @override + final String? department; + @override + @JsonKey() + final String status; + @override + @JsonKey(name: 'company_id') + final String? companyId; + @override + @JsonKey(name: 'branch_id') + final String? branchId; + @override + final String? avatarUrl; + final List _permissions; + @override + @JsonKey() + List get permissions { + if (_permissions is EqualUnmodifiableListView) return _permissions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_permissions); + } + + @override + final DateTime? createdAt; + @override + final DateTime? updatedAt; + + @override + String toString() { + return 'UserModel(id: $id, employeeId: $employeeId, name: $name, email: $email, mobile: $mobile, role: $role, department: $department, status: $status, companyId: $companyId, branchId: $branchId, avatarUrl: $avatarUrl, permissions: $permissions, createdAt: $createdAt, updatedAt: $updatedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.employeeId, employeeId) || + other.employeeId == employeeId) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.mobile, mobile) || other.mobile == mobile) && + (identical(other.role, role) || other.role == role) && + (identical(other.department, department) || + other.department == department) && + (identical(other.status, status) || other.status == status) && + (identical(other.companyId, companyId) || + other.companyId == companyId) && + (identical(other.branchId, branchId) || + other.branchId == branchId) && + (identical(other.avatarUrl, avatarUrl) || + other.avatarUrl == avatarUrl) && + const DeepCollectionEquality().equals( + other._permissions, + _permissions, + ) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + employeeId, + name, + email, + mobile, + role, + department, + status, + companyId, + branchId, + avatarUrl, + const DeepCollectionEquality().hash(_permissions), + createdAt, + updatedAt, + ); + + /// Create a copy of UserModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserModelImplCopyWith<_$UserModelImpl> get copyWith => + __$$UserModelImplCopyWithImpl<_$UserModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$UserModelImplToJson(this); + } +} + +abstract class _UserModel implements UserModel { + const factory _UserModel({ + required final String id, + @JsonKey(name: 'employee_id') required final String employeeId, + required final String name, + required final String email, + required final String mobile, + required final String role, + final String? department, + final String status, + @JsonKey(name: 'company_id') final String? companyId, + @JsonKey(name: 'branch_id') final String? branchId, + final String? avatarUrl, + final List permissions, + final DateTime? createdAt, + final DateTime? updatedAt, + }) = _$UserModelImpl; + + factory _UserModel.fromJson(Map json) = + _$UserModelImpl.fromJson; + + @override + String get id; + @override + @JsonKey(name: 'employee_id') + String get employeeId; + @override + String get name; + @override + String get email; + @override + String get mobile; + @override + String get role; + @override + String? get department; + @override + String get status; + @override + @JsonKey(name: 'company_id') + String? get companyId; + @override + @JsonKey(name: 'branch_id') + String? get branchId; + @override + String? get avatarUrl; + @override + List get permissions; + @override + DateTime? get createdAt; + @override + DateTime? get updatedAt; + + /// Create a copy of UserModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserModelImplCopyWith<_$UserModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +AuthTokens _$AuthTokensFromJson(Map json) { + return _AuthTokens.fromJson(json); +} + +/// @nodoc +mixin _$AuthTokens { + @JsonKey(name: 'access_token') + String get accessToken => throw _privateConstructorUsedError; + @JsonKey(name: 'refresh_token') + String get refreshToken => throw _privateConstructorUsedError; + @JsonKey(name: 'expires_in') + int? get expiresIn => throw _privateConstructorUsedError; + + /// Serializes this AuthTokens to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of AuthTokens + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AuthTokensCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AuthTokensCopyWith<$Res> { + factory $AuthTokensCopyWith( + AuthTokens value, + $Res Function(AuthTokens) then, + ) = _$AuthTokensCopyWithImpl<$Res, AuthTokens>; + @useResult + $Res call({ + @JsonKey(name: 'access_token') String accessToken, + @JsonKey(name: 'refresh_token') String refreshToken, + @JsonKey(name: 'expires_in') int? expiresIn, + }); +} + +/// @nodoc +class _$AuthTokensCopyWithImpl<$Res, $Val extends AuthTokens> + implements $AuthTokensCopyWith<$Res> { + _$AuthTokensCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AuthTokens + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessToken = null, + Object? refreshToken = null, + Object? expiresIn = freezed, + }) { + return _then( + _value.copyWith( + accessToken: null == accessToken + ? _value.accessToken + : accessToken // ignore: cast_nullable_to_non_nullable + as String, + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + expiresIn: freezed == expiresIn + ? _value.expiresIn + : expiresIn // ignore: cast_nullable_to_non_nullable + as int?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$AuthTokensImplCopyWith<$Res> + implements $AuthTokensCopyWith<$Res> { + factory _$$AuthTokensImplCopyWith( + _$AuthTokensImpl value, + $Res Function(_$AuthTokensImpl) then, + ) = __$$AuthTokensImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'access_token') String accessToken, + @JsonKey(name: 'refresh_token') String refreshToken, + @JsonKey(name: 'expires_in') int? expiresIn, + }); +} + +/// @nodoc +class __$$AuthTokensImplCopyWithImpl<$Res> + extends _$AuthTokensCopyWithImpl<$Res, _$AuthTokensImpl> + implements _$$AuthTokensImplCopyWith<$Res> { + __$$AuthTokensImplCopyWithImpl( + _$AuthTokensImpl _value, + $Res Function(_$AuthTokensImpl) _then, + ) : super(_value, _then); + + /// Create a copy of AuthTokens + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessToken = null, + Object? refreshToken = null, + Object? expiresIn = freezed, + }) { + return _then( + _$AuthTokensImpl( + accessToken: null == accessToken + ? _value.accessToken + : accessToken // ignore: cast_nullable_to_non_nullable + as String, + refreshToken: null == refreshToken + ? _value.refreshToken + : refreshToken // ignore: cast_nullable_to_non_nullable + as String, + expiresIn: freezed == expiresIn + ? _value.expiresIn + : expiresIn // ignore: cast_nullable_to_non_nullable + as int?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$AuthTokensImpl implements _AuthTokens { + const _$AuthTokensImpl({ + @JsonKey(name: 'access_token') required this.accessToken, + @JsonKey(name: 'refresh_token') required this.refreshToken, + @JsonKey(name: 'expires_in') this.expiresIn, + }); + + factory _$AuthTokensImpl.fromJson(Map json) => + _$$AuthTokensImplFromJson(json); + + @override + @JsonKey(name: 'access_token') + final String accessToken; + @override + @JsonKey(name: 'refresh_token') + final String refreshToken; + @override + @JsonKey(name: 'expires_in') + final int? expiresIn; + + @override + String toString() { + return 'AuthTokens(accessToken: $accessToken, refreshToken: $refreshToken, expiresIn: $expiresIn)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AuthTokensImpl && + (identical(other.accessToken, accessToken) || + other.accessToken == accessToken) && + (identical(other.refreshToken, refreshToken) || + other.refreshToken == refreshToken) && + (identical(other.expiresIn, expiresIn) || + other.expiresIn == expiresIn)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, accessToken, refreshToken, expiresIn); + + /// Create a copy of AuthTokens + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AuthTokensImplCopyWith<_$AuthTokensImpl> get copyWith => + __$$AuthTokensImplCopyWithImpl<_$AuthTokensImpl>(this, _$identity); + + @override + Map toJson() { + return _$$AuthTokensImplToJson(this); + } +} + +abstract class _AuthTokens implements AuthTokens { + const factory _AuthTokens({ + @JsonKey(name: 'access_token') required final String accessToken, + @JsonKey(name: 'refresh_token') required final String refreshToken, + @JsonKey(name: 'expires_in') final int? expiresIn, + }) = _$AuthTokensImpl; + + factory _AuthTokens.fromJson(Map json) = + _$AuthTokensImpl.fromJson; + + @override + @JsonKey(name: 'access_token') + String get accessToken; + @override + @JsonKey(name: 'refresh_token') + String get refreshToken; + @override + @JsonKey(name: 'expires_in') + int? get expiresIn; + + /// Create a copy of AuthTokens + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AuthTokensImplCopyWith<_$AuthTokensImpl> get copyWith => + throw _privateConstructorUsedError; +} + +LoginRequest _$LoginRequestFromJson(Map json) { + return _LoginRequest.fromJson(json); +} + +/// @nodoc +mixin _$LoginRequest { + String get email => throw _privateConstructorUsedError; + String get password => throw _privateConstructorUsedError; + @JsonKey(name: 'company_code') + String? get companyCode => throw _privateConstructorUsedError; + + /// Serializes this LoginRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LoginRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoginRequestCopyWith<$Res> { + factory $LoginRequestCopyWith( + LoginRequest value, + $Res Function(LoginRequest) then, + ) = _$LoginRequestCopyWithImpl<$Res, LoginRequest>; + @useResult + $Res call({ + String email, + String password, + @JsonKey(name: 'company_code') String? companyCode, + }); +} + +/// @nodoc +class _$LoginRequestCopyWithImpl<$Res, $Val extends LoginRequest> + implements $LoginRequestCopyWith<$Res> { + _$LoginRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? email = null, + Object? password = null, + Object? companyCode = freezed, + }) { + return _then( + _value.copyWith( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + companyCode: freezed == companyCode + ? _value.companyCode + : companyCode // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$LoginRequestImplCopyWith<$Res> + implements $LoginRequestCopyWith<$Res> { + factory _$$LoginRequestImplCopyWith( + _$LoginRequestImpl value, + $Res Function(_$LoginRequestImpl) then, + ) = __$$LoginRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + String email, + String password, + @JsonKey(name: 'company_code') String? companyCode, + }); +} + +/// @nodoc +class __$$LoginRequestImplCopyWithImpl<$Res> + extends _$LoginRequestCopyWithImpl<$Res, _$LoginRequestImpl> + implements _$$LoginRequestImplCopyWith<$Res> { + __$$LoginRequestImplCopyWithImpl( + _$LoginRequestImpl _value, + $Res Function(_$LoginRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? email = null, + Object? password = null, + Object? companyCode = freezed, + }) { + return _then( + _$LoginRequestImpl( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + companyCode: freezed == companyCode + ? _value.companyCode + : companyCode // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$LoginRequestImpl implements _LoginRequest { + const _$LoginRequestImpl({ + required this.email, + required this.password, + @JsonKey(name: 'company_code') this.companyCode, + }); + + factory _$LoginRequestImpl.fromJson(Map json) => + _$$LoginRequestImplFromJson(json); + + @override + final String email; + @override + final String password; + @override + @JsonKey(name: 'company_code') + final String? companyCode; + + @override + String toString() { + return 'LoginRequest(email: $email, password: $password, companyCode: $companyCode)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginRequestImpl && + (identical(other.email, email) || other.email == email) && + (identical(other.password, password) || + other.password == password) && + (identical(other.companyCode, companyCode) || + other.companyCode == companyCode)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, email, password, companyCode); + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith => + __$$LoginRequestImplCopyWithImpl<_$LoginRequestImpl>(this, _$identity); + + @override + Map toJson() { + return _$$LoginRequestImplToJson(this); + } +} + +abstract class _LoginRequest implements LoginRequest { + const factory _LoginRequest({ + required final String email, + required final String password, + @JsonKey(name: 'company_code') final String? companyCode, + }) = _$LoginRequestImpl; + + factory _LoginRequest.fromJson(Map json) = + _$LoginRequestImpl.fromJson; + + @override + String get email; + @override + String get password; + @override + @JsonKey(name: 'company_code') + String? get companyCode; + + /// Create a copy of LoginRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LoginRequestImplCopyWith<_$LoginRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} + +LoginResponse _$LoginResponseFromJson(Map json) { + return _LoginResponse.fromJson(json); +} + +/// @nodoc +mixin _$LoginResponse { + AuthTokens get tokens => throw _privateConstructorUsedError; + UserModel get user => throw _privateConstructorUsedError; + @JsonKey(name: 'requires_otp') + bool get requiresOtp => throw _privateConstructorUsedError; + + /// Serializes this LoginResponse to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $LoginResponseCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoginResponseCopyWith<$Res> { + factory $LoginResponseCopyWith( + LoginResponse value, + $Res Function(LoginResponse) then, + ) = _$LoginResponseCopyWithImpl<$Res, LoginResponse>; + @useResult + $Res call({ + AuthTokens tokens, + UserModel user, + @JsonKey(name: 'requires_otp') bool requiresOtp, + }); + + $AuthTokensCopyWith<$Res> get tokens; + $UserModelCopyWith<$Res> get user; +} + +/// @nodoc +class _$LoginResponseCopyWithImpl<$Res, $Val extends LoginResponse> + implements $LoginResponseCopyWith<$Res> { + _$LoginResponseCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tokens = null, + Object? user = null, + Object? requiresOtp = null, + }) { + return _then( + _value.copyWith( + tokens: null == tokens + ? _value.tokens + : tokens // ignore: cast_nullable_to_non_nullable + as AuthTokens, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as UserModel, + requiresOtp: null == requiresOtp + ? _value.requiresOtp + : requiresOtp // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AuthTokensCopyWith<$Res> get tokens { + return $AuthTokensCopyWith<$Res>(_value.tokens, (value) { + return _then(_value.copyWith(tokens: value) as $Val); + }); + } + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $UserModelCopyWith<$Res> get user { + return $UserModelCopyWith<$Res>(_value.user, (value) { + return _then(_value.copyWith(user: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$LoginResponseImplCopyWith<$Res> + implements $LoginResponseCopyWith<$Res> { + factory _$$LoginResponseImplCopyWith( + _$LoginResponseImpl value, + $Res Function(_$LoginResponseImpl) then, + ) = __$$LoginResponseImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + AuthTokens tokens, + UserModel user, + @JsonKey(name: 'requires_otp') bool requiresOtp, + }); + + @override + $AuthTokensCopyWith<$Res> get tokens; + @override + $UserModelCopyWith<$Res> get user; +} + +/// @nodoc +class __$$LoginResponseImplCopyWithImpl<$Res> + extends _$LoginResponseCopyWithImpl<$Res, _$LoginResponseImpl> + implements _$$LoginResponseImplCopyWith<$Res> { + __$$LoginResponseImplCopyWithImpl( + _$LoginResponseImpl _value, + $Res Function(_$LoginResponseImpl) _then, + ) : super(_value, _then); + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? tokens = null, + Object? user = null, + Object? requiresOtp = null, + }) { + return _then( + _$LoginResponseImpl( + tokens: null == tokens + ? _value.tokens + : tokens // ignore: cast_nullable_to_non_nullable + as AuthTokens, + user: null == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as UserModel, + requiresOtp: null == requiresOtp + ? _value.requiresOtp + : requiresOtp // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$LoginResponseImpl implements _LoginResponse { + const _$LoginResponseImpl({ + required this.tokens, + required this.user, + @JsonKey(name: 'requires_otp') this.requiresOtp = false, + }); + + factory _$LoginResponseImpl.fromJson(Map json) => + _$$LoginResponseImplFromJson(json); + + @override + final AuthTokens tokens; + @override + final UserModel user; + @override + @JsonKey(name: 'requires_otp') + final bool requiresOtp; + + @override + String toString() { + return 'LoginResponse(tokens: $tokens, user: $user, requiresOtp: $requiresOtp)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginResponseImpl && + (identical(other.tokens, tokens) || other.tokens == tokens) && + (identical(other.user, user) || other.user == user) && + (identical(other.requiresOtp, requiresOtp) || + other.requiresOtp == requiresOtp)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, tokens, user, requiresOtp); + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith => + __$$LoginResponseImplCopyWithImpl<_$LoginResponseImpl>(this, _$identity); + + @override + Map toJson() { + return _$$LoginResponseImplToJson(this); + } +} + +abstract class _LoginResponse implements LoginResponse { + const factory _LoginResponse({ + required final AuthTokens tokens, + required final UserModel user, + @JsonKey(name: 'requires_otp') final bool requiresOtp, + }) = _$LoginResponseImpl; + + factory _LoginResponse.fromJson(Map json) = + _$LoginResponseImpl.fromJson; + + @override + AuthTokens get tokens; + @override + UserModel get user; + @override + @JsonKey(name: 'requires_otp') + bool get requiresOtp; + + /// Create a copy of LoginResponse + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$LoginResponseImplCopyWith<_$LoginResponseImpl> get copyWith => + throw _privateConstructorUsedError; +} + +OtpVerifyRequest _$OtpVerifyRequestFromJson(Map json) { + return _OtpVerifyRequest.fromJson(json); +} + +/// @nodoc +mixin _$OtpVerifyRequest { + String get email => throw _privateConstructorUsedError; + String get otp => throw _privateConstructorUsedError; + + /// Serializes this OtpVerifyRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of OtpVerifyRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OtpVerifyRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OtpVerifyRequestCopyWith<$Res> { + factory $OtpVerifyRequestCopyWith( + OtpVerifyRequest value, + $Res Function(OtpVerifyRequest) then, + ) = _$OtpVerifyRequestCopyWithImpl<$Res, OtpVerifyRequest>; + @useResult + $Res call({String email, String otp}); +} + +/// @nodoc +class _$OtpVerifyRequestCopyWithImpl<$Res, $Val extends OtpVerifyRequest> + implements $OtpVerifyRequestCopyWith<$Res> { + _$OtpVerifyRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of OtpVerifyRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? email = null, Object? otp = null}) { + return _then( + _value.copyWith( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + otp: null == otp + ? _value.otp + : otp // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$OtpVerifyRequestImplCopyWith<$Res> + implements $OtpVerifyRequestCopyWith<$Res> { + factory _$$OtpVerifyRequestImplCopyWith( + _$OtpVerifyRequestImpl value, + $Res Function(_$OtpVerifyRequestImpl) then, + ) = __$$OtpVerifyRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String email, String otp}); +} + +/// @nodoc +class __$$OtpVerifyRequestImplCopyWithImpl<$Res> + extends _$OtpVerifyRequestCopyWithImpl<$Res, _$OtpVerifyRequestImpl> + implements _$$OtpVerifyRequestImplCopyWith<$Res> { + __$$OtpVerifyRequestImplCopyWithImpl( + _$OtpVerifyRequestImpl _value, + $Res Function(_$OtpVerifyRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of OtpVerifyRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? email = null, Object? otp = null}) { + return _then( + _$OtpVerifyRequestImpl( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + otp: null == otp + ? _value.otp + : otp // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$OtpVerifyRequestImpl implements _OtpVerifyRequest { + const _$OtpVerifyRequestImpl({required this.email, required this.otp}); + + factory _$OtpVerifyRequestImpl.fromJson(Map json) => + _$$OtpVerifyRequestImplFromJson(json); + + @override + final String email; + @override + final String otp; + + @override + String toString() { + return 'OtpVerifyRequest(email: $email, otp: $otp)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OtpVerifyRequestImpl && + (identical(other.email, email) || other.email == email) && + (identical(other.otp, otp) || other.otp == otp)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, email, otp); + + /// Create a copy of OtpVerifyRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$OtpVerifyRequestImplCopyWith<_$OtpVerifyRequestImpl> get copyWith => + __$$OtpVerifyRequestImplCopyWithImpl<_$OtpVerifyRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$OtpVerifyRequestImplToJson(this); + } +} + +abstract class _OtpVerifyRequest implements OtpVerifyRequest { + const factory _OtpVerifyRequest({ + required final String email, + required final String otp, + }) = _$OtpVerifyRequestImpl; + + factory _OtpVerifyRequest.fromJson(Map json) = + _$OtpVerifyRequestImpl.fromJson; + + @override + String get email; + @override + String get otp; + + /// Create a copy of OtpVerifyRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$OtpVerifyRequestImplCopyWith<_$OtpVerifyRequestImpl> get copyWith => + throw _privateConstructorUsedError; +} + +ChangePasswordRequest _$ChangePasswordRequestFromJson( + Map json, +) { + return _ChangePasswordRequest.fromJson(json); +} + +/// @nodoc +mixin _$ChangePasswordRequest { + @JsonKey(name: 'current_password') + String get currentPassword => throw _privateConstructorUsedError; + @JsonKey(name: 'new_password') + String get newPassword => throw _privateConstructorUsedError; + @JsonKey(name: 'confirm_password') + String get confirmPassword => throw _privateConstructorUsedError; + + /// Serializes this ChangePasswordRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ChangePasswordRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ChangePasswordRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChangePasswordRequestCopyWith<$Res> { + factory $ChangePasswordRequestCopyWith( + ChangePasswordRequest value, + $Res Function(ChangePasswordRequest) then, + ) = _$ChangePasswordRequestCopyWithImpl<$Res, ChangePasswordRequest>; + @useResult + $Res call({ + @JsonKey(name: 'current_password') String currentPassword, + @JsonKey(name: 'new_password') String newPassword, + @JsonKey(name: 'confirm_password') String confirmPassword, + }); +} + +/// @nodoc +class _$ChangePasswordRequestCopyWithImpl< + $Res, + $Val extends ChangePasswordRequest +> + implements $ChangePasswordRequestCopyWith<$Res> { + _$ChangePasswordRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ChangePasswordRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentPassword = null, + Object? newPassword = null, + Object? confirmPassword = null, + }) { + return _then( + _value.copyWith( + currentPassword: null == currentPassword + ? _value.currentPassword + : currentPassword // ignore: cast_nullable_to_non_nullable + as String, + newPassword: null == newPassword + ? _value.newPassword + : newPassword // ignore: cast_nullable_to_non_nullable + as String, + confirmPassword: null == confirmPassword + ? _value.confirmPassword + : confirmPassword // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ChangePasswordRequestImplCopyWith<$Res> + implements $ChangePasswordRequestCopyWith<$Res> { + factory _$$ChangePasswordRequestImplCopyWith( + _$ChangePasswordRequestImpl value, + $Res Function(_$ChangePasswordRequestImpl) then, + ) = __$$ChangePasswordRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(name: 'current_password') String currentPassword, + @JsonKey(name: 'new_password') String newPassword, + @JsonKey(name: 'confirm_password') String confirmPassword, + }); +} + +/// @nodoc +class __$$ChangePasswordRequestImplCopyWithImpl<$Res> + extends + _$ChangePasswordRequestCopyWithImpl<$Res, _$ChangePasswordRequestImpl> + implements _$$ChangePasswordRequestImplCopyWith<$Res> { + __$$ChangePasswordRequestImplCopyWithImpl( + _$ChangePasswordRequestImpl _value, + $Res Function(_$ChangePasswordRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ChangePasswordRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? currentPassword = null, + Object? newPassword = null, + Object? confirmPassword = null, + }) { + return _then( + _$ChangePasswordRequestImpl( + currentPassword: null == currentPassword + ? _value.currentPassword + : currentPassword // ignore: cast_nullable_to_non_nullable + as String, + newPassword: null == newPassword + ? _value.newPassword + : newPassword // ignore: cast_nullable_to_non_nullable + as String, + confirmPassword: null == confirmPassword + ? _value.confirmPassword + : confirmPassword // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ChangePasswordRequestImpl implements _ChangePasswordRequest { + const _$ChangePasswordRequestImpl({ + @JsonKey(name: 'current_password') required this.currentPassword, + @JsonKey(name: 'new_password') required this.newPassword, + @JsonKey(name: 'confirm_password') required this.confirmPassword, + }); + + factory _$ChangePasswordRequestImpl.fromJson(Map json) => + _$$ChangePasswordRequestImplFromJson(json); + + @override + @JsonKey(name: 'current_password') + final String currentPassword; + @override + @JsonKey(name: 'new_password') + final String newPassword; + @override + @JsonKey(name: 'confirm_password') + final String confirmPassword; + + @override + String toString() { + return 'ChangePasswordRequest(currentPassword: $currentPassword, newPassword: $newPassword, confirmPassword: $confirmPassword)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChangePasswordRequestImpl && + (identical(other.currentPassword, currentPassword) || + other.currentPassword == currentPassword) && + (identical(other.newPassword, newPassword) || + other.newPassword == newPassword) && + (identical(other.confirmPassword, confirmPassword) || + other.confirmPassword == confirmPassword)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, currentPassword, newPassword, confirmPassword); + + /// Create a copy of ChangePasswordRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ChangePasswordRequestImplCopyWith<_$ChangePasswordRequestImpl> + get copyWith => + __$$ChangePasswordRequestImplCopyWithImpl<_$ChangePasswordRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ChangePasswordRequestImplToJson(this); + } +} + +abstract class _ChangePasswordRequest implements ChangePasswordRequest { + const factory _ChangePasswordRequest({ + @JsonKey(name: 'current_password') required final String currentPassword, + @JsonKey(name: 'new_password') required final String newPassword, + @JsonKey(name: 'confirm_password') required final String confirmPassword, + }) = _$ChangePasswordRequestImpl; + + factory _ChangePasswordRequest.fromJson(Map json) = + _$ChangePasswordRequestImpl.fromJson; + + @override + @JsonKey(name: 'current_password') + String get currentPassword; + @override + @JsonKey(name: 'new_password') + String get newPassword; + @override + @JsonKey(name: 'confirm_password') + String get confirmPassword; + + /// Create a copy of ChangePasswordRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ChangePasswordRequestImplCopyWith<_$ChangePasswordRequestImpl> + get copyWith => throw _privateConstructorUsedError; +} + +ForgotPasswordRequest _$ForgotPasswordRequestFromJson( + Map json, +) { + return _ForgotPasswordRequest.fromJson(json); +} + +/// @nodoc +mixin _$ForgotPasswordRequest { + String get email => throw _privateConstructorUsedError; + + /// Serializes this ForgotPasswordRequest to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of ForgotPasswordRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ForgotPasswordRequestCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ForgotPasswordRequestCopyWith<$Res> { + factory $ForgotPasswordRequestCopyWith( + ForgotPasswordRequest value, + $Res Function(ForgotPasswordRequest) then, + ) = _$ForgotPasswordRequestCopyWithImpl<$Res, ForgotPasswordRequest>; + @useResult + $Res call({String email}); +} + +/// @nodoc +class _$ForgotPasswordRequestCopyWithImpl< + $Res, + $Val extends ForgotPasswordRequest +> + implements $ForgotPasswordRequestCopyWith<$Res> { + _$ForgotPasswordRequestCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ForgotPasswordRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? email = null}) { + return _then( + _value.copyWith( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$ForgotPasswordRequestImplCopyWith<$Res> + implements $ForgotPasswordRequestCopyWith<$Res> { + factory _$$ForgotPasswordRequestImplCopyWith( + _$ForgotPasswordRequestImpl value, + $Res Function(_$ForgotPasswordRequestImpl) then, + ) = __$$ForgotPasswordRequestImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String email}); +} + +/// @nodoc +class __$$ForgotPasswordRequestImplCopyWithImpl<$Res> + extends + _$ForgotPasswordRequestCopyWithImpl<$Res, _$ForgotPasswordRequestImpl> + implements _$$ForgotPasswordRequestImplCopyWith<$Res> { + __$$ForgotPasswordRequestImplCopyWithImpl( + _$ForgotPasswordRequestImpl _value, + $Res Function(_$ForgotPasswordRequestImpl) _then, + ) : super(_value, _then); + + /// Create a copy of ForgotPasswordRequest + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? email = null}) { + return _then( + _$ForgotPasswordRequestImpl( + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$ForgotPasswordRequestImpl implements _ForgotPasswordRequest { + const _$ForgotPasswordRequestImpl({required this.email}); + + factory _$ForgotPasswordRequestImpl.fromJson(Map json) => + _$$ForgotPasswordRequestImplFromJson(json); + + @override + final String email; + + @override + String toString() { + return 'ForgotPasswordRequest(email: $email)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ForgotPasswordRequestImpl && + (identical(other.email, email) || other.email == email)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, email); + + /// Create a copy of ForgotPasswordRequest + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ForgotPasswordRequestImplCopyWith<_$ForgotPasswordRequestImpl> + get copyWith => + __$$ForgotPasswordRequestImplCopyWithImpl<_$ForgotPasswordRequestImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$ForgotPasswordRequestImplToJson(this); + } +} + +abstract class _ForgotPasswordRequest implements ForgotPasswordRequest { + const factory _ForgotPasswordRequest({required final String email}) = + _$ForgotPasswordRequestImpl; + + factory _ForgotPasswordRequest.fromJson(Map json) = + _$ForgotPasswordRequestImpl.fromJson; + + @override + String get email; + + /// Create a copy of ForgotPasswordRequest + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ForgotPasswordRequestImplCopyWith<_$ForgotPasswordRequestImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/user_model.g.dart b/lib/shared/models/user_model.g.dart new file mode 100644 index 0000000..370386c --- /dev/null +++ b/lib/shared/models/user_model.g.dart @@ -0,0 +1,128 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$UserModelImpl _$$UserModelImplFromJson(Map json) => + _$UserModelImpl( + id: json['id'] as String, + employeeId: json['employee_id'] as String, + name: json['name'] as String, + email: json['email'] as String, + mobile: json['mobile'] as String, + role: json['role'] as String, + department: json['department'] as String?, + status: json['status'] as String? ?? 'active', + companyId: json['company_id'] as String?, + branchId: json['branch_id'] as String?, + avatarUrl: json['avatarUrl'] as String?, + permissions: + (json['permissions'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], + createdAt: json['createdAt'] == null + ? null + : DateTime.parse(json['createdAt'] as String), + updatedAt: json['updatedAt'] == null + ? null + : DateTime.parse(json['updatedAt'] as String), + ); + +Map _$$UserModelImplToJson(_$UserModelImpl instance) => + { + 'id': instance.id, + 'employee_id': instance.employeeId, + 'name': instance.name, + 'email': instance.email, + 'mobile': instance.mobile, + 'role': instance.role, + 'department': instance.department, + 'status': instance.status, + 'company_id': instance.companyId, + 'branch_id': instance.branchId, + 'avatarUrl': instance.avatarUrl, + 'permissions': instance.permissions, + 'createdAt': instance.createdAt?.toIso8601String(), + 'updatedAt': instance.updatedAt?.toIso8601String(), + }; + +_$AuthTokensImpl _$$AuthTokensImplFromJson(Map json) => + _$AuthTokensImpl( + accessToken: json['access_token'] as String, + refreshToken: json['refresh_token'] as String, + expiresIn: (json['expires_in'] as num?)?.toInt(), + ); + +Map _$$AuthTokensImplToJson(_$AuthTokensImpl instance) => + { + 'access_token': instance.accessToken, + 'refresh_token': instance.refreshToken, + 'expires_in': instance.expiresIn, + }; + +_$LoginRequestImpl _$$LoginRequestImplFromJson(Map json) => + _$LoginRequestImpl( + email: json['email'] as String, + password: json['password'] as String, + companyCode: json['company_code'] as String?, + ); + +Map _$$LoginRequestImplToJson(_$LoginRequestImpl instance) => + { + 'email': instance.email, + 'password': instance.password, + 'company_code': instance.companyCode, + }; + +_$LoginResponseImpl _$$LoginResponseImplFromJson(Map json) => + _$LoginResponseImpl( + tokens: AuthTokens.fromJson(json['tokens'] as Map), + user: UserModel.fromJson(json['user'] as Map), + requiresOtp: json['requires_otp'] as bool? ?? false, + ); + +Map _$$LoginResponseImplToJson(_$LoginResponseImpl instance) => + { + 'tokens': instance.tokens, + 'user': instance.user, + 'requires_otp': instance.requiresOtp, + }; + +_$OtpVerifyRequestImpl _$$OtpVerifyRequestImplFromJson( + Map json, +) => _$OtpVerifyRequestImpl( + email: json['email'] as String, + otp: json['otp'] as String, +); + +Map _$$OtpVerifyRequestImplToJson( + _$OtpVerifyRequestImpl instance, +) => {'email': instance.email, 'otp': instance.otp}; + +_$ChangePasswordRequestImpl _$$ChangePasswordRequestImplFromJson( + Map json, +) => _$ChangePasswordRequestImpl( + currentPassword: json['current_password'] as String, + newPassword: json['new_password'] as String, + confirmPassword: json['confirm_password'] as String, +); + +Map _$$ChangePasswordRequestImplToJson( + _$ChangePasswordRequestImpl instance, +) => { + 'current_password': instance.currentPassword, + 'new_password': instance.newPassword, + 'confirm_password': instance.confirmPassword, +}; + +_$ForgotPasswordRequestImpl _$$ForgotPasswordRequestImplFromJson( + Map json, +) => _$ForgotPasswordRequestImpl(email: json['email'] as String); + +Map _$$ForgotPasswordRequestImplToJson( + _$ForgotPasswordRequestImpl instance, +) => {'email': instance.email}; diff --git a/lib/shared/providers/auth_provider.dart b/lib/shared/providers/auth_provider.dart new file mode 100644 index 0000000..c71d0b6 --- /dev/null +++ b/lib/shared/providers/auth_provider.dart @@ -0,0 +1,125 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../core/config/dev_config.dart'; +import '../../modules/auth/data/repositories/auth_repository_impl.dart'; +import '../../modules/auth/domain/repositories/auth_repository.dart'; +import '../models/user_model.dart'; +import 'dev_user.dart'; + +enum AuthStatus { initial, authenticated, unauthenticated, loading } + +class AuthState { + const AuthState({ + this.status = AuthStatus.initial, + this.user, + this.error, + }); + + final AuthStatus status; + final UserModel? user; + final String? error; + + AuthState copyWith({ + AuthStatus? status, + UserModel? user, + String? error, + }) { + return AuthState( + status: status ?? this.status, + user: user ?? this.user, + error: error, + ); + } +} + +final authStateProvider = StateNotifierProvider((ref) { + return AuthNotifier(ref.watch(authRepositoryProvider)); +}); + +class AuthNotifier extends StateNotifier { + AuthNotifier(this._repository) : super(const AuthState()) { + checkAuth(); + } + + final AuthRepository _repository; + + Future checkAuth() async { + state = state.copyWith(status: AuthStatus.loading); + + final isAuth = await _repository.isAuthenticated(); + if (!isAuth) { + state = const AuthState(status: AuthStatus.unauthenticated); + return; + } + final result = await _repository.getCurrentUser(); + if (result.failure != null) { + state = const AuthState(status: AuthStatus.unauthenticated); + return; + } + state = AuthState(status: AuthStatus.authenticated, user: result.data); + } + + void loginAsDemo() { + state = const AuthState(status: AuthStatus.authenticated, user: demoUser); + } + + Future login(LoginRequest request) async { + if (DevConfig.bypassAuth) { + loginAsDemo(); + return true; + } + + state = state.copyWith(status: AuthStatus.loading, error: null); + + final result = await _repository.login(request); + if (result.failure != null) { + state = state.copyWith( + status: AuthStatus.unauthenticated, + error: _friendlyLoginError(result.failure!.message), + ); + return false; + } + + final loginResponse = result.data!; + if (loginResponse.requiresOtp) { + state = state.copyWith( + status: AuthStatus.unauthenticated, + error: 'OTP verification required', + ); + return false; + } + + state = AuthState( + status: AuthStatus.authenticated, + user: loginResponse.user, + ); + return true; + } + + Future logout() async { + await _repository.logout(); + state = const AuthState(status: AuthStatus.unauthenticated); + } + + /// Called when refresh token expires or refresh fails (401 interceptor). + void onSessionExpired() { + state = const AuthState( + status: AuthStatus.unauthenticated, + error: 'Session expired. Please login again.', + ); + } + + /// Shortens noisy server stack traces for the login snackbar. + static String _friendlyLoginError(String message) { + final trimmed = message.trim(); + if (trimmed.isEmpty) return 'Login failed. Please try again.'; + + if (trimmed.contains('PrismaClient') || trimmed.contains('invocation in')) { + return 'Login service is temporarily unavailable. Please contact support.'; + } + + final firstLine = trimmed.split('\n').first.trim(); + if (firstLine.length <= 160) return firstLine; + return '${firstLine.substring(0, 157)}...'; + } +} diff --git a/lib/shared/providers/dev_user.dart b/lib/shared/providers/dev_user.dart new file mode 100644 index 0000000..7ae3626 --- /dev/null +++ b/lib/shared/providers/dev_user.dart @@ -0,0 +1,17 @@ +import '../models/user_model.dart'; + +const demoUser = UserModel( + id: 'dev-1', + employeeId: 'EMP001', + name: 'Demo Admin', + email: 'admin@bharaterp.com', + mobile: '9876543210', + role: 'super_admin', + department: 'IT', + permissions: ['*'], +); + +bool isDemoLogin(LoginRequest request) { + return request.email.trim().toLowerCase() == 'admin@bharaterp.com' && + request.password == 'Admin@123'; +} diff --git a/lib/shared/routes/app_router.dart b/lib/shared/routes/app_router.dart new file mode 100644 index 0000000..245287d --- /dev/null +++ b/lib/shared/routes/app_router.dart @@ -0,0 +1,317 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/config/dev_config.dart'; +import '../../core/constants/route_constants.dart'; +import '../../modules/dashboard/presentation/screens/dashboard_screen.dart'; +import '../../modules/assets/presentation/screens/asset_allocations_screen.dart'; +import '../../modules/assets/presentation/screens/asset_categories_screen.dart'; +import '../../modules/assets/presentation/screens/asset_detail_screen.dart'; +import '../../modules/assets/presentation/screens/asset_disposal_screen.dart'; +import '../../modules/assets/presentation/screens/asset_form_screen.dart'; +import '../../modules/assets/presentation/screens/asset_list_screen.dart'; +import '../../modules/assets/presentation/screens/asset_maintenance_screen.dart'; +import '../../modules/assets/presentation/screens/asset_qr_generate_screen.dart'; +import '../../modules/assets/presentation/screens/asset_qr_scan_screen.dart'; +import '../../modules/auth/presentation/screens/change_password_screen.dart'; +import '../../modules/auth/presentation/screens/forgot_password_screen.dart'; +import '../../modules/auth/presentation/screens/login_screen.dart'; +import '../../modules/auth/presentation/screens/reset_password_screen.dart'; +import '../../modules/auth/presentation/screens/verify_otp_screen.dart'; +import '../../modules/master_data/presentation/screens/departments_screen.dart'; +import '../../modules/master_data/presentation/screens/locations_screen.dart'; +import '../../modules/master_data/presentation/screens/uom_screen.dart'; +import '../../modules/reports/presentation/screens/reports_screen.dart'; +import '../../modules/audit/presentation/screens/audit_logs_screen.dart'; +import '../../modules/branch/presentation/screens/branch_form_screen.dart'; +import '../../modules/branch/presentation/screens/branch_list_screen.dart'; +import '../../modules/company/presentation/screens/company_form_screen.dart'; +import '../../modules/company/presentation/screens/company_list_screen.dart'; +import '../../modules/dev/presentation/screens/screen_gallery_screen.dart'; +import '../../modules/rbac/presentation/providers/rbac_provider.dart'; +import '../../modules/rbac/presentation/screens/users_role_management_screen.dart'; +import '../../modules/users/presentation/screens/user_detail_screen.dart'; +import '../../modules/users/presentation/screens/user_form_screen.dart'; +import '../../modules/users/presentation/screens/user_profile_screen.dart'; +import '../../modules/roles/presentation/screens/permission_matrix_screen.dart'; +import '../../modules/settings/presentation/screens/appearance_settings_screen.dart'; +import '../../modules/settings/presentation/screens/asset_settings_screen.dart'; +import '../../modules/settings/presentation/screens/company_profile_settings_screen.dart'; +import '../../modules/settings/presentation/screens/email_configuration_screen.dart'; +import '../../modules/settings/presentation/screens/general_settings_screen.dart'; +import '../../modules/settings/presentation/screens/notification_settings_screen.dart'; +import '../../modules/settings/presentation/screens/roles_permissions_settings_screen.dart'; +import '../../modules/settings/presentation/screens/security_settings_screen.dart'; +import '../../modules/settings/presentation/screens/settings_screen.dart'; +import '../providers/auth_provider.dart'; +import '../widgets/app_shell.dart'; + +final routerProvider = Provider((ref) { + final refreshListenable = _AuthListenable(ref); + + final router = GoRouter( + initialLocation: RouteConstants.login, + refreshListenable: refreshListenable, + redirect: (context, state) { + final authState = ref.read(authStateProvider); + final isAuthenticated = authState.status == AuthStatus.authenticated; + final isPreview = state.uri.queryParameters['preview'] == 'true'; + final location = state.matchedLocation; + final isAuthRoute = location == RouteConstants.login || + location == RouteConstants.forgotPassword || + location == RouteConstants.resetPassword || + location == RouteConstants.verifyOtp; + + if (authState.status == AuthStatus.initial || + authState.status == AuthStatus.loading) { + return null; + } + + // Screen gallery and ?preview=true routes work without login API. + if (DevConfig.screenPreviewEnabled && + location == RouteConstants.screenGallery) { + return null; + } + if (isPreview) return null; + + if (!isAuthenticated && !isAuthRoute) { + return RouteConstants.login; + } + + if (isAuthenticated && isAuthRoute && !isPreview) { + return RouteConstants.dashboard; + } + + return null; + }, + routes: [ + GoRoute( + path: RouteConstants.login, + builder: (context, state) => const LoginScreen(), + ), + GoRoute( + path: RouteConstants.forgotPassword, + builder: (context, state) => const ForgotPasswordScreen(), + ), + GoRoute( + path: RouteConstants.resetPassword, + builder: (context, state) => const ResetPasswordScreen(), + ), + GoRoute( + path: RouteConstants.verifyOtp, + builder: (context, state) { + final email = state.uri.queryParameters['email'] ?? ''; + return VerifyOtpScreen(email: email); + }, + ), + GoRoute( + path: RouteConstants.changePassword, + builder: (context, state) => const ChangePasswordScreen(), + ), + ShellRoute( + builder: (context, state, child) => AppShell(child: child), + routes: [ + GoRoute( + path: RouteConstants.dashboard, + builder: (context, state) => const DashboardScreen(), + ), + GoRoute( + path: RouteConstants.companies, + builder: (context, state) => const CompanyListScreen(), + routes: [ + GoRoute( + path: 'add', + builder: (context, state) => const CompanyFormScreen(), + ), + GoRoute( + path: ':id/edit', + builder: (context, state) => + CompanyFormScreen(companyId: state.pathParameters['id']), + ), + ], + ), + GoRoute( + path: RouteConstants.branches, + builder: (context, state) => const BranchListScreen(), + routes: [ + GoRoute( + path: 'add', + builder: (context, state) => const BranchFormScreen(), + ), + GoRoute( + path: ':id/edit', + builder: (context, state) => + BranchFormScreen(branchId: state.pathParameters['id']), + ), + ], + ), + GoRoute( + path: RouteConstants.usersRoleManagement, + builder: (context, state) => UsersRoleManagementScreen( + initialTab: rbacTabFromLocation(state.uri.toString()), + ), + ), + GoRoute( + path: RouteConstants.users, + builder: (context, state) => const UsersRoleManagementScreen( + initialTab: RbacTab.users, + ), + routes: [ + GoRoute( + path: 'add', + builder: (context, state) => const UserFormScreen(), + ), + GoRoute( + path: ':id/edit', + builder: (context, state) => + UserFormScreen(userId: state.pathParameters['id']), + ), + GoRoute( + path: ':id', + builder: (context, state) => + UserDetailScreen(userId: state.pathParameters['id']!), + ), + ], + ), + GoRoute( + path: RouteConstants.roles, + builder: (context, state) => const UsersRoleManagementScreen( + initialTab: RbacTab.roles, + ), + routes: [ + GoRoute( + path: ':id/permissions', + builder: (context, state) => PermissionMatrixScreen( + roleId: state.pathParameters['id']!, + ), + ), + ], + ), + GoRoute( + path: RouteConstants.profile, + builder: (context, state) => const UserProfileScreen(), + ), + GoRoute( + path: RouteConstants.assets, + builder: (context, state) => const AssetListScreen(), + routes: [ + GoRoute( + path: 'add', + builder: (context, state) => const AssetFormScreen(), + ), + GoRoute( + path: 'categories', + builder: (context, state) => const AssetCategoriesScreen(), + ), + GoRoute( + path: 'allocations', + builder: (context, state) => const AssetAllocationsScreen(), + ), + GoRoute( + path: 'maintenance', + builder: (context, state) => const AssetMaintenanceScreen(), + ), + GoRoute( + path: 'disposal', + builder: (context, state) => const AssetDisposalScreen(), + ), + GoRoute( + path: 'qr-scan', + builder: (context, state) => const AssetQrScanScreen(), + ), + GoRoute( + path: 'qr-generate', + builder: (context, state) => const AssetQrGenerateScreen(), + ), + GoRoute( + path: ':id', + builder: (context, state) => + AssetDetailScreen(assetId: state.pathParameters['id']!), + ), + GoRoute( + path: ':id/edit', + builder: (context, state) => + AssetFormScreen(assetId: state.pathParameters['id']), + ), + ], + ), + GoRoute( + path: RouteConstants.departments, + builder: (context, state) => const DepartmentsScreen(), + ), + GoRoute( + path: RouteConstants.locations, + builder: (context, state) => const LocationsScreen(), + ), + GoRoute( + path: RouteConstants.uom, + builder: (context, state) => const UomScreen(), + ), + GoRoute( + path: RouteConstants.reports, + builder: (context, state) => const ReportsScreen(), + ), + GoRoute( + path: RouteConstants.auditLogs, + builder: (context, state) => const AuditLogsScreen(), + ), + GoRoute( + path: RouteConstants.settings, + builder: (context, state) => const SettingsScreen(), + routes: [ + GoRoute( + path: 'general', + builder: (context, state) => const GeneralSettingsScreen(), + ), + GoRoute( + path: 'company-profile', + builder: (context, state) => const CompanyProfileSettingsScreen(), + ), + GoRoute( + path: 'appearance', + builder: (context, state) => const AppearanceSettingsScreen(), + ), + GoRoute( + path: 'roles', + builder: (context, state) => const RolesPermissionsSettingsScreen(), + ), + GoRoute( + path: 'asset', + builder: (context, state) => const AssetSettingsScreen(), + ), + GoRoute( + path: 'notifications', + builder: (context, state) => const NotificationSettingsScreen(), + ), + GoRoute( + path: 'email', + builder: (context, state) => const EmailConfigurationScreen(), + ), + GoRoute( + path: 'security', + builder: (context, state) => const SecuritySettingsScreen(), + ), + ], + ), + if (DevConfig.screenPreviewEnabled) + GoRoute( + path: RouteConstants.screenGallery, + builder: (context, state) => const ScreenGalleryScreen(), + ), + ], + ), + ], + ); + + ref.onDispose(router.dispose); + return router; +}); + +class _AuthListenable extends ChangeNotifier { + _AuthListenable(this._ref) { + _ref.listen(authStateProvider, (_, __) => notifyListeners()); + } + + final Ref _ref; +} diff --git a/lib/shared/routes/menu_config.dart b/lib/shared/routes/menu_config.dart new file mode 100644 index 0000000..e885345 --- /dev/null +++ b/lib/shared/routes/menu_config.dart @@ -0,0 +1,165 @@ +import 'package:flutter/material.dart'; + +import '../../core/constants/enums.dart'; +import '../../core/constants/route_constants.dart'; +import '../../core/utils/permission_utils.dart'; + +class MenuItem { + const MenuItem({ + required this.label, + required this.icon, + required this.route, + required this.module, + this.children = const [], + this.requiredRole, + }); + + final String label; + final IconData icon; + final String route; + final String module; + final List children; + final UserRole? requiredRole; +} + +const List appMenuItems = [ + MenuItem( + label: 'Dashboard', + icon: Icons.dashboard_outlined, + route: RouteConstants.dashboard, + module: 'dashboard', + ), + MenuItem( + label: 'Companies', + icon: Icons.business_outlined, + route: RouteConstants.companies, + module: 'companies', + requiredRole: UserRole.superAdmin, + ), + MenuItem( + label: 'Branches', + icon: Icons.account_tree_outlined, + route: RouteConstants.branches, + module: 'branches', + ), + MenuItem( + label: 'Users & Roles', + icon: Icons.admin_panel_settings_outlined, + route: RouteConstants.usersRoleManagement, + module: 'users', + ), + MenuItem( + label: 'Assets', + icon: Icons.inventory_2_outlined, + route: RouteConstants.assets, + module: 'assets', + children: [ + MenuItem( + label: 'Asset Master', + icon: Icons.devices_outlined, + route: RouteConstants.assets, + module: 'assets', + ), + MenuItem( + label: 'Categories', + icon: Icons.category_outlined, + route: RouteConstants.assetCategories, + module: 'asset_categories', + ), + MenuItem( + label: 'Allocations', + icon: Icons.assignment_ind_outlined, + route: RouteConstants.assetAllocations, + module: 'asset_allocations', + ), + MenuItem( + label: 'Maintenance', + icon: Icons.build_outlined, + route: RouteConstants.assetMaintenance, + module: 'asset_maintenance', + ), + MenuItem( + label: 'Disposal', + icon: Icons.delete_outline, + route: RouteConstants.assetDisposal, + module: 'asset_disposal', + ), + MenuItem( + label: 'QR Scan', + icon: Icons.qr_code_scanner_outlined, + route: RouteConstants.assetQrScan, + module: 'assets', + ), + MenuItem( + label: 'QR Generate', + icon: Icons.qr_code_2_outlined, + route: RouteConstants.assetQrGenerate, + module: 'assets', + ), + ], + ), + MenuItem( + label: 'Master Data', + icon: Icons.dataset_outlined, + route: RouteConstants.departments, + module: 'master_data', + children: [ + MenuItem( + label: 'Departments', + icon: Icons.apartment_outlined, + route: RouteConstants.departments, + module: 'departments', + ), + MenuItem( + label: 'Locations', + icon: Icons.location_on_outlined, + route: RouteConstants.locations, + module: 'locations', + ), + MenuItem( + label: 'UOM', + icon: Icons.straighten_outlined, + route: RouteConstants.uom, + module: 'uom', + ), + ], + ), + MenuItem( + label: 'Reports', + icon: Icons.assessment_outlined, + route: RouteConstants.reports, + module: 'reports', + ), + MenuItem( + label: 'Audit Logs', + icon: Icons.history_outlined, + route: RouteConstants.auditLogs, + module: 'audit_logs', + ), + MenuItem( + label: 'Settings', + icon: Icons.settings_outlined, + route: RouteConstants.settings, + module: 'settings', + ), +]; + +List getVisibleMenuItems({ + required List permissions, + required UserRole role, +}) { + if (isSuperAdmin(role) || permissions.contains('*')) { + return appMenuItems; + } + + return appMenuItems.where((item) { + if (item.requiredRole != null && !isSuperAdmin(role) && role != item.requiredRole) { + return false; + } + return hasPermission( + userPermissions: permissions, + module: item.module, + action: PermissionAction.read, + ) || isAdmin(role); + }).toList(); +} diff --git a/lib/shared/widgets/api_feedback.dart b/lib/shared/widgets/api_feedback.dart new file mode 100644 index 0000000..87b7c5b --- /dev/null +++ b/lib/shared/widgets/api_feedback.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import '../../core/errors/failure.dart'; +import '../../core/network/api_handler.dart'; + +void showAccessDeniedSnackBar(BuildContext context, {String? message}) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Row( + children: [ + const Icon(Icons.block, color: Colors.white, size: 18), + const SizedBox(width: 8), + Expanded( + child: Text(message ?? 'Access denied'), + ), + ], + ), + backgroundColor: const Color(0xFFDC2626), + behavior: SnackBarBehavior.floating, + ), + ); +} + +void showApiFailureSnackBar(BuildContext context, Failure failure) { + if (isForbiddenFailure(failure)) { + showAccessDeniedSnackBar(context, message: failure.message); + return; + } + + if (isConflictFailure(failure)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(failure.message)), + ); + return; + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + failure is ValidationFailure + ? validationErrorMessage(failure) + : failure.message, + ), + ), + ); +} diff --git a/lib/shared/widgets/app_button.dart b/lib/shared/widgets/app_button.dart new file mode 100644 index 0000000..80565c5 --- /dev/null +++ b/lib/shared/widgets/app_button.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +class AppButton extends StatelessWidget { + const AppButton({ + super.key, + required this.label, + required this.onPressed, + this.isLoading = false, + this.isOutlined = false, + this.icon, + this.expand = true, + }); + + final String label; + final VoidCallback? onPressed; + final bool isLoading; + final bool isOutlined; + final IconData? icon; + final bool expand; + + @override + Widget build(BuildContext context) { + final child = isLoading + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (icon != null) ...[Icon(icon, size: 20), const SizedBox(width: 8)], + Text(label), + ], + ); + + final button = isOutlined + ? OutlinedButton(onPressed: isLoading ? null : onPressed, child: child) + : ElevatedButton(onPressed: isLoading ? null : onPressed, child: child); + + return expand ? SizedBox(width: double.infinity, child: button) : button; + } +} diff --git a/lib/shared/widgets/app_card.dart b/lib/shared/widgets/app_card.dart new file mode 100644 index 0000000..796acd0 --- /dev/null +++ b/lib/shared/widgets/app_card.dart @@ -0,0 +1,110 @@ +import 'package:flutter/material.dart'; + +import 'app_hover_effect.dart'; + +/// Card that follows the active theme with a shared hover animation. +class AppCard extends StatefulWidget { + const AppCard({ + super.key, + required this.child, + this.clipBehavior, + this.elevation, + this.shape, + this.margin, + this.color, + this.enableHover = true, + this.onTap, + }); + + final Widget child; + final Clip? clipBehavior; + final double? elevation; + final ShapeBorder? shape; + final EdgeInsetsGeometry? margin; + final Color? color; + final bool enableHover; + final VoidCallback? onTap; + + @override + State createState() => _AppCardState(); +} + +class _AppCardState extends State { + bool _hovered = false; + + void _setHovered(bool value) { + if (!widget.enableHover || _hovered == value) return; + setState(() => _hovered = value); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final baseElevation = widget.elevation ?? 0; + final radius = _borderRadius(widget.shape) ?? 12.0; + final idleBorder = theme.colorScheme.outline.withValues(alpha: 0.12); + final hoverBorder = theme.colorScheme.primary.withValues(alpha: 0.28); + + final shape = widget.shape ?? + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(radius), + side: BorderSide( + color: widget.enableHover && _hovered ? hoverBorder : idleBorder, + ), + ); + + Widget card = AnimatedScale( + scale: widget.enableHover && _hovered ? AppHoverStyle.scale : 1, + duration: AppHoverStyle.duration, + curve: AppHoverStyle.curve, + child: AnimatedContainer( + duration: AppHoverStyle.duration, + curve: AppHoverStyle.curve, + margin: widget.margin, + transform: Matrix4.translationValues( + 0, + widget.enableHover && _hovered ? -AppHoverStyle.lift : 0, + 0, + ), + child: Card( + color: widget.color, + surfaceTintColor: Colors.transparent, + clipBehavior: widget.clipBehavior, + elevation: widget.enableHover && _hovered + ? baseElevation + AppHoverStyle.elevationDelta + : baseElevation, + shape: shape is RoundedRectangleBorder && widget.enableHover + ? shape.copyWith( + side: BorderSide( + color: _hovered ? hoverBorder : idleBorder, + ), + ) + : shape, + child: widget.onTap == null + ? widget.child + : InkWell( + onTap: widget.onTap, + borderRadius: BorderRadius.circular(radius), + child: widget.child, + ), + ), + ), + ); + + if (!widget.enableHover) return card; + + return MouseRegion( + onEnter: (_) => _setHovered(true), + onExit: (_) => _setHovered(false), + cursor: widget.onTap != null ? SystemMouseCursors.click : SystemMouseCursors.basic, + child: card, + ); + } + + double? _borderRadius(ShapeBorder? shape) { + if (shape is RoundedRectangleBorder) { + return shape.borderRadius.resolve(TextDirection.ltr).topLeft.x; + } + return null; + } +} diff --git a/lib/shared/widgets/app_confirmation_dialog.dart b/lib/shared/widgets/app_confirmation_dialog.dart new file mode 100644 index 0000000..2e29c1f --- /dev/null +++ b/lib/shared/widgets/app_confirmation_dialog.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; + +Future showAppConfirmationDialog({ + required BuildContext context, + required String title, + required String message, + String confirmLabel = 'Confirm', + String cancelLabel = 'Cancel', + bool isDestructive = false, +}) { + return showDialog( + context: context, + builder: (context) => AppConfirmationDialog( + title: title, + message: message, + confirmLabel: confirmLabel, + cancelLabel: cancelLabel, + isDestructive: isDestructive, + ), + ); +} + +class AppConfirmationDialog extends StatelessWidget { + const AppConfirmationDialog({ + super.key, + required this.title, + required this.message, + this.confirmLabel = 'Confirm', + this.cancelLabel = 'Cancel', + this.isDestructive = false, + }); + + final String title; + final String message; + final String confirmLabel; + final String cancelLabel; + final bool isDestructive; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(title), + content: Text(message), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(cancelLabel), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + style: isDestructive + ? TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error) + : null, + child: Text(confirmLabel), + ), + ], + ); + } +} diff --git a/lib/shared/widgets/app_data_table.dart b/lib/shared/widgets/app_data_table.dart new file mode 100644 index 0000000..eaeb801 --- /dev/null +++ b/lib/shared/widgets/app_data_table.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; + +import 'app_card.dart'; + +class AppDataColumn { + const AppDataColumn({ + required this.label, + required this.cellBuilder, + this.sortKey, + this.flex = 1, + this.alignment = Alignment.centerLeft, + }); + + final String label; + final Widget Function(BuildContext context, T row) cellBuilder; + final String? sortKey; + final int flex; + final Alignment alignment; +} + +class AppDataTable extends StatelessWidget { + const AppDataTable({ + super.key, + required this.columns, + required this.rows, + this.sortColumn, + this.sortAscending = true, + this.onSort, + this.emptyMessage = 'No records found', + }); + + final List> columns; + final List rows; + final String? sortColumn; + final bool sortAscending; + final void Function(String column, bool ascending)? onSort; + final String emptyMessage; + + @override + Widget build(BuildContext context) { + if (rows.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Text( + emptyMessage, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + ); + } + + return AppCard( + clipBehavior: Clip.antiAlias, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: ConstrainedBox( + constraints: const BoxConstraints(minWidth: 720), + child: DataTable( + sortColumnIndex: sortColumn == null + ? null + : columns.indexWhere((c) => c.sortKey == sortColumn), + sortAscending: sortAscending, + columns: columns + .map( + (col) => DataColumn( + label: Text(col.label), + onSort: col.sortKey == null + ? null + : (_, ascending) => onSort?.call(col.sortKey!, ascending), + ), + ) + .toList(), + rows: rows + .map( + (row) => DataRow( + cells: columns + .map((col) => DataCell(col.cellBuilder(context, row))) + .toList(), + ), + ) + .toList(), + ), + ), + ), + ); + } +} diff --git a/lib/shared/widgets/app_dropdown.dart b/lib/shared/widgets/app_dropdown.dart new file mode 100644 index 0000000..68c1fdb --- /dev/null +++ b/lib/shared/widgets/app_dropdown.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class AppDropdownOption { + const AppDropdownOption({required this.value, required this.label}); + + final T value; + final String label; +} + +class AppDropdown extends StatelessWidget { + const AppDropdown({ + super.key, + required this.label, + required this.value, + required this.options, + required this.onChanged, + this.validator, + this.hint, + this.enabled = true, + }); + + final String label; + final T? value; + final List> options; + final ValueChanged onChanged; + final String? Function(T?)? validator; + final String? hint; + final bool enabled; + + @override + Widget build(BuildContext context) { + return DropdownButtonFormField( + value: value, + decoration: InputDecoration(labelText: label, hintText: hint), + items: options + .map( + (option) => DropdownMenuItem( + value: option.value, + child: Text(option.label), + ), + ) + .toList(), + onChanged: enabled ? onChanged : null, + validator: validator, + ); + } +} diff --git a/lib/shared/widgets/app_empty_state.dart b/lib/shared/widgets/app_empty_state.dart new file mode 100644 index 0000000..16e3c3c --- /dev/null +++ b/lib/shared/widgets/app_empty_state.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class AppEmptyState extends StatelessWidget { + const AppEmptyState({ + super.key, + required this.title, + this.description, + this.icon = Icons.inbox_outlined, + this.action, + }); + + final String title; + final String? description; + final IconData icon; + final Widget? action; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 56, color: Theme.of(context).colorScheme.outline), + const SizedBox(height: 16), + Text(title, style: Theme.of(context).textTheme.titleMedium), + if (description != null) ...[ + const SizedBox(height: 8), + Text( + description!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + if (action != null) ...[ + const SizedBox(height: 20), + action!, + ], + ], + ), + ), + ); + } +} diff --git a/lib/shared/widgets/app_hover_effect.dart b/lib/shared/widgets/app_hover_effect.dart new file mode 100644 index 0000000..c599165 --- /dev/null +++ b/lib/shared/widgets/app_hover_effect.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; + +/// Shared hover animation for cards and tappable surfaces (web/desktop). +class AppHoverStyle { + AppHoverStyle._(); + + static const Duration duration = Duration(milliseconds: 200); + static const Curve curve = Curves.easeOutCubic; + static const double lift = 2; + static const double scale = 1.01; + static const double elevationDelta = 3; +} + +class AppHoverEffect extends StatefulWidget { + const AppHoverEffect({ + super.key, + required this.child, + this.enabled = true, + this.onTap, + this.borderRadius = 12, + this.hoverBorderColor, + this.idleBorderColor, + this.showHoverBorder = true, + }); + + final Widget child; + final bool enabled; + final VoidCallback? onTap; + final double borderRadius; + final Color? hoverBorderColor; + final Color? idleBorderColor; + final bool showHoverBorder; + + @override + State createState() => _AppHoverEffectState(); +} + +class _AppHoverEffectState extends State { + bool _hovered = false; + + void _setHovered(bool value) { + if (!widget.enabled || _hovered == value) return; + setState(() => _hovered = value); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final borderRadius = BorderRadius.circular(widget.borderRadius); + final idleBorder = widget.idleBorderColor ?? + theme.colorScheme.outline.withValues(alpha: 0.12); + final hoverBorder = widget.hoverBorderColor ?? + theme.colorScheme.primary.withValues(alpha: 0.28); + + Widget content = AnimatedScale( + scale: widget.enabled && _hovered ? AppHoverStyle.scale : 1, + duration: AppHoverStyle.duration, + curve: AppHoverStyle.curve, + child: AnimatedContainer( + duration: AppHoverStyle.duration, + curve: AppHoverStyle.curve, + transform: Matrix4.translationValues( + 0, + widget.enabled && _hovered ? -AppHoverStyle.lift : 0, + 0, + ), + decoration: widget.showHoverBorder + ? BoxDecoration( + borderRadius: borderRadius, + border: Border.all( + color: widget.enabled && _hovered ? hoverBorder : idleBorder, + ), + ) + : null, + child: widget.child, + ), + ); + + if (widget.onTap != null) { + content = Material( + color: Colors.transparent, + child: InkWell( + onTap: widget.onTap, + borderRadius: borderRadius, + child: content, + ), + ); + } + + if (!widget.enabled) return content; + + return MouseRegion( + onEnter: (_) => _setHovered(true), + onExit: (_) => _setHovered(false), + cursor: widget.onTap != null ? SystemMouseCursors.click : SystemMouseCursors.basic, + child: content, + ); + } +} diff --git a/lib/shared/widgets/app_loading_view.dart b/lib/shared/widgets/app_loading_view.dart new file mode 100644 index 0000000..fe8576b --- /dev/null +++ b/lib/shared/widgets/app_loading_view.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import 'loading_indicator.dart'; + +class AppLoadingView extends StatelessWidget { + const AppLoadingView({ + super.key, + this.message, + }); + + final String? message; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const AppLoadingIndicator(), + if (message != null) ...[ + const SizedBox(height: 16), + Text( + message!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ); + } +} diff --git a/lib/shared/widgets/app_pagination.dart b/lib/shared/widgets/app_pagination.dart new file mode 100644 index 0000000..88fb974 --- /dev/null +++ b/lib/shared/widgets/app_pagination.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; + +class AppPagination extends StatelessWidget { + const AppPagination({ + super.key, + required this.currentPage, + required this.totalPages, + required this.totalItems, + required this.pageSize, + required this.onPageChanged, + this.onPageSizeChanged, + this.pageSizeOptions = const [10, 20, 50], + }); + + final int currentPage; + final int totalPages; + final int totalItems; + final int pageSize; + final ValueChanged onPageChanged; + final ValueChanged? onPageSizeChanged; + final List pageSizeOptions; + + @override + Widget build(BuildContext context) { + final start = totalItems == 0 ? 0 : ((currentPage - 1) * pageSize) + 1; + final end = (currentPage * pageSize).clamp(0, totalItems); + + return Wrap( + spacing: 12, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + 'Showing $start–$end of $totalItems', + style: Theme.of(context).textTheme.bodySmall, + ), + if (onPageSizeChanged != null) + DropdownButton( + value: pageSize, + items: pageSizeOptions + .map((size) => DropdownMenuItem(value: size, child: Text('$size / page'))) + .toList(), + onChanged: (value) { + if (value != null) onPageSizeChanged!(value); + }, + ), + IconButton( + tooltip: 'Previous page', + onPressed: currentPage > 1 ? () => onPageChanged(currentPage - 1) : null, + icon: const Icon(Icons.chevron_left), + ), + Text('Page $currentPage of ${totalPages.clamp(1, totalPages)}'), + IconButton( + tooltip: 'Next page', + onPressed: currentPage < totalPages ? () => onPageChanged(currentPage + 1) : null, + icon: const Icon(Icons.chevron_right), + ), + ], + ); + } +} diff --git a/lib/shared/widgets/app_search_field.dart b/lib/shared/widgets/app_search_field.dart new file mode 100644 index 0000000..26674bb --- /dev/null +++ b/lib/shared/widgets/app_search_field.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; + +class AppSearchField extends StatelessWidget { + const AppSearchField({ + super.key, + required this.controller, + this.hint = 'Search...', + this.onChanged, + this.onSubmitted, + this.onClear, + }); + + final TextEditingController controller; + final String hint; + final ValueChanged? onChanged; + final ValueChanged? onSubmitted; + final VoidCallback? onClear; + + @override + Widget build(BuildContext context) { + return TextField( + controller: controller, + onChanged: onChanged, + onSubmitted: onSubmitted, + decoration: InputDecoration( + hintText: hint, + prefixIcon: const Icon(Icons.search), + suffixIcon: controller.text.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + controller.clear(); + onClear?.call(); + onChanged?.call(''); + }, + ) + : null, + isDense: true, + ), + ); + } +} diff --git a/lib/shared/widgets/app_searchable_dropdown.dart b/lib/shared/widgets/app_searchable_dropdown.dart new file mode 100644 index 0000000..f90a5da --- /dev/null +++ b/lib/shared/widgets/app_searchable_dropdown.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; + +import 'app_dropdown.dart'; + +/// Dropdown that opens a searchable bottom sheet to pick an option. +class AppSearchableDropdown extends StatelessWidget { + const AppSearchableDropdown({ + super.key, + required this.label, + required this.value, + required this.options, + required this.onChanged, + this.validator, + this.hint, + this.searchHint = 'Search...', + this.enabled = true, + this.isDense = false, + }); + + final String label; + final T? value; + final List> options; + final ValueChanged onChanged; + final String? Function(T?)? validator; + final String? hint; + final String searchHint; + final bool enabled; + final bool isDense; + + String? _labelForValue(T? selected) { + if (selected == null) return null; + for (final option in options) { + if (option.value == selected) return option.label; + } + return null; + } + + Future _openPicker(BuildContext context, FormFieldState field) async { + if (!enabled || options.isEmpty) return; + + final theme = Theme.of(context); + + final selected = await showModalBottomSheet( + context: context, + isScrollControlled: true, + showDragHandle: true, + backgroundColor: theme.bottomSheetTheme.backgroundColor ?? + theme.colorScheme.surface, + builder: (context) => Theme( + data: theme, + child: _SearchableDropdownSheet( + title: label, + options: options, + selected: value, + searchHint: searchHint, + ), + ), + ); + + if (selected == null) return; + field.didChange(selected); + onChanged(selected); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final displayLabel = _labelForValue(value); + + return FormField( + initialValue: value, + validator: validator, + builder: (field) { + final effectiveHint = hint ?? 'Select ${label.toLowerCase()}'; + final canOpen = enabled && options.isNotEmpty; + final colors = theme.colorScheme; + + return InkWell( + onTap: canOpen ? () => _openPicker(context, field) : null, + borderRadius: BorderRadius.circular(8), + child: InputDecorator( + isFocused: false, + isEmpty: displayLabel == null, + decoration: InputDecoration( + labelText: label, + hintText: displayLabel == null ? effectiveHint : null, + floatingLabelBehavior: FloatingLabelBehavior.always, + isDense: isDense, + errorText: field.errorText, + suffixIcon: Icon( + Icons.arrow_drop_down, + color: canOpen ? colors.onSurfaceVariant : theme.disabledColor, + ), + enabled: canOpen, + ), + child: displayLabel == null + ? const SizedBox.shrink() + : Text( + displayLabel, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyLarge?.copyWith( + color: colors.onSurface, + ), + ), + ), + ); + }, + ); + } +} + +class _SearchableDropdownSheet extends StatefulWidget { + const _SearchableDropdownSheet({ + required this.title, + required this.options, + required this.selected, + required this.searchHint, + }); + + final String title; + final List> options; + final T? selected; + final String searchHint; + + @override + State<_SearchableDropdownSheet> createState() => + _SearchableDropdownSheetState(); +} + +class _SearchableDropdownSheetState extends State<_SearchableDropdownSheet> { + final _searchController = TextEditingController(); + String _query = ''; + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + List> get _filtered { + final q = _query.trim().toLowerCase(); + if (q.isEmpty) return widget.options; + return widget.options + .where((option) => option.label.toLowerCase().contains(q)) + .toList(); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final maxHeight = MediaQuery.sizeOf(context).height * 0.55; + final filtered = _filtered; + + return SafeArea( + child: Material( + color: theme.colorScheme.surface, + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.viewInsetsOf(context).bottom, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 0), + child: Text( + widget.title, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(16), + child: TextField( + controller: _searchController, + autofocus: true, + decoration: InputDecoration( + hintText: widget.searchHint, + prefixIcon: const Icon(Icons.search, size: 20), + isDense: true, + ), + onChanged: (value) => setState(() => _query = value), + ), + ), + ConstrainedBox( + constraints: BoxConstraints(maxHeight: maxHeight), + child: filtered.isEmpty + ? Padding( + padding: const EdgeInsets.all(24), + child: Text( + 'No options found', + textAlign: TextAlign.center, + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ) + : ListView.separated( + shrinkWrap: true, + itemCount: filtered.length, + separatorBuilder: (_, __) => const Divider(height: 1), + itemBuilder: (context, index) { + final option = filtered[index]; + final isSelected = option.value == widget.selected; + return ListTile( + title: Text( + option.label, + overflow: TextOverflow.ellipsis, + ), + trailing: isSelected + ? Icon( + Icons.check, + color: theme.colorScheme.primary, + ) + : null, + selected: isSelected, + onTap: () => Navigator.of(context).pop(option.value), + ); + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/shared/widgets/app_shell.dart b/lib/shared/widgets/app_shell.dart new file mode 100644 index 0000000..3968293 --- /dev/null +++ b/lib/shared/widgets/app_shell.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/config/dev_config.dart'; +import '../../core/constants/app_constants.dart'; +import '../../core/constants/route_constants.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/utils/responsive_utils.dart'; +import '../../core/constants/enums.dart'; +import '../../modules/settings/presentation/providers/settings_provider.dart'; +import '../models/user_model.dart'; +import '../providers/auth_provider.dart'; +import '../routes/menu_config.dart' as menu; +import '../utils/navigation_utils.dart'; +import 'app_sidebar.dart'; +import 'app_top_nav.dart'; + +class AppShell extends ConsumerStatefulWidget { + const AppShell({super.key, required this.child}); + + final Widget child; + + @override + ConsumerState createState() => _AppShellState(); +} + +class _AppShellState extends ConsumerState { + final _scaffoldKey = GlobalKey(); + bool _sidebarCollapsed = false; + + @override + Widget build(BuildContext context) { + final authState = ref.watch(authStateProvider); + final user = authState.user; + final currentRoute = GoRouterState.of(context).matchedLocation; + + final menuItems = user != null + ? menu.getVisibleMenuItems( + permissions: user.permissions, + role: user.userRole, + ) + : []; + + if (context.isMobile) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text(AppConstants.appName), + leading: IconButton( + icon: const Icon(Icons.menu), + onPressed: () => _scaffoldKey.currentState?.openDrawer(), + ), + actions: [ + if (DevConfig.screenPreviewEnabled) + IconButton( + icon: const Icon(Icons.apps_outlined), + tooltip: 'All Screens', + onPressed: () => context.go(RouteConstants.screenGallery), + ), + _UserMenu(userName: user?.name), + ], + ), + drawer: _AppDrawer( + menuItems: menuItems, + currentRoute: currentRoute, + user: user, + onItemTap: (route) { + goAndDismissOverlays(context, route); + _scaffoldKey.currentState?.closeDrawer(); + }, + ), + body: KeyedSubtree( + key: ValueKey(currentRoute), + child: widget.child, + ), + ); + } + + final navigationLayout = NavigationLayout.fromValue( + ref.watch(appSettingsProvider).uiPreferences.navigationLayout, + ); + final useTopNav = navigationLayout == NavigationLayout.top; + + if (useTopNav) { + return Scaffold( + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? AppColors.darkBackground + : AppColors.lightBackground, + body: Column( + children: [ + AppTopNav( + menuItems: menuItems, + currentRoute: currentRoute, + user: user, + onItemTap: (route) => goAndDismissOverlays(context, route), + ), + Expanded( + child: KeyedSubtree( + key: ValueKey(currentRoute), + child: widget.child, + ), + ), + ], + ), + ); + } + + return Scaffold( + backgroundColor: Theme.of(context).brightness == Brightness.dark + ? AppColors.darkBackground + : AppColors.lightBackground, + body: Padding( + padding: const EdgeInsets.all(12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppSidebar( + menuItems: menuItems, + currentRoute: currentRoute, + user: user, + collapsed: _sidebarCollapsed, + onToggleCollapse: () => setState(() => _sidebarCollapsed = !_sidebarCollapsed), + onItemTap: (route) => goAndDismissOverlays(context, route), + ), + const SizedBox(width: 12), + Expanded( + child: KeyedSubtree( + key: ValueKey(currentRoute), + child: widget.child, + ), + ), + ], + ), + ), + ); + } +} + +class _AppDrawer extends ConsumerWidget { + const _AppDrawer({ + required this.menuItems, + required this.currentRoute, + required this.onItemTap, + this.user, + }); + + final List menuItems; + final String currentRoute; + final void Function(String route) onItemTap; + final UserModel? user; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Drawer( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), + ), + ), + child: SafeArea( + child: AppSidebar( + menuItems: menuItems, + currentRoute: currentRoute, + user: user, + isDrawer: true, + onItemTap: onItemTap, + ), + ), + ); + } +} + +class _UserMenu extends ConsumerWidget { + const _UserMenu({this.userName}); + + final String? userName; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PopupMenuButton( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 16, + child: Text((userName ?? 'U')[0].toUpperCase()), + ), + const SizedBox(width: 8), + if (!context.isMobile) Text(userName ?? 'User'), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + onSelected: (value) async { + switch (value) { + case 'password': + context.push(RouteConstants.changePassword); + case 'settings': + goAndDismissOverlays(context, RouteConstants.settings); + case 'logout': + await ref.read(authStateProvider.notifier).logout(); + if (context.mounted) context.go(RouteConstants.login); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'password', child: Text('Change Password')), + const PopupMenuItem(value: 'settings', child: Text('Settings')), + const PopupMenuDivider(), + const PopupMenuItem(value: 'logout', child: Text('Logout')), + ], + ); + } +} diff --git a/lib/shared/widgets/app_sidebar.dart b/lib/shared/widgets/app_sidebar.dart new file mode 100644 index 0000000..1d8e986 --- /dev/null +++ b/lib/shared/widgets/app_sidebar.dart @@ -0,0 +1,627 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/constants/app_constants.dart'; +import '../../core/constants/enums.dart'; +import '../../core/constants/route_constants.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/theme_provider.dart'; +import '../../modules/settings/presentation/providers/settings_provider.dart'; +import '../models/user_model.dart'; +import '../providers/auth_provider.dart'; +import '../routes/menu_config.dart' as menu; +import 'sidebar_logo.dart'; + +const _sidebarExpandedWidth = 280.0; +const _sidebarCollapsedWidth = 72.0; +const _sidebarNarrowBreakpoint = 200.0; +const _sidebarItemHeight = 44.0; +const _sidebarItemPadding = 12.0; +const _sidebarIconSize = 20.0; +const _sidebarChildIndent = 28.0; + +class AppSidebar extends ConsumerStatefulWidget { + const AppSidebar({ + super.key, + required this.menuItems, + required this.currentRoute, + required this.onItemTap, + this.user, + this.collapsed = false, + this.onToggleCollapse, + this.isDrawer = false, + }); + + final List menuItems; + final String currentRoute; + final void Function(String route) onItemTap; + final UserModel? user; + final bool collapsed; + final VoidCallback? onToggleCollapse; + final bool isDrawer; + + @override + ConsumerState createState() => _AppSidebarState(); +} + +class _AppSidebarState extends ConsumerState { + final Set _expandedMenus = {}; + + List get _mainMenuItems => + widget.menuItems.where((item) => item.route != RouteConstants.settings).toList(); + + bool _isSelected(String route) { + if (route == '/') return widget.currentRoute == route; + return widget.currentRoute.startsWith(route); + } + + bool _isGroupActive(menu.MenuItem item) => + item.children.any((child) => _isSelected(child.route)); + + bool _isGroupExpanded(menu.MenuItem item) => + _expandedMenus.contains(item.route) || _isGroupActive(item); + + void _toggleGroup(menu.MenuItem item) { + setState(() { + if (_expandedMenus.contains(item.route)) { + _expandedMenus.remove(item.route); + } else { + _expandedMenus.add(item.route); + } + }); + } + + @override + void didUpdateWidget(covariant AppSidebar oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.currentRoute != widget.currentRoute) { + for (final item in _mainMenuItems) { + if (item.children.isNotEmpty && _isGroupActive(item)) { + _expandedMenus.add(item.route); + } + } + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final themeMode = ref.watch(themeModeProvider); + final isLightActive = themeMode == ThemeModeOption.light || + (themeMode == ThemeModeOption.system && !isDark); + + return AnimatedContainer( + duration: AppConstants.animationDuration, + width: widget.isDrawer + ? double.infinity + : (widget.collapsed ? _sidebarCollapsedWidth : _sidebarExpandedWidth), + child: DecoratedBox( + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurface : AppColors.lightSurface, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: theme.colorScheme.outline.withValues(alpha: 0.08), + ), + boxShadow: widget.isDrawer + ? null + : [ + BoxShadow( + color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.06), + blurRadius: 24, + offset: const Offset(0, 4), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: LayoutBuilder( + builder: (context, constraints) { + final isNarrow = !widget.isDrawer && + constraints.maxWidth < _sidebarNarrowBreakpoint; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildHeader(context, isNarrow: isNarrow), + if (!isNarrow) ...[ + const SizedBox(height: 16), + _buildThemeToggle(context, isLightActive), + ], + const SizedBox(height: 20), + Expanded( + child: SingleChildScrollView( + padding: EdgeInsets.symmetric(horizontal: isNarrow ? 8 : 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!isNarrow) + const _SectionLabel(label: 'MAIN MENU') + else + const SizedBox(height: 4), + ..._mainMenuItems.map((item) { + if (item.children.isNotEmpty && !isNarrow) { + final expanded = _isGroupExpanded(item); + final active = _isGroupActive(item); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _SidebarNavItem( + icon: item.icon, + label: item.label, + selected: active, + collapsed: false, + showChevron: true, + chevronExpanded: expanded, + onTap: () => _toggleGroup(item), + ), + if (expanded) + ...item.children.map( + (child) => _SidebarNavItem( + icon: child.icon, + label: child.label, + selected: _isSelected(child.route), + collapsed: false, + indent: _sidebarChildIndent, + onTap: () => widget.onItemTap(child.route), + ), + ), + ], + ); + } + + return _SidebarNavItem( + icon: item.icon, + label: item.label, + selected: _isSelected(item.route) || + (isNarrow && _isGroupActive(item)), + collapsed: isNarrow, + onTap: () { + if (item.children.isNotEmpty && isNarrow) { + widget.onItemTap(item.children.first.route); + return; + } + widget.onItemTap(item.route); + }, + ); + }), + const SizedBox(height: 16), + if (!isNarrow) const _SectionLabel(label: 'SUPPORT'), + _SidebarNavItem( + icon: Icons.notifications_outlined, + label: 'Notifications', + selected: false, + collapsed: isNarrow, + badge: isNarrow ? null : '3', + onTap: () {}, + ), + if (_hasSettings) + _SidebarNavItem( + icon: Icons.settings_outlined, + label: 'Settings', + selected: _isSelected(RouteConstants.settings), + collapsed: isNarrow, + onTap: () => widget.onItemTap(RouteConstants.settings), + ), + ], + ), + ), + ), + _buildUserProfile(context, isNarrow: isNarrow), + ], + ); + }, + ), + ), + ), + ); + } + + bool get _hasSettings => + widget.menuItems.any((item) => item.route == RouteConstants.settings); + + Widget _buildHeader(BuildContext context, {required bool isNarrow}) { + final theme = Theme.of(context); + final companyProfile = ref.watch(appSettingsProvider).companyProfile; + final branding = ref.watch(brandingProvider); + final logoUrl = resolveSidebarLogoUrl( + companyProfileLogo: companyProfile.logoUrl, + brandingLogo: branding.logoUrl, + ); + final title = resolveSidebarTitle( + companyName: companyProfile.companyName, + fallback: AppConstants.appName, + ); + + if (isNarrow) { + return Padding( + padding: const EdgeInsets.fromLTRB(6, 16, 6, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + SidebarLogo(logoUrl: logoUrl, size: 32), + if (widget.onToggleCollapse != null) ...[ + const SizedBox(height: 2), + IconButton( + padding: EdgeInsets.zero, + constraints: const BoxConstraints.tightFor(width: 32, height: 32), + visualDensity: VisualDensity.compact, + icon: const Icon(Icons.chevron_right, size: 18), + tooltip: 'Expand sidebar', + onPressed: widget.onToggleCollapse, + ), + ], + ], + ), + ); + } + + return Padding( + padding: const EdgeInsets.fromLTRB(14, 16, 8, 0), + child: Row( + children: [ + SidebarLogo(logoUrl: logoUrl, size: 36), + const SizedBox(width: 8), + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: -0.2, + ), + ), + ), + if (widget.onToggleCollapse != null) + IconButton( + icon: const Icon(Icons.chevron_left, size: 20), + tooltip: 'Collapse sidebar', + visualDensity: VisualDensity.compact, + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints.tightFor(width: 36, height: 36), + onPressed: widget.onToggleCollapse, + ), + ], + ), + ); + } + + Widget _buildThemeToggle(BuildContext context, bool isLightActive) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Container( + height: 36, + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest.withValues(alpha: 0.6), + borderRadius: BorderRadius.circular(20), + ), + child: Row( + children: [ + Expanded( + child: _ThemeToggleOption( + label: 'LIGHT', + selected: isLightActive, + onTap: () => ref.read(themeModeProvider.notifier).setThemeMode(ThemeModeOption.light), + ), + ), + Expanded( + child: _ThemeToggleOption( + label: 'DARK', + selected: !isLightActive, + onTap: () => ref.read(themeModeProvider.notifier).setThemeMode(ThemeModeOption.dark), + ), + ), + ], + ), + ), + ); + } + + Widget _buildUserProfile(BuildContext context, {required bool isNarrow}) { + final theme = Theme.of(context); + final user = widget.user; + final name = user?.name ?? 'User'; + final email = user?.email ?? ''; + + if (isNarrow) { + return Padding( + padding: const EdgeInsets.all(12), + child: Center( + child: _UserAvatar(name: name), + ), + ); + } + + return Padding( + padding: const EdgeInsets.all(12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.45), + borderRadius: BorderRadius.circular(14), + ), + child: Row( + children: [ + _UserAvatar(name: name), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + Text( + email, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + _UserProfileMenu(userName: name), + ], + ), + ), + ); + } +} + +class _SectionLabel extends StatelessWidget { + const _SectionLabel({required this.label}); + + final String label; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(left: 12, bottom: 8), + child: Text( + label, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + letterSpacing: 0.8, + ), + ), + ); + } +} + +class _SidebarNavItem extends StatelessWidget { + const _SidebarNavItem({ + required this.icon, + required this.label, + required this.selected, + required this.collapsed, + required this.onTap, + this.badge, + this.indent = 0, + this.showChevron = false, + this.chevronExpanded = false, + }); + + final IconData icon; + final String label; + final bool selected; + final bool collapsed; + final VoidCallback onTap; + final String? badge; + final double indent; + final bool showChevron; + final bool chevronExpanded; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final primary = theme.colorScheme.primary; + const itemHeight = _sidebarItemHeight; + const itemPadding = _sidebarItemPadding; + const iconSize = _sidebarIconSize; + + if (collapsed) { + return Padding( + padding: const EdgeInsets.only(bottom: 4), + child: SizedBox( + width: double.infinity, + child: Tooltip( + message: label, + waitDuration: const Duration(milliseconds: 500), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Container( + height: itemHeight, + alignment: Alignment.center, + decoration: BoxDecoration( + color: selected ? primary.withValues(alpha: 0.12) : Colors.transparent, + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + icon, + size: 22, + color: selected ? primary : theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ), + ); + } + + return Padding( + padding: EdgeInsets.only(left: indent, bottom: 4), + child: Material( + color: selected ? primary.withValues(alpha: 0.1) : Colors.transparent, + borderRadius: BorderRadius.circular(12), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: SizedBox( + height: itemHeight, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: itemPadding), + child: Row( + children: [ + SizedBox( + width: iconSize, + child: Icon( + icon, + size: iconSize, + color: selected ? primary : theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + label, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodyMedium?.copyWith( + color: selected ? primary : theme.colorScheme.onSurfaceVariant, + fontWeight: selected ? FontWeight.w600 : FontWeight.w500, + ), + ), + ), + if (badge != null) + Container( + margin: const EdgeInsets.only(right: 4), + padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2), + decoration: BoxDecoration( + color: AppColors.success, + borderRadius: BorderRadius.circular(10), + ), + child: Text( + badge!, + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + if (showChevron) + Icon( + chevronExpanded + ? Icons.keyboard_arrow_up + : Icons.keyboard_arrow_down, + size: 18, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ), + ), + ); + } +} + +class _ThemeToggleOption extends StatelessWidget { + const _ThemeToggleOption({ + required this.label, + required this.selected, + required this.onTap, + }); + + final String label; + final bool selected; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: AppConstants.animationDuration, + alignment: Alignment.center, + decoration: BoxDecoration( + color: selected ? theme.colorScheme.primary : Colors.transparent, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + color: selected ? Colors.white : theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w700, + letterSpacing: 0.5, + ), + ), + ), + ); + } +} + +class _UserAvatar extends StatelessWidget { + const _UserAvatar({required this.name}); + + final String name; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return CircleAvatar( + radius: 18, + backgroundColor: theme.colorScheme.primary.withValues(alpha: 0.15), + child: Text( + name.isNotEmpty ? name[0].toUpperCase() : 'U', + style: theme.textTheme.titleSmall?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w700, + ), + ), + ); + } +} + +class _UserProfileMenu extends ConsumerWidget { + const _UserProfileMenu({this.userName}); + + final String? userName; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PopupMenuButton( + icon: Icon( + Icons.more_vert, + size: 20, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + padding: EdgeInsets.zero, + onSelected: (value) async { + switch (value) { + case 'profile': + context.push(RouteConstants.profile); + case 'password': + context.push(RouteConstants.changePassword); + case 'settings': + context.go(RouteConstants.settings); + case 'logout': + await ref.read(authStateProvider.notifier).logout(); + if (context.mounted) context.go(RouteConstants.login); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'profile', child: Text('Profile')), + const PopupMenuItem(value: 'password', child: Text('Change Password')), + const PopupMenuItem(value: 'settings', child: Text('Settings')), + const PopupMenuDivider(), + const PopupMenuItem(value: 'logout', child: Text('Logout')), + ], + ); + } +} diff --git a/lib/shared/widgets/app_status_chip.dart b/lib/shared/widgets/app_status_chip.dart new file mode 100644 index 0000000..0ec085a --- /dev/null +++ b/lib/shared/widgets/app_status_chip.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import '../../core/constants/enums.dart'; + +class AppStatusChip extends StatelessWidget { + const AppStatusChip({ + super.key, + required this.status, + this.compact = false, + }); + + final String status; + final bool compact; + + @override + Widget build(BuildContext context) { + final (color, label) = _resolveStatus(status); + return Chip( + label: Text( + label, + style: TextStyle( + color: color, + fontSize: compact ? 11 : 12, + fontWeight: FontWeight.w600, + ), + ), + backgroundColor: color.withValues(alpha: 0.12), + side: BorderSide(color: color.withValues(alpha: 0.3)), + visualDensity: compact ? VisualDensity.compact : VisualDensity.standard, + padding: compact ? EdgeInsets.zero : null, + ); + } + + (Color, String) _resolveStatus(String raw) { + switch (raw.toLowerCase()) { + case 'active': + return (Colors.green.shade700, EntityStatus.active.label); + case 'inactive': + return (Colors.grey.shade700, EntityStatus.inactive.label); + case 'locked': + return (Colors.orange.shade800, 'Locked'); + default: + return (Colors.blueGrey, raw); + } + } +} diff --git a/lib/shared/widgets/app_text_field.dart b/lib/shared/widgets/app_text_field.dart new file mode 100644 index 0000000..1ea0a04 --- /dev/null +++ b/lib/shared/widgets/app_text_field.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +class AppTextField extends StatelessWidget { + const AppTextField({ + super.key, + required this.controller, + this.label, + this.hint, + this.prefixIcon, + this.suffixIcon, + this.obscureText = false, + this.keyboardType, + this.validator, + this.onChanged, + this.maxLines = 1, + this.enabled = true, + this.autofillHints, + }); + + final TextEditingController controller; + final String? label; + final String? hint; + final Widget? prefixIcon; + final Widget? suffixIcon; + final bool obscureText; + final TextInputType? keyboardType; + final String? Function(String?)? validator; + final void Function(String)? onChanged; + final int maxLines; + final bool enabled; + final Iterable? autofillHints; + + @override + Widget build(BuildContext context) { + return TextFormField( + controller: controller, + obscureText: obscureText, + keyboardType: keyboardType, + validator: validator, + onChanged: onChanged, + maxLines: maxLines, + enabled: enabled, + autofillHints: autofillHints, + decoration: InputDecoration( + labelText: label, + hintText: hint, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + ), + ); + } +} diff --git a/lib/shared/widgets/app_top_nav.dart b/lib/shared/widgets/app_top_nav.dart new file mode 100644 index 0000000..58f7b3e --- /dev/null +++ b/lib/shared/widgets/app_top_nav.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../core/config/dev_config.dart'; +import '../../core/constants/app_constants.dart'; +import '../../core/constants/route_constants.dart'; +import '../../core/theme/app_colors.dart'; +import '../../core/theme/theme_provider.dart'; +import '../../modules/settings/presentation/providers/settings_provider.dart'; +import '../models/user_model.dart'; +import '../providers/auth_provider.dart'; +import '../routes/menu_config.dart' as menu; +import 'sidebar_logo.dart'; + +class AppTopNav extends ConsumerWidget { + const AppTopNav({ + super.key, + required this.menuItems, + required this.currentRoute, + required this.onItemTap, + this.user, + }); + + final List menuItems; + final String currentRoute; + final void Function(String route) onItemTap; + final UserModel? user; + + bool _isSelected(String route) { + if (route == '/') return currentRoute == route; + return currentRoute.startsWith(route); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + final isDark = theme.brightness == Brightness.dark; + final companyProfile = ref.watch(appSettingsProvider).companyProfile; + final branding = ref.watch(brandingProvider); + final logoUrl = resolveSidebarLogoUrl( + companyProfileLogo: companyProfile.logoUrl, + brandingLogo: branding.logoUrl, + ); + final companyName = resolveSidebarTitle( + companyName: companyProfile.companyName, + fallback: AppConstants.appName, + ); + + return Material( + color: isDark ? AppColors.darkSurface : AppColors.lightSurface, + elevation: 0, + child: Container( + height: 64, + padding: const EdgeInsets.symmetric(horizontal: 24), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outline.withValues(alpha: 0.12), + ), + ), + ), + child: Row( + children: [ + SidebarLogo(logoUrl: logoUrl, size: 32), + const SizedBox(width: 10), + Text( + companyName, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(width: 32), + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: menuItems.map((item) { + final selected = _isSelected(item.route); + return Padding( + padding: const EdgeInsets.only(right: 4), + child: TextButton( + onPressed: () => onItemTap(item.route), + style: TextButton.styleFrom( + foregroundColor: selected + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, + backgroundColor: selected + ? theme.colorScheme.primary.withValues(alpha: 0.08) + : Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + child: Text( + item.label, + style: TextStyle( + fontWeight: + selected ? FontWeight.w600 : FontWeight.w500, + ), + ), + ), + ); + }).toList(), + ), + ), + ), + IconButton( + icon: const Icon(Icons.notifications_outlined, size: 22), + onPressed: () {}, + ), + if (DevConfig.screenPreviewEnabled) + IconButton( + icon: const Icon(Icons.apps_outlined, size: 22), + tooltip: 'All Screens', + onPressed: () => context.go(RouteConstants.screenGallery), + ), + _TopNavUserMenu(userName: user?.name), + ], + ), + ), + ); + } +} + +class _TopNavUserMenu extends ConsumerWidget { + const _TopNavUserMenu({this.userName}); + + final String? userName; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PopupMenuButton( + offset: const Offset(0, 48), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 16, + child: Text((userName ?? 'U')[0].toUpperCase()), + ), + const SizedBox(width: 8), + Text(userName ?? 'User'), + const Icon(Icons.arrow_drop_down, size: 20), + ], + ), + ), + onSelected: (value) async { + switch (value) { + case 'profile': + context.push(RouteConstants.profile); + case 'password': + context.push(RouteConstants.changePassword); + case 'settings': + context.go(RouteConstants.settings); + case 'logout': + await ref.read(authStateProvider.notifier).logout(); + if (context.mounted) context.go(RouteConstants.login); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'profile', child: Text('Profile')), + const PopupMenuItem(value: 'password', child: Text('Change Password')), + const PopupMenuItem(value: 'settings', child: Text('Settings')), + const PopupMenuDivider(), + const PopupMenuItem(value: 'logout', child: Text('Logout')), + ], + ); + } +} diff --git a/lib/shared/widgets/error_view.dart b/lib/shared/widgets/error_view.dart new file mode 100644 index 0000000..7980155 --- /dev/null +++ b/lib/shared/widgets/error_view.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; + +import '../../core/errors/failure.dart'; + +class ErrorView extends StatelessWidget { + const ErrorView({ + super.key, + required this.message, + this.onRetry, + }); + + final String message; + final VoidCallback? onRetry; + + factory ErrorView.fromFailure(Failure failure, {VoidCallback? onRetry}) { + return ErrorView( + message: failure.when( + server: (message, _, __) => message, + network: (message) => message, + unauthorized: (message) => message, + validation: (message, _) => message, + notFound: (message) => message, + cache: (message) => message, + unknown: (message) => message, + ), + onRetry: onRetry, + ); + } + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.error_outline, size: 48, color: Theme.of(context).colorScheme.error), + const SizedBox(height: 16), + Text( + message, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyLarge, + ), + if (onRetry != null) ...[ + const SizedBox(height: 16), + OutlinedButton.icon( + onPressed: onRetry, + icon: const Icon(Icons.refresh), + label: const Text('Retry'), + ), + ], + ], + ), + ), + ); + } +} + +class EmptyStateView extends StatelessWidget { + const EmptyStateView({ + super.key, + required this.title, + this.description, + this.icon = Icons.inbox_outlined, + this.action, + this.actionLabel, + }); + + final String title; + final String? description; + final IconData icon; + final VoidCallback? action; + final String? actionLabel; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(icon, size: 64, color: Theme.of(context).colorScheme.outline), + const SizedBox(height: 16), + Text(title, style: Theme.of(context).textTheme.titleLarge), + if (description != null) ...[ + const SizedBox(height: 8), + Text( + description!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + if (action != null && actionLabel != null) ...[ + const SizedBox(height: 24), + ElevatedButton(onPressed: action, child: Text(actionLabel!)), + ], + ], + ), + ), + ); + } +} diff --git a/lib/shared/widgets/kpi_card.dart b/lib/shared/widgets/kpi_card.dart new file mode 100644 index 0000000..77c29f5 --- /dev/null +++ b/lib/shared/widgets/kpi_card.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; + +import 'app_card.dart'; + +class KpiCard extends StatelessWidget { + const KpiCard({ + super.key, + required this.title, + required this.value, + required this.icon, + this.color, + this.onTap, + }); + + final String title; + final String value; + final IconData icon; + final Color? color; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final cardColor = color ?? Theme.of(context).colorScheme.primary; + + return AppCard( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: cardColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Icon(icon, color: cardColor, size: 24), + ), + const Spacer(), + ], + ), + const SizedBox(height: 16), + Text( + value, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + title, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/shared/widgets/loading_indicator.dart b/lib/shared/widgets/loading_indicator.dart new file mode 100644 index 0000000..32d49b3 --- /dev/null +++ b/lib/shared/widgets/loading_indicator.dart @@ -0,0 +1,68 @@ +import 'app_card.dart'; + +import 'package:flutter/material.dart'; + +class LoadingOverlay extends StatelessWidget { + const LoadingOverlay({ + super.key, + required this.isLoading, + required this.child, + this.message, + }); + + final bool isLoading; + final Widget child; + final String? message; + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + child, + if (isLoading) + ColoredBox( + color: Colors.black26, + child: Center( + child: AppCard( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + if (message != null) ...[ + const SizedBox(height: 16), + Text(message!), + ], + ], + ), + ), + ), + ), + ), + ], + ); + } +} + +class AppLoadingIndicator extends StatelessWidget { + const AppLoadingIndicator({super.key, this.message}); + + final String? message; + + @override + Widget build(BuildContext context) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + if (message != null) ...[ + const SizedBox(height: 16), + Text(message!, style: Theme.of(context).textTheme.bodyMedium), + ], + ], + ), + ); + } +} diff --git a/lib/shared/widgets/page_header.dart b/lib/shared/widgets/page_header.dart new file mode 100644 index 0000000..6e521e9 --- /dev/null +++ b/lib/shared/widgets/page_header.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; + +import '../../core/utils/responsive_utils.dart'; + +class PageHeader extends StatelessWidget { + const PageHeader({ + super.key, + required this.title, + this.subtitle, + this.actions, + }); + + final String title; + final String? subtitle; + final List? actions; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 24), + child: context.isMobile + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.headlineSmall), + if (subtitle != null) ...[ + const SizedBox(height: 4), + Text( + subtitle!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + if (actions != null && actions!.isNotEmpty) ...[ + const SizedBox(height: 16), + Wrap(spacing: 8, runSpacing: 8, children: actions!), + ], + ], + ) + : Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.headlineSmall), + if (subtitle != null) ...[ + const SizedBox(height: 4), + Text( + subtitle!, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + if (actions != null) ...actions!, + ], + ), + ); + } +} diff --git a/lib/shared/widgets/permission_guard.dart b/lib/shared/widgets/permission_guard.dart new file mode 100644 index 0000000..d381ab7 --- /dev/null +++ b/lib/shared/widgets/permission_guard.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../core/constants/enums.dart'; +import '../../core/utils/permission_utils.dart'; + +class PermissionGuard extends StatelessWidget { + const PermissionGuard({ + super.key, + required this.module, + required this.action, + required this.permissions, + required this.child, + this.fallback, + }); + + final String module; + final PermissionAction action; + final List permissions; + final Widget child; + final Widget? fallback; + + @override + Widget build(BuildContext context) { + if (hasPermission( + userPermissions: permissions, + module: module, + action: action, + )) { + return child; + } + return fallback ?? const SizedBox.shrink(); + } +} diff --git a/lib/shared/widgets/placeholder_screen.dart b/lib/shared/widgets/placeholder_screen.dart new file mode 100644 index 0000000..c5c6067 --- /dev/null +++ b/lib/shared/widgets/placeholder_screen.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class PlaceholderScreen extends StatelessWidget { + const PlaceholderScreen({ + super.key, + required this.title, + this.description, + }); + + final String title; + final String? description; + + @override + Widget build(BuildContext context) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.construction_outlined, + size: 64, + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text(title, style: Theme.of(context).textTheme.headlineSmall), + if (description != null) ...[ + const SizedBox(height: 8), + Text( + description!, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/shared/widgets/sidebar_logo.dart b/lib/shared/widgets/sidebar_logo.dart new file mode 100644 index 0000000..d3a7147 --- /dev/null +++ b/lib/shared/widgets/sidebar_logo.dart @@ -0,0 +1,96 @@ +import 'dart:convert'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; + +/// Displays company logo in the sidebar from URL, data URI, or fallback icon. +class SidebarLogo extends StatelessWidget { + const SidebarLogo({ + super.key, + this.logoUrl, + this.size = 36, + }); + + final String? logoUrl; + final double size; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final fallback = Icon( + Icons.inventory_2_outlined, + size: size * 0.55, + color: theme.colorScheme.primary, + ); + + return Container( + width: size, + height: size, + decoration: BoxDecoration( + color: theme.colorScheme.primary.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(10), + ), + clipBehavior: Clip.antiAlias, + child: _buildLogoContent(fallback), + ); + } + + Widget _buildLogoContent(Widget fallback) { + final url = logoUrl?.trim(); + if (url == null || url.isEmpty) { + return Center(child: fallback); + } + + if (url.startsWith('data:image')) { + try { + final base64Str = url.contains(',') ? url.split(',').last : url; + return Image.memory( + base64Decode(base64Str), + width: size, + height: size, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => Center(child: fallback), + ); + } catch (_) { + return Center(child: fallback); + } + } + + if (url.startsWith('http://') || url.startsWith('https://')) { + return CachedNetworkImage( + imageUrl: url, + width: size, + height: size, + fit: BoxFit.cover, + placeholder: (_, __) => Center( + child: SizedBox( + width: size * 0.4, + height: size * 0.4, + child: const CircularProgressIndicator(strokeWidth: 2), + ), + ), + errorWidget: (_, __, ___) => Center(child: fallback), + ); + } + + return Center(child: fallback); + } +} + +/// Resolves logo URL from company profile settings or branding config. +String? resolveSidebarLogoUrl({ + required String companyProfileLogo, + required String? brandingLogo, +}) { + if (companyProfileLogo.isNotEmpty) return companyProfileLogo; + if (brandingLogo != null && brandingLogo.isNotEmpty) return brandingLogo; + return null; +} + +/// Resolves sidebar title from company name or app tagline. +String resolveSidebarTitle({ + required String companyName, + required String fallback, +}) { + return companyName.isNotEmpty ? companyName : fallback; +} diff --git a/pubspec.lock b/pubspec.lock new file mode 100644 index 0000000..cdab5da --- /dev/null +++ b/pubspec.lock @@ -0,0 +1,1146 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: da0d9209ca76bde579f2da330aeb9df62b6319c834fa7baae052021b0462401f + url: "https://pub.dev" + source: hosted + version: "85.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: "974859dc0ff5f37bc4313244b3218c791810d03ab3470a579580279ba971a48d" + url: "https://pub.dev" + source: hosted + version: "7.7.1" + args: + dependency: transitive + description: + name: args + sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + async: + dependency: transitive + description: + name: async + sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + url: "https://pub.dev" + source: hosted + version: "2.13.1" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + build: + dependency: transitive + description: + name: build + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_config: + dependency: transitive + description: + name: build_config + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 + url: "https://pub.dev" + source: hosted + version: "4.1.1" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" + url: "https://pub.dev" + source: hosted + version: "9.1.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" + url: "https://pub.dev" + source: hosted + version: "8.12.6" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" + url: "https://pub.dev" + source: hosted + version: "2.0.4" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" + url: "https://pub.dev" + source: hosted + version: "4.11.1" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + sha256: b5e72753cf63becce2c61fd04dfe0f1c430cc5278b53a1342dc5ad839eab29ec + url: "https://pub.dev" + source: hosted + version: "6.1.5" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + sha256: "3c09627c536d22fd24691a905cdd8b14520de69da52c7a97499c8be5284a32ed" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937" + url: "https://pub.dev" + source: hosted + version: "0.3.5+2" + crypto: + dependency: transitive + description: + name: crypto + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf + url: "https://pub.dev" + source: hosted + version: "3.0.7" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + url: "https://pub.dev" + source: hosted + version: "1.0.9" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "8a0e5fba27e8ee025d2ffb4ee820b4e6e2cf5e4246a6b1a477eb66866947e0bb" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" + url: "https://pub.dev" + source: hosted + version: "0.7.14" + dio: + dependency: "direct main" + description: + name: dio + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c + url: "https://pub.dev" + source: hosted + version: "5.9.2" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + equatable: + dependency: "direct main" + description: + name: equatable + sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b" + url: "https://pub.dev" + source: hosted + version: "2.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + file_picker: + dependency: "direct main" + description: + name: file_picker + sha256: f13a03000d942e476bc1ff0a736d2e9de711d2f89a95cd4c1d88f861c3348387 + url: "https://pub.dev" + source: hosted + version: "11.0.2" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: b7c7be5cd9f6ef7a78429cabd2774d3c4af50e79cb2b7593e3d5d763ef95c61b + url: "https://pub.dev" + source: hosted + version: "5.2.1" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + url: "https://pub.dev" + source: hosted + version: "2.0.34" + flutter_riverpod: + dependency: "direct main" + description: + name: flutter_riverpod + sha256: "9532ee6db4a943a1ed8383072a2e3eeda041db5657cdf6d2acecf3c21ecbe7e1" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + name: flutter_secure_storage + sha256: "9cad52d75ebc511adfae3d447d5d13da15a55a92c9410e50f67335b6d21d16ea" + url: "https://pub.dev" + source: hosted + version: "9.2.4" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: be76c1d24a97d0b98f8b54bce6b481a380a6590df992d0098f868ad54dc8f688 + url: "https://pub.dev" + source: hosted + version: "1.2.3" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "6c0a2795a2d1de26ae202a0d78527d163f4acbb11cde4c75c670f3a0fc064247" + url: "https://pub.dev" + source: hosted + version: "3.1.3" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + name: flutter_secure_storage_web + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: "59a584c24b3acdc5250bb856d0d3e9c0b798ed14a4af1ddb7dc1c7b41df91c9c" + url: "https://pub.dev" + source: hosted + version: "2.5.8" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" + source: hosted + version: "2.4.4" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + url: "https://pub.dev" + source: hosted + version: "14.8.1" + google_fonts: + dependency: "direct main" + description: + name: google_fonts + sha256: ba03d03bcaa2f6cb7bd920e3b5027181db75ab524f8891c8bc3aa603885b8055 + url: "https://pub.dev" + source: hosted + version: "6.3.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http: + dependency: transitive + description: + name: http + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" + url: "https://pub.dev" + source: hosted + version: "4.1.2" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f + url: "https://pub.dev" + source: hosted + version: "1.0.0" + jni_flutter: + dependency: transitive + description: + name: jni_flutter + sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: c50ef5fc083d5b5e12eef489503ba3bf5ccc899e487d691584699b4bdefeea8c + url: "https://pub.dev" + source: hosted + version: "6.9.5" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" + url: "https://pub.dev" + source: hosted + version: "11.0.2" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + logger: + dependency: "direct main" + description: + name: logger + sha256: "25aee487596a6257655a1e091ec2ae66bc30e7af663592cc3a27e6591e05035c" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "0b466a0a8a211b366c2e87f3345715faef9b6011c7147556ad22f37de6ba3173" + url: "https://pub.dev" + source: hosted + version: "6.0.11" + nm: + dependency: transitive + description: + name: nm + sha256: "2c9aae4127bdc8993206464fcc063611e0e36e72018696cd9631023a31b24254" + url: "https://pub.dev" + source: hosted + version: "0.5.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc + url: "https://pub.dev" + source: hosted + version: "2.2.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "6d13aece7b3f5c5a9731eaf553ff9dcbc2eff41087fd2df587fd0fed9a3eb0c4" + url: "https://pub.dev" + source: hosted + version: "2.5.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" + url: "https://pub.dev" + source: hosted + version: "7.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pool: + dependency: transitive + description: + name: pool + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" + url: "https://pub.dev" + source: hosted + version: "1.5.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "0560ba233314abbed0a48a2956f7f022cce7c3e1e73df540277da7544cad4082" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" + responsive_framework: + dependency: "direct main" + description: + name: responsive_framework + sha256: a8e1c13d4ba980c60cbf6fa1e9907cd60662bf2585184d7c96ca46c43de91552 + url: "https://pub.dev" + source: hosted + version: "1.5.1" + riverpod: + dependency: transitive + description: + name: riverpod + sha256: "59062512288d3056b2321804332a13ffdd1bf16df70dcc8e506e411280a72959" + url: "https://pub.dev" + source: hosted + version: "2.6.1" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + url: "https://pub.dev" + source: hosted + version: "2.4.23" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shelf: + dependency: transitive + description: + name: shelf + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + url: "https://pub.dev" + source: hosted + version: "1.4.2" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + url: "https://pub.dev" + source: hosted + version: "3.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: a447acb083d3a5ef17f983dd36201aeea33fedadb3228fa831f2f0c92f0f3aca + url: "https://pub.dev" + source: hosted + version: "1.3.7" + source_span: + dependency: transitive + description: + name: source_span + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" + url: "https://pub.dev" + source: hosted + version: "1.10.2" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 + url: "https://pub.dev" + source: hosted + version: "2.4.2+2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" + url: "https://pub.dev" + source: hosted + version: "2.5.6" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + sqflite_platform_interface: + dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + state_notifier: + dependency: transitive + description: + name: state_notifier + sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb + url: "https://pub.dev" + source: hosted + version: "1.0.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 + url: "https://pub.dev" + source: hosted + version: "3.4.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + uuid: + dependency: transitive + description: + name: uuid + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" + url: "https://pub.dev" + source: hosted + version: "4.5.3" + vector_graphics: + dependency: transitive + description: + name: vector_graphics + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + vector_graphics_codec: + dependency: transitive + description: + name: vector_graphics_codec + sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146" + url: "https://pub.dev" + source: hosted + version: "1.1.13" + vector_graphics_compiler: + dependency: transitive + description: + name: vector_graphics_compiler + sha256: b9b3f391857781aa96acacef96066f2f49b4cd03cf9fce3ca4d8da2ef5ea129e + url: "https://pub.dev" + source: hosted + version: "1.2.3" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" + url: "https://pub.dev" + source: hosted + version: "15.2.0" + watcher: + dependency: transitive + description: + name: watcher + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8 + url: "https://pub.dev" + source: hosted + version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + url: "https://pub.dev" + source: hosted + version: "5.15.0" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" + url: "https://pub.dev" + source: hosted + version: "6.6.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" +sdks: + dart: ">=3.9.2 <4.0.0" + flutter: ">=3.35.6" diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..85c12fb --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,70 @@ +name: bharat_erp +description: Bharat ERP - Phase 1 Asset Management System (AMS) +publish_to: 'none' +version: 1.0.0+1 + +environment: + sdk: ^3.9.2 + +dependencies: + flutter: + sdk: flutter + + cupertino_icons: ^1.0.8 + + # State Management + flutter_riverpod: ^2.6.1 + + # Routing + go_router: ^14.8.1 + + # Networking + dio: ^5.8.0+1 + connectivity_plus: ^6.1.4 + + # Serialization + freezed_annotation: ^2.4.4 + json_annotation: ^4.9.0 + + # Storage + shared_preferences: ^2.5.3 + flutter_secure_storage: ^9.2.4 + + # UI + responsive_framework: ^1.5.1 + google_fonts: ^6.2.1 + cached_network_image: ^3.4.1 + flutter_svg: ^2.0.17 + + # QR Code + qr_flutter: ^4.1.0 + mobile_scanner: ^6.0.7 + + # Utilities + intl: ^0.20.2 + logger: ^2.5.0 + flutter_dotenv: ^5.2.1 + equatable: ^2.0.7 + file_picker: ^11.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + build_runner: ^2.4.15 + freezed: ^2.5.8 + json_serializable: ^6.9.4 + +flutter: + uses-material-design: true + + assets: + - assets/images/ + - assets/icons/ + - .env + - .env.development + - .env.uat + - .env.production + - .env.development + - .env.uat + - .env.production diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 0000000..ecae6e1 --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,16 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import 'package:bharat_erp/app.dart'; + +void main() { + testWidgets('App smoke test', (WidgetTester tester) async { + await tester.pumpWidget( + const ProviderScope( + child: BharatErpApp(), + ), + ); + + expect(find.text('Bharat ERP'), findsOneWidget); + }); +} diff --git a/web/favicon.png b/web/favicon.png new file mode 100755 index 0000000..8aaa46a Binary files /dev/null and b/web/favicon.png differ diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png new file mode 100755 index 0000000..b749bfe Binary files /dev/null and b/web/icons/Icon-192.png differ diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png new file mode 100755 index 0000000..88cfd48 Binary files /dev/null and b/web/icons/Icon-512.png differ diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000..eb9b4d7 Binary files /dev/null and b/web/icons/Icon-maskable-192.png differ diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000..d69c566 Binary files /dev/null and b/web/icons/Icon-maskable-512.png differ diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f82bb7c --- /dev/null +++ b/web/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + bharat_erp + + + + + + diff --git a/web/manifest.json b/web/manifest.json new file mode 100644 index 0000000..53af80d --- /dev/null +++ b/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "bharat_erp", + "short_name": "bharat_erp", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +}