diff --git a/.env.example b/.env.example index 993fb9a..28ddac9 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ +# Dev API (demo server). Override only if needed: API_BASE_URL=https://demo.venbait.in/api/v1 API_TIMEOUT_SECONDS=30 DEV_BYPASS_AUTH=false diff --git a/README.md b/README.md index a6ffa8e..aa37da6 100644 --- a/README.md +++ b/README.md @@ -39,24 +39,59 @@ 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 ``` +### Environment config + +Each environment has its own entry point in `lib/config/` that sets `Environment.flavor`. +API URL and `.env` file are picked automatically (see `lib/core/config/environment.dart`). + +| Flavor | Entry point | API URL (auto) | Web URL | Web base-href | +|--------|-------------|----------------|---------|---------------| +| **dev** | `lib/config/main_dev.dart` | `https://demo.venbait.in/api/v1` | `https://bharatconsumerproducts.com/erp/login` | `/erp/` | +| **uat** | `lib/config/main_uat.dart` | `https://uat-api.bharaterp.com/api/v1` | — | `/` | +| **prod** | `lib/config/main_prod.dart` | `https://api.bharaterp.com/api/v1` | — | `/app/` | + +Dev web uses **path URLs** (no `index.html#`). Build with `--base-href /erp/` and deploy `web/.htaccess` for Apache SPA fallback. + +### Web builds + ```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 +# Dev +flutter build web -t lib/config/main_dev.dart --release --base-href /erp/ + +# UAT +flutter build web -t lib/config/main_uat.dart --release --base-href / + +# Prod +flutter build web -t lib/config/main_prod.dart --release --base-href /app/ ``` +### Android APK / AAB + +```bash +flutter build apk --flavor dev -t lib/config/main_dev.dart --release +flutter build apk --flavor uat -t lib/config/main_uat.dart --release +flutter build apk --flavor prod -t lib/config/main_prod.dart --release + +flutter build appbundle --flavor prod -t lib/config/main_prod.dart --release +``` + +### iOS + +```bash +flutter build ios -t lib/config/main_prod.dart --release +``` + +### Local dev run + +```bash +flutter run -d chrome -t lib/config/main_dev.dart --web-port=8080 +flutter run --flavor dev -t lib/config/main_dev.dart +``` + +Output (web): `build/web/` + ## Project Structure See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for full architecture documentation. diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a20f905..661debc 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -10,6 +10,26 @@ android { compileSdk = flutter.compileSdkVersion ndkVersion = flutter.ndkVersion + flavorDimensions += "environment" + productFlavors { + create("dev") { + dimension = "environment" + applicationIdSuffix = ".dev" + versionNameSuffix = "-dev" + resValue("string", "app_name", "Bharat ERP Dev") + } + create("uat") { + dimension = "environment" + applicationIdSuffix = ".uat" + versionNameSuffix = "-uat" + resValue("string", "app_name", "Bharat ERP UAT") + } + create("prod") { + dimension = "environment" + resValue("string", "app_name", "Bharat ERP") + } + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b3ba8a9..4bff20e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,6 +1,6 @@ + + Bharat ERP + diff --git a/lib/config/main_dev.dart b/lib/config/main_dev.dart new file mode 100644 index 0000000..d42c593 --- /dev/null +++ b/lib/config/main_dev.dart @@ -0,0 +1,7 @@ +import '../core/config/app_bootstrap.dart'; +import '../core/config/environment.dart'; + +Future main() async { + Environment.flavor = Flavor.dev; + await startApp(); +} diff --git a/lib/config/main_prod.dart b/lib/config/main_prod.dart new file mode 100644 index 0000000..46fd351 --- /dev/null +++ b/lib/config/main_prod.dart @@ -0,0 +1,7 @@ +import '../core/config/app_bootstrap.dart'; +import '../core/config/environment.dart'; + +Future main() async { + Environment.flavor = Flavor.prod; + await startApp(); +} diff --git a/lib/config/main_uat.dart b/lib/config/main_uat.dart new file mode 100644 index 0000000..6f4ba97 --- /dev/null +++ b/lib/config/main_uat.dart @@ -0,0 +1,7 @@ +import '../core/config/app_bootstrap.dart'; +import '../core/config/environment.dart'; + +Future main() async { + Environment.flavor = Flavor.uat; + await startApp(); +} diff --git a/lib/core/config/app_bootstrap.dart b/lib/core/config/app_bootstrap.dart new file mode 100644 index 0000000..2532960 --- /dev/null +++ b/lib/core/config/app_bootstrap.dart @@ -0,0 +1,28 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_web_plugins/url_strategy.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../app.dart'; +import '../theme/theme_provider.dart'; +import 'environment.dart'; + +Future startApp() async { + WidgetsFlutterBinding.ensureInitialized(); + if (kIsWeb) { + usePathUrlStrategy(); + } + await dotenv.load(fileName: Environment.envFileName); + final prefs = await SharedPreferences.getInstance(); + + runApp( + ProviderScope( + overrides: [ + sharedPreferencesProvider.overrideWithValue(prefs), + ], + child: const BharatErpApp(), + ), + ); +} diff --git a/lib/core/config/app_env.dart b/lib/core/config/app_env.dart index 6f7b893..a101a4a 100644 --- a/lib/core/config/app_env.dart +++ b/lib/core/config/app_env.dart @@ -1,26 +1,16 @@ -import 'package:flutter/foundation.dart'; +import 'environment.dart'; +/// Back-compat alias — prefer [Environment] for new code. class AppEnv { AppEnv._(); - static const String development = 'development'; - static const String uat = 'uat'; - static const String production = 'production'; + static String get development => Environment.name; + static String get uat => 'uat'; + static String get production => 'production'; - static String get current { - const env = String.fromEnvironment('APP_ENV', defaultValue: development); - return env; - } + static String get current => Environment.name; - 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'; - } - } + static String get apiBaseUrl => Environment.apiBaseUrl; + + static String get envFileName => Environment.envFileName; } diff --git a/lib/core/config/dev_config.dart b/lib/core/config/dev_config.dart index ae26ef3..e417886 100644 --- a/lib/core/config/dev_config.dart +++ b/lib/core/config/dev_config.dart @@ -5,14 +5,18 @@ class DevConfig { DevConfig._(); /// True when dev bypass is enabled via .env or running in debug mode. + /// Never enabled in release builds. static bool get bypassAuth { + if (kReleaseMode) return false; 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). + /// Never enabled in release builds. static bool get screenPreviewEnabled { + if (kReleaseMode) return false; if (bypassAuth) return true; final env = dotenv.env['DEV_SCREEN_PREVIEW']; if (env != null) return env.toLowerCase() == 'true'; diff --git a/lib/core/config/environment.dart b/lib/core/config/environment.dart new file mode 100644 index 0000000..4c42694 --- /dev/null +++ b/lib/core/config/environment.dart @@ -0,0 +1,54 @@ +import 'package:flutter_dotenv/flutter_dotenv.dart'; + +enum Flavor { dev, uat, prod } + +/// Environment config — set [flavor] in each `lib/config/main_*.dart` entry point. +class Environment { + Environment._(); + + /// Overwritten by each `main_*.dart` before [startApp] runs. + static Flavor flavor = Flavor.dev; + + static bool get isProd => flavor == Flavor.prod; + + static String get name => switch (flavor) { + Flavor.dev => 'development', + Flavor.uat => 'uat', + Flavor.prod => 'production', + }; + + /// Web deploy path (`--base-href` should match this value). + static String get baseHref => switch (flavor) { + Flavor.dev => '/erp/', + Flavor.uat => '/', + Flavor.prod => '/app/', + }; + + /// Public web app URL (path-based routing — no `index.html#`). + static String get webAppUrl => switch (flavor) { + Flavor.dev => 'https://bharatconsumerproducts.com/erp', + Flavor.uat => 'https://uat.bharaterp.com', + Flavor.prod => 'https://app.bharaterp.com', + }; + + static String get envFileName => switch (flavor) { + Flavor.dev => '.env.development', + Flavor.uat => '.env.uat', + Flavor.prod => '.env.production', + }; + + /// Priority: `--dart-define=API_BASE_URL` → `.env` file → flavor default. + static String get apiBaseUrl { + const fromDefine = String.fromEnvironment('API_BASE_URL'); + if (fromDefine.isNotEmpty) return fromDefine; + + final fromEnvFile = dotenv.env['API_BASE_URL']; + if (fromEnvFile != null && fromEnvFile.isNotEmpty) return fromEnvFile; + + return switch (flavor) { + Flavor.dev => 'https://demo.venbait.in/api/v1', + Flavor.uat => 'https://uat-api.bharaterp.com/api/v1', + Flavor.prod => 'https://api.bharaterp.com/api/v1', + }; + } +} diff --git a/lib/core/constants/api_endpoints.dart b/lib/core/constants/api_endpoints.dart index 420f00f..509ca90 100644 --- a/lib/core/constants/api_endpoints.dart +++ b/lib/core/constants/api_endpoints.dart @@ -70,6 +70,37 @@ class ApiEndpoints { static const String warehouses = '/masters/warehouses'; static String warehouseById(String id) => '/masters/warehouses/$id'; + // Vendors + static const String vendors = '/vendors'; + static String vendorById(String id) => '/vendors/$id'; + static String vendorStatus(String id) => '/vendors/$id/status'; + static String vendorAddresses(String vendorId) => '/vendors/$vendorId/addresses'; + static String vendorAddressById(String vendorId, String addressId) => + '/vendors/$vendorId/addresses/$addressId'; + static String vendorContacts(String vendorId) => '/vendors/$vendorId/contacts'; + static String vendorContactById(String vendorId, String contactId) => + '/vendors/$vendorId/contacts/$contactId'; + static String vendorBankDetails(String vendorId) => + '/vendors/$vendorId/bank-details'; + static String vendorBankDetailById(String vendorId, String bankDetailId) => + '/vendors/$vendorId/bank-details/$bankDetailId'; + + // Purchase Orders + static const String purchaseOrders = '/purchase-orders'; + static String purchaseOrderById(String id) => '/purchase-orders/$id'; + static String purchaseOrderSubmit(String id) => '/purchase-orders/$id/submit'; + static String purchaseOrderApprove(String id) => '/purchase-orders/$id/approve'; + static String purchaseOrderReject(String id) => '/purchase-orders/$id/reject'; + static String purchaseOrderAmend(String id) => '/purchase-orders/$id/amend'; + static String purchaseOrderCancel(String id) => '/purchase-orders/$id/cancel'; + static String purchaseOrderPdf(String id) => '/purchase-orders/$id/pdf'; + + // GRN + static const String grn = '/grn'; + static String grnById(String id) => '/grn/$id'; + static String grnCancel(String id) => '/grn/$id/cancel'; + static String grnPdf(String id) => '/grn/$id/pdf'; + // Assets static const String assets = '/assets'; static String assetById(String id) => '/assets/$id'; diff --git a/lib/core/constants/route_constants.dart b/lib/core/constants/route_constants.dart index 7e95fe6..ea6ede9 100644 --- a/lib/core/constants/route_constants.dart +++ b/lib/core/constants/route_constants.dart @@ -39,6 +39,24 @@ class RouteConstants { // Profile static const String profile = '/profile'; + // Vendors + static const String vendors = '/vendors'; + static const String vendorAdd = '/vendors/add'; + static const String vendorEdit = '/vendors/:id/edit'; + static const String vendorDetail = '/vendors/:id'; + + // Purchase Orders + static const String purchaseOrders = '/purchase-orders'; + static const String purchaseOrderAdd = '/purchase-orders/add'; + static const String purchaseOrderEdit = '/purchase-orders/:id/edit'; + static const String purchaseOrderDetail = '/purchase-orders/:id'; + + // GRN + static const String grn = '/grn'; + static const String grnAdd = '/grn/add'; + static const String grnEdit = '/grn/:id/edit'; + static const String grnDetail = '/grn/:id'; + // Assets static const String assets = '/assets'; static const String assetAdd = '/assets/add'; diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index bc84bbc..80ebbe5 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -3,13 +3,14 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../config/environment.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', + baseUrl: Environment.apiBaseUrl, connectTimeout: Duration( seconds: int.tryParse(dotenv.env['API_TIMEOUT_SECONDS'] ?? '30') ?? 30, ), diff --git a/lib/core/network/token_refresh_service.dart b/lib/core/network/token_refresh_service.dart index 80eb7e7..498bf97 100644 --- a/lib/core/network/token_refresh_service.dart +++ b/lib/core/network/token_refresh_service.dart @@ -1,7 +1,7 @@ import 'package:dio/dio.dart'; -import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../config/environment.dart'; import '../constants/api_endpoints.dart'; import '../errors/exceptions.dart'; import 'api_envelope.dart'; @@ -33,8 +33,7 @@ class TokenRefreshService { final dio = Dio( BaseOptions( - baseUrl: dotenv.env['API_BASE_URL'] ?? - 'https://demo.venbait.in/api/v1', + baseUrl: Environment.apiBaseUrl, headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', diff --git a/lib/core/utils/formatters.dart b/lib/core/utils/formatters.dart index 9c2a0c0..d054ad9 100644 --- a/lib/core/utils/formatters.dart +++ b/lib/core/utils/formatters.dart @@ -27,6 +27,21 @@ class DateFormatter { if (value == null || value.isEmpty) return null; return DateTime.tryParse(value); } + + static String formatUserLastLogin(DateTime? value) { + if (value == null) return '—'; + final local = value.toLocal(); + final now = DateTime.now(); + final today = DateTime(now.year, now.month, now.day); + final date = DateTime(local.year, local.month, local.day); + final dayDiff = today.difference(date).inDays; + final time = DateFormat('hh:mm a').format(local); + + if (dayDiff == 0) return 'Today $time'; + if (dayDiff == 1) return 'Yesterday'; + if (dayDiff < 7) return '$dayDiff days ago'; + return displayDateTime(local); + } } class CurrencyFormatter { diff --git a/lib/core/utils/permission_utils.dart b/lib/core/utils/permission_utils.dart index cb8ea5b..3d178ea 100644 --- a/lib/core/utils/permission_utils.dart +++ b/lib/core/utils/permission_utils.dart @@ -15,6 +15,8 @@ const Map permissionModuleAliases = { 'asset_maintenance': 'ASSET', 'asset_disposal': 'ASSET', 'vendor': 'VENDOR', + 'vendors': 'VENDOR', + 'purchase_orders': 'PURCHASE_ORDER', 'purchase_order': 'PURCHASE_ORDER', 'grn': 'GRN', }; diff --git a/lib/core/utils/validators.dart b/lib/core/utils/validators.dart index d3b3f7c..0306579 100644 --- a/lib/core/utils/validators.dart +++ b/lib/core/utils/validators.dart @@ -1,6 +1,17 @@ +import 'package:flutter/services.dart'; + class Validators { Validators._(); + static final RegExp _gstinPattern = + RegExp(r'^\d{2}[A-Z]{5}\d{4}[A-Z][A-Z\d]Z[A-Z\d]$'); + static final RegExp _panPattern = RegExp(r'^[A-Z]{5}\d{4}[A-Z]$'); + static final RegExp _emailPattern = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + ); + static final RegExp _ifscPattern = RegExp(r'^[A-Z]{4}0[A-Z0-9]{6}$'); + static final RegExp _pincodePattern = RegExp(r'^[1-9]\d{5}$'); + static String? required(String? value, {String fieldName = 'This field'}) { if (value == null || value.trim().isEmpty) { return '$fieldName is required'; @@ -10,15 +21,119 @@ class Validators { 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 _validateEmail(value.trim()); + } + + /// Validates email only when a value is entered. + static String? optionalEmail(String? value) { + if (value == null || value.trim().isEmpty) return null; + return _validateEmail(value.trim()); + } + + static String? _validateEmail(String raw) { + if (!_emailPattern.hasMatch(raw)) { + 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'; + /// Required 9–18 digit bank account number. + static String? accountNumber(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Account number is required'; + } + return _validateAccountNumber(value.trim()); + } + + /// Validates account number only when a value is entered. + static String? optionalAccountNumber(String? value) { + if (value == null || value.trim().isEmpty) return null; + return _validateAccountNumber(value.trim()); + } + + static String? _validateAccountNumber(String digits) { + if (!RegExp(r'^\d+$').hasMatch(digits)) { + return 'Account number must contain digits only'; + } + if (digits.length < 9 || digits.length > 18) { + return 'Account number must be 9 to 18 digits'; + } + return null; + } + + /// Required 11-character IFSC code (e.g. SBIN0001234). + static String? ifsc(String? value) { + if (value == null || value.trim().isEmpty) { + return 'IFSC is required'; + } + return _validateIfsc(value.trim()); + } + + /// Validates IFSC only when a value is entered. + static String? optionalIfsc(String? value) { + if (value == null || value.trim().isEmpty) return null; + return _validateIfsc(value.trim()); + } + + static String? _validateIfsc(String raw) { + final normalized = raw.toUpperCase(); + if (normalized.length != 11) { + return 'IFSC must be exactly 11 characters'; + } + if (!_ifscPattern.hasMatch(normalized)) { + return 'Enter a valid IFSC code (e.g. SBIN0001234)'; + } + return null; + } + + /// Required 6-digit Indian pincode. + static String? pincode(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Pincode is required'; + } + return _validatePincode(value.trim()); + } + + /// Validates pincode only when a value is entered. + static String? optionalPincode(String? value) { + if (value == null || value.trim().isEmpty) return null; + return _validatePincode(value.trim()); + } + + static String? _validatePincode(String digits) { + if (!RegExp(r'^\d+$').hasMatch(digits)) { + return 'Pincode must contain digits only'; + } + if (!_pincodePattern.hasMatch(digits)) { + return 'Enter a valid 6-digit pincode'; + } + return null; + } + + /// Required 10-digit mobile — digits only. + static String? mobile(String? value) { + if (value == null || value.trim().isEmpty) { + return 'Mobile number is required'; + } + return _validateMobileDigits(value.trim()); + } + + /// Alias for [mobile]. + static String? phone(String? value) => mobile(value); + + /// Validates mobile only when a value is entered. + static String? optionalMobile(String? value) { + if (value == null || value.trim().isEmpty) return null; + return _validateMobileDigits(value.trim()); + } + + static String? _validateMobileDigits(String digits) { + if (!RegExp(r'^\d+$').hasMatch(digits)) { + return 'Mobile number must contain digits only'; + } + if (digits.length != 10) { + return 'Mobile number must be exactly 10 digits'; + } return null; } @@ -31,10 +146,56 @@ class Validators { return null; } - static String? gstNumber(String? value) { + /// Required 15-character GSTIN pattern. + static String? gstin(String? value) { + if (value == null || value.trim().isEmpty) { + return 'GSTIN is required'; + } + return _validateGstin(value.trim()); + } + + /// Validates GSTIN only when a value is entered. + static String? optionalGstin(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 _validateGstin(value.trim()); + } + + /// Alias for [optionalGstin] (legacy company forms). + static String? gstNumber(String? value) => optionalGstin(value); + + static String? _validateGstin(String raw) { + final normalized = raw.toUpperCase(); + if (normalized.length != 15) { + return 'GSTIN must be exactly 15 characters'; + } + if (!_gstinPattern.hasMatch(normalized)) { + return 'Enter a valid GSTIN (e.g. 27ABCDE1234F1Z5)'; + } + return null; + } + + /// Required 10-character PAN pattern (e.g. ABCDE1234F). + static String? pan(String? value) { + if (value == null || value.trim().isEmpty) { + return 'PAN is required'; + } + return _validatePan(value.trim()); + } + + /// Validates PAN only when a value is entered. + static String? optionalPan(String? value) { + if (value == null || value.trim().isEmpty) return null; + return _validatePan(value.trim()); + } + + static String? _validatePan(String raw) { + final normalized = raw.toUpperCase(); + if (normalized.length != 10) { + return 'PAN must be exactly 10 characters'; + } + if (!_panPattern.hasMatch(normalized)) { + return 'Enter a valid PAN (e.g. ABCDE1234F)'; + } return null; } @@ -44,4 +205,114 @@ class Validators { } return null; } + + /// Resolves validators for master-data and dynamic form fields by key. + static String? forFieldKey( + String key, + String? value, { + required bool required, + String fieldName = 'Field', + }) { + final normalizedKey = key.trim().toLowerCase(); + + if (normalizedKey == 'phone' || normalizedKey == 'mobile') { + return required ? mobile(value) : optionalMobile(value); + } + if (normalizedKey == 'email') { + return required ? email(value) : optionalEmail(value); + } + if (normalizedKey == 'gstin' || + normalizedKey == 'gst_number' || + normalizedKey == 'gst') { + return required ? gstin(value) : optionalGstin(value); + } + if (normalizedKey == 'pan') { + return required ? pan(value) : optionalPan(value); + } + if (normalizedKey == 'pincode' || + normalizedKey == 'postal_code' || + normalizedKey == 'zip') { + return required ? pincode(value) : optionalPincode(value); + } + if (normalizedKey == 'ifsc') { + return required ? ifsc(value) : optionalIfsc(value); + } + if (normalizedKey == 'account_number' || normalizedKey == 'account_no') { + return required ? accountNumber(value) : optionalAccountNumber(value); + } + + if (required) { + return Validators.required(value, fieldName: fieldName); + } + return null; + } + + static List inputFormattersForFieldKey(String key) { + final normalizedKey = key.trim().toLowerCase(); + + if (normalizedKey == 'phone' || normalizedKey == 'mobile') { + return mobileInput; + } + if (normalizedKey == 'pincode' || + normalizedKey == 'postal_code' || + normalizedKey == 'zip') { + return pincodeInput; + } + if (normalizedKey == 'ifsc') { + return ifscInput; + } + if (normalizedKey == 'account_number' || normalizedKey == 'account_no') { + return accountNumberInput; + } + if (normalizedKey == 'gstin' || + normalizedKey == 'gst_number' || + normalizedKey == 'gst') { + return gstinInput; + } + if (normalizedKey == 'pan') { + return panInput; + } + return const []; + } + + static List get mobileInput => [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(10), + ]; + + static List get pincodeInput => [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(6), + ]; + + static List get accountNumberInput => [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(18), + ]; + + static List get ifscInput => [ + FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')), + LengthLimitingTextInputFormatter(11), + _upperCaseFormatter, + ]; + + static List get gstinInput => [ + FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')), + LengthLimitingTextInputFormatter(15), + _upperCaseFormatter, + ]; + + static List get panInput => [ + FilteringTextInputFormatter.allow(RegExp(r'[A-Za-z0-9]')), + LengthLimitingTextInputFormatter(10), + _upperCaseFormatter, + ]; + + static final TextInputFormatter _upperCaseFormatter = + TextInputFormatter.withFunction( + (oldValue, newValue) => TextEditingValue( + text: newValue.text.toUpperCase(), + selection: newValue.selection, + ), + ); } diff --git a/lib/main.dart b/lib/main.dart index 9a22c41..9a8ea3b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,23 +1,9 @@ -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'; +import 'core/config/app_bootstrap.dart'; +import 'core/config/environment.dart'; +/// Default entry point — development flavor. +/// For UAT/Prod use `lib/config/main_uat.dart` or `lib/config/main_prod.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(), - ), - ); + Environment.flavor = Flavor.dev; + await startApp(); } diff --git a/lib/modules/assets/presentation/screens/asset_list_screen.dart b/lib/modules/assets/presentation/screens/asset_list_screen.dart index b9b7863..282bee4 100644 --- a/lib/modules/assets/presentation/screens/asset_list_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_list_screen.dart @@ -11,14 +11,16 @@ import '../../../../shared/models/asset_model.dart'; import '../../../../shared/providers/permissions_provider.dart'; import '../../../../shared/widgets/app_card.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_searchable_dropdown.dart'; +import '../../../../shared/widgets/app_status_chip.dart'; +import '../../../../shared/widgets/app_table_action_icon.dart'; import '../../../../shared/widgets/can_permission.dart'; import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; -import '../../../rbac/presentation/widgets/rbac_widgets.dart'; import '../providers/asset_categories_provider.dart'; import '../providers/assets_provider.dart'; import '../widgets/asset_form_panel.dart'; @@ -31,8 +33,6 @@ class AssetListScreen extends ConsumerStatefulWidget { } class _AssetListScreenState extends ConsumerState { - static const _tableMinWidth = 1040.0; - String? _selectedCategory; String? _selectedPlant; String? _selectedStatus; @@ -182,119 +182,13 @@ class _AssetListScreenState extends ConsumerState { ) else Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final tableWidth = - constraints.maxWidth < _tableMinWidth - ? _tableMinWidth - : constraints.maxWidth; - - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: - const EdgeInsets.symmetric(horizontal: 4), - child: ConstrainedBox( - constraints: - BoxConstraints(minWidth: tableWidth), - child: DataTable( - horizontalMargin: 20, - columnSpacing: 16, - headingRowColor: WidgetStateProperty.all( - Theme.of(context) - .colorScheme - .surfaceContainerHighest - .withValues(alpha: 0.4), - ), - columns: const [ - DataColumn(label: Text('ASSET')), - DataColumn(label: Text('CATEGORY')), - DataColumn(label: Text('PLANT')), - DataColumn(label: Text('WARRANTY')), - DataColumn(label: Text('STATUS')), - DataColumn( - label: _AssetTableActionsHeader(), - ), - ], - rows: filteredAssets.map((asset) { - final status = - assetStatusDisplay(asset.status); - return DataRow( - cells: [ - DataCell( - Row( - children: [ - UserAvatarChip( - name: asset.assetName, - initials: - assetInitials(asset), - ), - const SizedBox(width: 10), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Text( - asset.assetName, - style: const TextStyle( - fontWeight: - FontWeight.w600, - ), - ), - Text( - asset.assetCode ?? '—', - style: Theme.of(context) - .textTheme - .bodySmall, - ), - ], - ), - ], - ), - ), - DataCell( - RoleBadge( - label: asset.assetCategoryName ?? - '—', - ), - ), - DataCell( - Text(asset.plantName ?? '—'), - ), - DataCell( - Text( - DateFormatter.displayDate( - asset.warrantyExpiryDate, - ), - ), - ), - DataCell( - StatusBadge( - label: status.label, - color: status.color, - ), - ), - DataCell( - _AssetTableActionsCell( - child: _AssetTableActions( - canView: true, - canEdit: canEdit, - canDelete: canDelete, - onView: () => _viewAsset(asset), - onEdit: () => _editAsset(asset), - onDelete: () => - _deleteAsset(asset), - ), - ), - ), - ], - ); - }).toList(), - ), - ), - ); - }, + child: _AssetDataTable( + assets: filteredAssets, + canEdit: canEdit, + canDelete: canDelete, + onView: _viewAsset, + onEdit: _editAsset, + onDelete: _deleteAsset, ), ), const Divider(height: 1), @@ -452,21 +346,6 @@ class _AssetListScreenState extends ConsumerState { } } -String assetInitials(AssetModel asset) { - final code = asset.assetCode; - if (code != null && code.isNotEmpty) { - return code.length >= 2 ? code.substring(0, 2).toUpperCase() : code.toUpperCase(); - } - if (asset.assetName.isNotEmpty) { - final parts = asset.assetName.trim().split(RegExp(r'\s+')); - if (parts.length >= 2) { - return '${parts[0][0]}${parts[1][0]}'.toUpperCase(); - } - return asset.assetName[0].toUpperCase(); - } - return 'A'; -} - ({String label, Color color}) assetStatusDisplay(String? status) { return switch (status?.toUpperCase().replaceAll(' ', '_')) { 'IN_USE' => (label: 'In Use', color: const Color(0xFF16A34A)), @@ -599,119 +478,90 @@ class _AssetFilterDropdown extends StatelessWidget { } } -class _AssetTableActions extends StatelessWidget { - const _AssetTableActions({ +class _AssetDataTable extends StatelessWidget { + const _AssetDataTable({ + required this.assets, + required this.canEdit, + required this.canDelete, required this.onView, required this.onEdit, required this.onDelete, - this.canView = true, - this.canEdit = true, - this.canDelete = true, }); - static const columnWidth = 108.0; - - final VoidCallback onView; - final VoidCallback onEdit; - final VoidCallback onDelete; - final bool canView; + final List assets; final bool canEdit; final bool canDelete; + final void Function(AssetModel asset) onView; + final void Function(AssetModel asset) onEdit; + final Future Function(AssetModel asset) onDelete; @override Widget build(BuildContext context) { - final muted = Theme.of(context).colorScheme.onSurfaceVariant; - - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (canView) - _AssetActionIcon( - tooltip: 'View asset', - icon: Icons.visibility_outlined, - color: muted, - onPressed: onView, + return AppDataTable( + wrapInCard: false, + columns: [ + AppDataColumn( + label: 'Asset Code', + flex: 1, + cellBuilder: (_, asset) => Text(asset.assetCode ?? '—'), + ), + AppDataColumn( + label: 'Asset Name', + flex: 2, + cellBuilder: (_, asset) => Text(asset.assetName), + ), + AppDataColumn( + label: 'Category', + flex: 2, + cellBuilder: (_, asset) => Text(asset.assetCategoryName ?? '—'), + ), + AppDataColumn( + label: 'Plant', + flex: 1, + cellBuilder: (_, asset) => Text(asset.plantName ?? '—'), + ), + AppDataColumn( + label: 'Warranty', + flex: 1, + cellBuilder: (_, asset) => Text( + DateFormatter.displayDate(asset.warrantyExpiryDate), ), - if (canEdit) - _AssetActionIcon( - tooltip: 'Edit asset', - icon: Icons.edit_outlined, - color: muted, - onPressed: onEdit, + ), + AppDataColumn( + label: 'Status', + flex: 1, + cellBuilder: (_, asset) => AppStatusChip( + status: asset.status?.toLowerCase().replaceAll(' ', '_') ?? 'active', ), - if (canDelete) - _AssetActionIcon( - tooltip: 'Delete asset', - icon: Icons.delete_outline, - color: muted, - onPressed: onDelete, + ), + AppDataColumn( + label: 'Actions', + flex: 1, + alignment: Alignment.centerRight, + cellBuilder: (_, asset) => AppTableActions( + children: [ + AppTableActionIcon( + tooltip: 'View', + icon: Icons.visibility_outlined, + onPressed: () => onView(asset), + ), + if (canEdit) + AppTableActionIcon( + tooltip: 'Edit', + icon: Icons.edit_outlined, + onPressed: () => onEdit(asset), + ), + if (canDelete) + AppTableActionIcon( + tooltip: 'Delete', + icon: Icons.delete_outline, + onPressed: () => onDelete(asset), + ), + ], ), + ), ], - ); - } -} - -class _AssetTableActionsHeader extends StatelessWidget { - const _AssetTableActionsHeader(); - - @override - Widget build(BuildContext context) { - return const SizedBox( - width: _AssetTableActions.columnWidth, - child: Align( - alignment: Alignment.centerRight, - child: Text('ACTIONS'), - ), - ); - } -} - -class _AssetTableActionsCell extends StatelessWidget { - const _AssetTableActionsCell({required this.child}); - - final Widget child; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: _AssetTableActions.columnWidth, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.only(right: 4), - child: child, - ), - ), - ); - } -} - -class _AssetActionIcon extends StatelessWidget { - const _AssetActionIcon({ - required this.tooltip, - required this.icon, - required this.color, - required this.onPressed, - }); - - final String tooltip; - final IconData icon; - final Color color; - final VoidCallback onPressed; - - @override - Widget build(BuildContext context) { - return Tooltip( - message: tooltip, - child: InkWell( - onTap: onPressed, - borderRadius: BorderRadius.circular(6), - child: Padding( - padding: const EdgeInsets.all(6), - child: Icon(icon, size: 18, color: color), - ), - ), + rows: assets, ); } } @@ -748,50 +598,54 @@ class _AssetMobileList extends StatelessWidget { separatorBuilder: (_, __) => const SizedBox(height: 8), itemBuilder: (context, index) { final asset = assets[index]; - final status = assetStatusDisplay(asset.status); return AppCard( - child: ListTile( - onTap: () => onView(asset), - leading: UserAvatarChip( - name: asset.assetName, - initials: assetInitials(asset), - ), - title: Text( - asset.assetName, - style: const TextStyle(fontWeight: FontWeight.w600), - ), - subtitle: Column( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(asset.assetCode ?? '—'), - const SizedBox(height: 4), Row( children: [ - Flexible( - child: RoleBadge( - label: asset.assetCategoryName ?? '—', + Expanded( + child: Text( + asset.assetName, + style: Theme.of(context).textTheme.titleMedium, ), ), - const SizedBox(width: 8), - StatusBadge(label: status.label, color: status.color), + AppStatusChip( + status: asset.status?.toLowerCase().replaceAll(' ', '_') ?? + 'active', + compact: true, + ), + ], + ), + const SizedBox(height: 4), + Text(asset.assetCode ?? '—'), + Text('${asset.assetCategoryName ?? '—'} · ${asset.plantName ?? '—'}'), + const SizedBox(height: 8), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + AppTableActionIcon( + tooltip: 'View', + icon: Icons.visibility_outlined, + onPressed: () => onView(asset), + ), + if (onEdit != null) + AppTableActionIcon( + tooltip: 'Edit', + icon: Icons.edit_outlined, + onPressed: () => onEdit!(asset), + ), + if (onDelete != null) + AppTableActionIcon( + tooltip: 'Delete', + icon: Icons.delete_outline, + onPressed: () => onDelete!(asset), + ), ], ), - ], - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (onEdit != null) - IconButton( - icon: const Icon(Icons.edit_outlined), - onPressed: () => onEdit!(asset), - ), - if (onDelete != null) - IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () => onDelete!(asset), - ), ], ), ), diff --git a/lib/modules/assets/presentation/widgets/asset_form_panel.dart b/lib/modules/assets/presentation/widgets/asset_form_panel.dart index fa519c9..f088060 100644 --- a/lib/modules/assets/presentation/widgets/asset_form_panel.dart +++ b/lib/modules/assets/presentation/widgets/asset_form_panel.dart @@ -8,6 +8,8 @@ import '../../../../core/utils/validators.dart'; import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/user_management_models.dart' show FilterOptionModel; import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; import '../../../../shared/widgets/app_loading_view.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_text_field.dart'; @@ -250,18 +252,18 @@ class _AssetFormPanelState extends ConsumerState { .map((c) => int.tryParse(c.id)) .whereType() .toList(); - return DropdownButtonFormField( + return AppSearchableDropdown( + label: 'Category *', value: _dropdownValue(_categoryId, categoryIds), - isExpanded: true, - decoration: const InputDecoration(labelText: 'Category *'), - items: categories + searchHint: 'Search category...', + options: categories .map( - (c) => DropdownMenuItem( - value: int.tryParse(c.id), - child: Text('${c.code} — ${c.name}'), + (c) => AppDropdownOption( + value: int.tryParse(c.id) ?? 0, + label: '${c.code} — ${c.name}', ), ) - .where((item) => item.value != null) + .where((option) => option.value != 0) .toList(), onChanged: (v) => setState(() => _categoryId = v), validator: (v) => v == null ? 'Category is required' : null, @@ -273,18 +275,18 @@ class _AssetFormPanelState extends ConsumerState { .map((p) => int.tryParse(p.id)) .whereType() .toList(); - return DropdownButtonFormField( + return AppSearchableDropdown( + label: 'Plant *', value: _dropdownValue(_plantId, plantIds), - isExpanded: true, - decoration: const InputDecoration(labelText: 'Plant *'), - items: plants + searchHint: 'Search plant...', + options: plants .map( - (p) => DropdownMenuItem( - value: int.tryParse(p.id), - child: Text(p.name), + (p) => AppDropdownOption( + value: int.tryParse(p.id) ?? 0, + label: p.name, ), ) - .where((item) => item.value != null) + .where((option) => option.value != 0) .toList(), onChanged: (v) => setState(() => _plantId = v), validator: (v) => v == null ? 'Plant is required' : null, diff --git a/lib/modules/assets/presentation/widgets/asset_side_panels.dart b/lib/modules/assets/presentation/widgets/asset_side_panels.dart index fa100eb..c010675 100644 --- a/lib/modules/assets/presentation/widgets/asset_side_panels.dart +++ b/lib/modules/assets/presentation/widgets/asset_side_panels.dart @@ -4,6 +4,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/utils/formatters.dart'; import '../../../../core/utils/validators.dart'; import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_text_field.dart'; import '../providers/assets_provider.dart'; @@ -119,18 +121,16 @@ class _AddAmcPanelState extends ConsumerState { label: 'Contract No', ), ), - DropdownButtonFormField( + AppSearchableDropdown( + label: 'Contract Type', value: _contractType, - isExpanded: true, - decoration: const InputDecoration(labelText: 'Contract Type'), - items: const [ + searchHint: 'Search contract type...', + options: stringDropdownOptions(const [ 'COMPREHENSIVE', 'LABOUR_ONLY', 'PARTS_ONLY', 'PREVENTIVE_ONLY', - ] - .map((t) => DropdownMenuItem(value: t, child: Text(t))) - .toList(), + ]), onChanged: (v) { if (v != null) setState(() => _contractType = v); }, @@ -238,37 +238,33 @@ class _LogServiceVisitPanelState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ SidePanelFormRow( - left: DropdownButtonFormField( + left: AppSearchableDropdown( + label: 'Visit Type *', value: _visitType, - isExpanded: true, - decoration: const InputDecoration(labelText: 'Visit Type *'), - items: const [ + searchHint: 'Search visit type...', + options: stringDropdownOptions(const [ 'PREVENTIVE', 'BREAKDOWN', 'INSPECTION', 'INSTALLATION', 'CALIBRATION', 'OTHER', - ] - .map((t) => DropdownMenuItem(value: t, child: Text(t))) - .toList(), + ]), onChanged: (v) { if (v != null) setState(() => _visitType = v); }, ), - right: DropdownButtonFormField( + right: AppSearchableDropdown( + label: 'Status', value: _status, - isExpanded: true, - decoration: const InputDecoration(labelText: 'Status'), - items: const [ + searchHint: 'Search status...', + options: stringDropdownOptions(const [ 'SCHEDULED', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED', 'PENDING_PARTS', - ] - .map((t) => DropdownMenuItem(value: t, child: Text(t))) - .toList(), + ]), onChanged: (v) { if (v != null) setState(() => _status = v); }, @@ -396,20 +392,18 @@ class _AddInsurancePanelState extends ConsumerState { ), ), SidePanelFormRow( - left: DropdownButtonFormField( + left: AppSearchableDropdown( + label: 'Policy Type', value: _policyType, - isExpanded: true, - decoration: const InputDecoration(labelText: 'Policy Type'), - items: const [ + searchHint: 'Search policy type...', + options: stringDropdownOptions(const [ 'FIRE_AND_ALLIED', 'MACHINERY_BREAKDOWN', 'COMPREHENSIVE', 'THIRD_PARTY', 'VEHICLE', 'OTHER', - ] - .map((t) => DropdownMenuItem(value: t, child: Text(t))) - .toList(), + ]), onChanged: (v) { if (v != null) setState(() => _policyType = v); }, diff --git a/lib/modules/company/presentation/screens/company_form_screen.dart b/lib/modules/company/presentation/screens/company_form_screen.dart index 6b7c9de..b522f88 100644 --- a/lib/modules/company/presentation/screens/company_form_screen.dart +++ b/lib/modules/company/presentation/screens/company_form_screen.dart @@ -63,7 +63,8 @@ class _CompanyFormScreenState extends State { AppTextField( controller: _gstController, label: 'GST Number', - validator: Validators.gstNumber, + validator: Validators.optionalGstin, + inputFormatters: Validators.gstinInput, ), const SizedBox(height: 16), AppTextField( @@ -83,7 +84,8 @@ class _CompanyFormScreenState extends State { controller: _phoneController, label: 'Phone', keyboardType: TextInputType.phone, - validator: Validators.phone, + validator: Validators.mobile, + inputFormatters: Validators.mobileInput, ), const SizedBox(height: 24), AppButton( diff --git a/lib/modules/grn/data/datasources/grn_remote_data_source.dart b/lib/modules/grn/data/datasources/grn_remote_data_source.dart new file mode 100644 index 0000000..a7c6f5b --- /dev/null +++ b/lib/modules/grn/data/datasources/grn_remote_data_source.dart @@ -0,0 +1,121 @@ +import 'package:dio/dio.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/grn_model.dart'; + +class GrnRemoteDataSource { + GrnRemoteDataSource({required this.dio}); + + final Dio dio; + + Future> getGrns(GrnListQuery query) async { + final response = await dio.get( + ApiEndpoints.grn, + queryParameters: _queryToMap(query), + ); + return _parsePaginated(response.data, GrnModel.fromJson); + } + + Future getGrnById(String id) async { + final response = await dio.get(ApiEndpoints.grnById(id)); + return GrnModel.fromJson(response.data['data'] as Map); + } + + Future createGrn(Map data) async { + final response = await dio.post(ApiEndpoints.grn, data: data); + return GrnModel.fromJson(response.data['data'] as Map); + } + + Future updateGrn(String id, Map data) async { + final response = await dio.put(ApiEndpoints.grnById(id), data: data); + return GrnModel.fromJson(response.data['data'] as Map); + } + + Future cancelGrn(String id, {required String cancellationReason}) async { + final response = await dio.post( + ApiEndpoints.grnCancel(id), + data: {'cancellation_reason': cancellationReason}, + ); + return GrnModel.fromJson(response.data['data'] as Map); + } + + Future> downloadGrnPdf(String id) async { + final response = await dio.get>( + ApiEndpoints.grnPdf(id), + options: Options(responseType: ResponseType.bytes), + ); + return response.data ?? []; + } + + Map _queryToMap(GrnListQuery 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.poId != null) 'po_id': query.poId, + if (query.vendorId != null) 'vendor_id': query.vendorId, + if (query.warehouseId != null) 'warehouse_id': query.warehouseId, + if (query.dateFrom != null) 'date_from': query.dateFrom, + if (query.dateTo != null) 'date_to': query.dateTo, + }; + } + + PaginatedResponse _parsePaginated( + dynamic body, + T Function(Map) fromJson, + ) { + if (body is! Map) { + return const PaginatedResponse( + items: [], + page: 1, + limit: 20, + total: 0, + totalPages: 1, + ); + } + + final raw = body['data']; + final meta = body['meta'] as Map? ?? {}; + + if (raw is List) { + final items = raw.whereType>().map(fromJson).toList(); + final limit = (meta['limit'] as num?)?.toInt() ?? items.length; + final total = (meta['total'] as num?)?.toInt() ?? items.length; + return PaginatedResponse( + items: items, + page: (meta['page'] as num?)?.toInt() ?? 1, + limit: limit, + total: total, + totalPages: + limit > 0 ? ((total + limit - 1) / limit).ceil().clamp(1, 999999) : 1, + ); + } + + if (raw is Map) { + final list = raw['items']; + if (list is List) { + final items = list.whereType>().map(fromJson).toList(); + final limit = (meta['limit'] as num?)?.toInt() ?? 20; + final total = (meta['total'] as num?)?.toInt() ?? items.length; + return PaginatedResponse( + items: items, + page: (meta['page'] as num?)?.toInt() ?? 1, + limit: limit, + total: total, + totalPages: + limit > 0 ? ((total + limit - 1) / limit).ceil().clamp(1, 999999) : 1, + ); + } + } + + return const PaginatedResponse( + items: [], + page: 1, + limit: 20, + total: 0, + totalPages: 1, + ); + } +} diff --git a/lib/modules/grn/data/repositories/grn_repository_impl.dart b/lib/modules/grn/data/repositories/grn_repository_impl.dart new file mode 100644 index 0000000..eeb0d60 --- /dev/null +++ b/lib/modules/grn/data/repositories/grn_repository_impl.dart @@ -0,0 +1,57 @@ +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/grn_model.dart'; +import '../../domain/repositories/grn_repository.dart'; +import '../datasources/grn_remote_data_source.dart'; + +final grnRemoteDataSourceProvider = Provider((ref) { + return GrnRemoteDataSource(dio: ref.watch(dioProvider)); +}); + +final grnRepositoryProvider = Provider((ref) { + return GrnRepositoryImpl(dataSource: ref.watch(grnRemoteDataSourceProvider)); +}); + +class GrnRepositoryImpl implements GrnRepository { + GrnRepositoryImpl({required this.dataSource}); + + final GrnRemoteDataSource dataSource; + + @override + Future>> getGrns(GrnListQuery query) { + return safeApiCall(() => dataSource.getGrns(query)); + } + + @override + Future> getGrnById(String id) { + return safeApiCall(() => dataSource.getGrnById(id)); + } + + @override + Future> createGrn(Map data) { + return safeApiCall(() => dataSource.createGrn(data)); + } + + @override + Future> updateGrn(String id, Map data) { + return safeApiCall(() => dataSource.updateGrn(id, data)); + } + + @override + Future> cancelGrn( + String id, { + required String cancellationReason, + }) { + return safeApiCall( + () => dataSource.cancelGrn(id, cancellationReason: cancellationReason), + ); + } + + @override + Future>> downloadGrnPdf(String id) { + return safeApiCall(() => dataSource.downloadGrnPdf(id)); + } +} diff --git a/lib/modules/grn/domain/repositories/grn_repository.dart b/lib/modules/grn/domain/repositories/grn_repository.dart new file mode 100644 index 0000000..9f61ce6 --- /dev/null +++ b/lib/modules/grn/domain/repositories/grn_repository.dart @@ -0,0 +1,12 @@ +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/grn_model.dart'; + +abstract class GrnRepository { + Future>> getGrns(GrnListQuery query); + Future> getGrnById(String id); + Future> createGrn(Map data); + Future> updateGrn(String id, Map data); + Future> cancelGrn(String id, {required String cancellationReason}); + Future>> downloadGrnPdf(String id); +} diff --git a/lib/modules/grn/presentation/providers/grn_lookups_provider.dart b/lib/modules/grn/presentation/providers/grn_lookups_provider.dart new file mode 100644 index 0000000..cc775cc --- /dev/null +++ b/lib/modules/grn/presentation/providers/grn_lookups_provider.dart @@ -0,0 +1,65 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/constants/app_constants.dart'; +import '../../../../shared/models/purchase_order_model.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../masters/data/datasources/master_remote_data_source.dart'; +import '../../../purchase_orders/data/repositories/purchase_order_repository_impl.dart'; + +class GrnLookups { + const GrnLookups({ + this.warehouses = const [], + this.receivablePurchaseOrders = const [], + this.assetCategories = const [], + }); + + final List warehouses; + final List receivablePurchaseOrders; + final List assetCategories; +} + +final grnLookupsProvider = FutureProvider.autoDispose((ref) async { + final master = ref.watch(masterRemoteDataSourceProvider); + final poRepo = ref.watch(purchaseOrderRepositoryProvider); + + final warehouses = await _safeOptions(master.listWarehouses); + final assetCategories = await _safeOptions(master.listAssetCategories); + + final receivablePos = []; + for (final status in ['APPROVED', 'PARTIALLY_RECEIVED']) { + final result = await poRepo.getPurchaseOrders( + PurchaseOrderListQuery( + page: 1, + limit: AppConstants.maxPageSize, + status: status, + ), + ); + if (result.failure == null && result.data != null) { + receivablePos.addAll(result.data!.items); + } + } + + return GrnLookups( + warehouses: warehouses, + receivablePurchaseOrders: receivablePos, + assetCategories: assetCategories, + ); +}); + +Future> _safeOptions( + Future> Function() load, +) async { + try { + return await load(); + } catch (_) { + return const []; + } +} + +final grnPurchaseOrderProvider = + FutureProvider.autoDispose.family((ref, poId) async { + final result = + await ref.read(purchaseOrderRepositoryProvider).getPurchaseOrderById(poId); + if (result.failure != null) throw result.failure!; + return result.data; +}); diff --git a/lib/modules/grn/presentation/providers/grn_provider.dart b/lib/modules/grn/presentation/providers/grn_provider.dart new file mode 100644 index 0000000..c762028 --- /dev/null +++ b/lib/modules/grn/presentation/providers/grn_provider.dart @@ -0,0 +1,188 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/models/grn_model.dart'; +import '../../data/repositories/grn_repository_impl.dart'; + +class GrnListState { + const GrnListState({ + this.grns = const [], + this.query = const GrnListQuery(limit: 20), + this.total = 0, + this.totalPages = 1, + this.isRefreshing = false, + this.actionError, + this.actionSuccess, + }); + + final List grns; + final GrnListQuery query; + final int total; + final int totalPages; + final bool isRefreshing; + final String? actionError; + final String? actionSuccess; + + GrnListState copyWith({ + List? grns, + GrnListQuery? query, + int? total, + int? totalPages, + bool? isRefreshing, + String? actionError, + String? actionSuccess, + bool clearMessages = false, + }) { + return GrnListState( + grns: grns ?? this.grns, + 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 grnListProvider = + AsyncNotifierProvider.autoDispose( + GrnListNotifier.new, +); + +class GrnListNotifier extends AutoDisposeAsyncNotifier { + @override + Future build() async { + return _load(const GrnListQuery(limit: 20)); + } + + Future _load(GrnListQuery query) async { + final repository = ref.read(grnRepositoryProvider); + final result = await repository.getGrns(query); + if (result.failure != null) throw result.failure!; + final page = result.data!; + return GrnListState( + grns: page.items, + query: query, + total: page.total, + totalPages: page.totalPages, + ); + } + + Future refresh() async { + final current = state.valueOrNull ?? const GrnListState(); + state = AsyncData(current.copyWith(isRefreshing: true, clearMessages: true)); + try { + state = AsyncData(await _load(current.query)); + } catch (e, st) { + state = AsyncError(e, st); + } + } + + Future applyQuery(GrnListQuery query) async { + final previous = state.valueOrNull; + if (previous == null) { + state = const AsyncLoading(); + } + try { + state = AsyncData(await _load(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 setStatusFilter(String? status) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(status: status, 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)); + } +} + +final grnDetailProvider = + AsyncNotifierProvider.family( + GrnDetailNotifier.new, +); + +class GrnDetailNotifier extends FamilyAsyncNotifier { + @override + Future build(String arg) async { + final repository = ref.read(grnRepositoryProvider); + final result = await repository.getGrnById(arg); + if (result.failure != null) throw result.failure!; + return result.data!; + } + + Future reload() async { + state = const AsyncLoading(); + state = AsyncData(await build(arg)); + } + + Future cancel({required String cancellationReason}) async { + final repository = ref.read(grnRepositoryProvider); + final result = await repository.cancelGrn( + arg, + cancellationReason: cancellationReason, + ); + if (result.failure != null) throw result.failure!; + state = AsyncData(result.data!); + ref.invalidate(grnListProvider); + return result.data!; + } + + Future> downloadPdf() async { + final repository = ref.read(grnRepositoryProvider); + final result = await repository.downloadGrnPdf(arg); + if (result.failure != null) throw result.failure!; + return result.data ?? []; + } +} + +final grnFormProvider = + AsyncNotifierProvider.family( + GrnFormNotifier.new, +); + +class GrnFormNotifier extends FamilyAsyncNotifier { + @override + Future build(String? arg) async { + if (arg == null) return null; + final repository = ref.read(grnRepositoryProvider); + final result = await repository.getGrnById(arg); + if (result.failure != null) throw result.failure!; + return result.data; + } + + Future submitCreate(Map data) async { + final repository = ref.read(grnRepositoryProvider); + final result = await repository.createGrn(data); + if (result.failure != null) throw result.failure!; + ref.invalidate(grnListProvider); + return result.data!; + } + + Future submitUpdate(String id, Map data) async { + final repository = ref.read(grnRepositoryProvider); + final result = await repository.updateGrn(id, data); + if (result.failure != null) throw result.failure!; + ref.invalidate(grnListProvider); + ref.invalidate(grnDetailProvider(id)); + return result.data!; + } +} diff --git a/lib/modules/grn/presentation/screens/grn_detail_screen.dart b/lib/modules/grn/presentation/screens/grn_detail_screen.dart new file mode 100644 index 0000000..f8d4c15 --- /dev/null +++ b/lib/modules/grn/presentation/screens/grn_detail_screen.dart @@ -0,0 +1,276 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/formatters.dart'; +import '../../../../shared/models/grn_model.dart'; +import '../../../../shared/providers/permissions_provider.dart'; +import '../../../../shared/utils/file_download_helper.dart'; +import '../../../../shared/widgets/app_card.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/grn_provider.dart'; +import '../widgets/grn_line_items_editor.dart'; +import '../widgets/grn_status_chip.dart'; + +class GrnDetailScreen extends ConsumerStatefulWidget { + const GrnDetailScreen({super.key, required this.grnId}); + + final String grnId; + + @override + ConsumerState createState() => _GrnDetailScreenState(); +} + +class _GrnDetailScreenState extends ConsumerState { + bool _isWorking = false; + + @override + Widget build(BuildContext context) { + final detailAsync = ref.watch(grnDetailProvider(widget.grnId)); + final canEdit = ref.can('grn', PermissionAction.update); + final canExport = ref.can('grn', PermissionAction.export); + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go(RouteConstants.grn), + ), + title: detailAsync.maybeWhen( + data: (grn) => Text(grn.grnNumber ?? 'GRN #${grn.id}'), + orElse: () => const Text('GRN'), + ), + ), + body: detailAsync.when( + loading: () => const AppLoadingView(message: 'Loading GRN...'), + error: (e, _) => ErrorView.fromFailure( + e is Failure ? e : Failure.unknown(message: e.toString()), + onRetry: () => ref.invalidate(grnDetailProvider(widget.grnId)), + ), + data: (grn) => SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: grn.grnNumber ?? 'GRN #${grn.id}', + subtitle: + 'PO ${grn.poNumber ?? '—'} · ${grn.vendorName ?? '—'}', + actions: [ + if (canExport) + OutlinedButton.icon( + onPressed: _isWorking ? null : () => _downloadPdf(grn), + icon: const Icon(Icons.picture_as_pdf_outlined), + label: const Text('PDF'), + ), + if (canEdit && grn.canEdit) ...[ + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: _isWorking + ? null + : () => context.push( + '${RouteConstants.grn}/${grn.id}/edit', + ), + icon: const Icon(Icons.edit_outlined), + label: const Text('Edit'), + ), + ], + if (canEdit && grn.canCancel) ...[ + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: _isWorking ? null : () => _cancel(grn), + icon: const Icon(Icons.block_outlined), + label: const Text('Cancel'), + ), + ], + ], + ), + const SizedBox(height: 16), + GrnStatusChip(status: grn.status), + if (grn.cancellationReason != null && + grn.cancellationReason!.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + 'Cancellation reason: ${grn.cancellationReason}', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.error, + ), + ), + ], + const SizedBox(height: 16), + _OverviewCard(grn: grn), + const SizedBox(height: 16), + AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Line Items', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + GrnItemsTable(items: grn.items), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Future _runWorkflow(Future Function() action, String success) async { + setState(() => _isWorking = true); + try { + await action(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(success))); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } finally { + if (mounted) setState(() => _isWorking = false); + } + } + + Future _cancel(GrnModel grn) async { + final reasonController = TextEditingController(); + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Cancel GRN'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Cancel ${grn.grnNumber ?? grn.id}? This will reverse PO receipts.'), + const SizedBox(height: 16), + AppTextField( + label: 'Cancellation reason *', + controller: reasonController, + maxLines: 3, + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Close'), + ), + FilledButton( + onPressed: () { + if (reasonController.text.trim().isEmpty) return; + Navigator.pop(context, true); + }, + child: const Text('Cancel GRN'), + ), + ], + ), + ); + if (confirmed != true || !mounted) return; + + await _runWorkflow( + () => ref.read(grnDetailProvider(widget.grnId).notifier).cancel( + cancellationReason: reasonController.text.trim(), + ), + 'GRN cancelled', + ); + reasonController.dispose(); + } + + Future _downloadPdf(GrnModel grn) async { + await _runWorkflow(() async { + final bytes = + await ref.read(grnDetailProvider(widget.grnId).notifier).downloadPdf(); + await downloadFile( + bytes: bytes, + fileName: '${grn.grnNumber ?? 'GRN-${grn.id}'}.pdf', + ); + }, 'PDF downloaded'); + } +} + +class _OverviewCard extends StatelessWidget { + const _OverviewCard({required this.grn}); + + final GrnModel grn; + + @override + Widget build(BuildContext context) { + return AppCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Overview', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + _DetailRow(label: 'GRN Date', value: DateFormatter.displayDate(grn.grnDate)), + _DetailRow(label: 'PO Number', value: grn.poNumber ?? '—'), + _DetailRow(label: 'Vendor', value: grn.vendorName ?? '—'), + _DetailRow(label: 'Warehouse', value: grn.warehouseName ?? '—'), + _DetailRow(label: 'Vendor Invoice No', value: grn.vendorInvoiceNo ?? '—'), + _DetailRow( + label: 'Vendor Invoice Date', + value: DateFormatter.displayDate(grn.vendorInvoiceDate), + ), + _DetailRow( + label: 'Vendor Invoice Amount', + value: grn.vendorInvoiceAmount != null + ? CurrencyFormatter.format(grn.vendorInvoiceAmount!) + : '—', + ), + _DetailRow(label: 'Vehicle No', value: grn.vehicleNo ?? '—'), + _DetailRow(label: 'LR No', value: grn.lrNo ?? '—'), + _DetailRow(label: 'LR Date', value: DateFormatter.displayDate(grn.lrDate)), + _DetailRow(label: 'Remarks', value: grn.remarks ?? '—'), + ], + ), + ); + } +} + +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.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 180, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded(child: Text(value)), + ], + ), + ); + } +} diff --git a/lib/modules/grn/presentation/screens/grn_form_screen.dart b/lib/modules/grn/presentation/screens/grn_form_screen.dart new file mode 100644 index 0000000..c9dae5e --- /dev/null +++ b/lib/modules/grn/presentation/screens/grn_form_screen.dart @@ -0,0 +1,562 @@ +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/network/api_handler.dart'; +import '../../../../core/utils/formatters.dart'; +import '../../../../shared/models/grn_model.dart'; +import '../../../../shared/models/purchase_order_model.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/utils/navigation_utils.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 '../../../../shared/widgets/app_side_panel.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/grn_lookups_provider.dart'; +import '../providers/grn_provider.dart'; +import '../widgets/grn_line_items_editor.dart'; + +class GrnFormScreen extends ConsumerStatefulWidget { + const GrnFormScreen({super.key, this.grnId}); + + final String? grnId; + + bool get isEditing => grnId != null; + + @override + ConsumerState createState() => _GrnFormScreenState(); +} + +class _GrnFormScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _scrollController = ScrollController(); + final _vendorInvoiceNoController = TextEditingController(); + final _vendorInvoiceAmountController = TextEditingController(); + final _vehicleNoController = TextEditingController(); + final _lrNoController = TextEditingController(); + final _remarksController = TextEditingController(); + + DateTime? _grnDate; + DateTime? _vendorInvoiceDate; + DateTime? _lrDate; + String? _selectedPoId; + int? _warehouseId; + final List _lines = []; + bool _isSubmitting = false; + String? _populatedSignature; + + @override + void initState() { + super.initState(); + if (!widget.isEditing) { + _grnDate = DateTime.now(); + } + } + + @override + void dispose() { + _scrollController.dispose(); + _vendorInvoiceNoController.dispose(); + _vendorInvoiceAmountController.dispose(); + _vehicleNoController.dispose(); + _lrNoController.dispose(); + _remarksController.dispose(); + for (final line in _lines) { + line.dispose(); + } + super.dispose(); + } + + String _grnSignature(GrnModel grn) => + '${grn.id}:${grn.updatedAt?.toIso8601String()}'; + + void _populateFromGrn(GrnModel grn) { + setState(() { + _grnDate = grn.grnDate ?? DateTime.now(); + _selectedPoId = grn.poId?.toString(); + _warehouseId = grn.warehouseId; + _vendorInvoiceNoController.text = grn.vendorInvoiceNo ?? ''; + _vendorInvoiceDate = grn.vendorInvoiceDate; + _vendorInvoiceAmountController.text = + grn.vendorInvoiceAmount?.toString() ?? ''; + _vehicleNoController.text = grn.vehicleNo ?? ''; + _lrNoController.text = grn.lrNo ?? ''; + _lrDate = grn.lrDate; + _remarksController.text = grn.remarks ?? ''; + }); + } + + void _loadLinesFromPo(PurchaseOrderModel po) { + for (final line in _lines) { + line.dispose(); + } + setState(() { + _lines + ..clear() + ..addAll(draftsFromPurchaseOrder(po)); + if (_warehouseId == null && po.warehouseId != null) { + _warehouseId = po.warehouseId; + } + }); + } + + int? _parseId(String value) => int.tryParse(value.trim()); + + int? _dropdownValue(int? selected, Iterable validIds) { + if (selected == null) return null; + return validIds.contains(selected) ? selected : null; + } + + List> _intOptions(List options) { + return options + .map((e) { + final id = _parseId(e.id); + if (id == null) return null; + return AppDropdownOption(value: id, label: e.name); + }) + .whereType>() + .toList(); + } + + List> _assetCategoryOptions( + List categories, + ) { + return categories + .map((e) => AppDropdownOption(value: e.id, label: e.name)) + .toList(); + } + + Map _buildCreatePayload() { + final poId = int.tryParse(_selectedPoId ?? ''); + return { + 'grn_date': DateFormatter.toApiDate(_grnDate ?? DateTime.now()), + 'po_id': poId, + 'warehouse_id': _warehouseId, + if (_vendorInvoiceNoController.text.trim().isNotEmpty) + 'vendor_invoice_no': _vendorInvoiceNoController.text.trim(), + if (_vendorInvoiceDate != null) + 'vendor_invoice_date': DateFormatter.toApiDate(_vendorInvoiceDate!), + if (_vendorInvoiceAmountController.text.trim().isNotEmpty) + 'vendor_invoice_amount': + double.tryParse(_vendorInvoiceAmountController.text.trim()), + if (_vehicleNoController.text.trim().isNotEmpty) + 'vehicle_no': _vehicleNoController.text.trim(), + if (_lrNoController.text.trim().isNotEmpty) 'lr_no': _lrNoController.text.trim(), + if (_lrDate != null) 'lr_date': DateFormatter.toApiDate(_lrDate!), + if (_remarksController.text.trim().isNotEmpty) + 'remarks': _remarksController.text.trim(), + 'items': _lines.map((line) => line.toPayload()).toList(), + }; + } + + Map _buildUpdatePayload() { + return { + if (_vendorInvoiceNoController.text.trim().isNotEmpty) + 'vendor_invoice_no': _vendorInvoiceNoController.text.trim(), + if (_vendorInvoiceDate != null) + 'vendor_invoice_date': DateFormatter.toApiDate(_vendorInvoiceDate!), + if (_vendorInvoiceAmountController.text.trim().isNotEmpty) + 'vendor_invoice_amount': + double.tryParse(_vendorInvoiceAmountController.text.trim()), + if (_vehicleNoController.text.trim().isNotEmpty) + 'vehicle_no': _vehicleNoController.text.trim(), + if (_lrNoController.text.trim().isNotEmpty) 'lr_no': _lrNoController.text.trim(), + if (_lrDate != null) 'lr_date': DateFormatter.toApiDate(_lrDate!), + if (_remarksController.text.trim().isNotEmpty) + 'remarks': _remarksController.text.trim(), + }; + } + + String? _lineItemsError() { + if (_lines.isEmpty) return 'Add at least one line item with quantity'; + for (final line in _lines) { + if (line.currentQty <= 0) { + return 'Enter quantity for line ${line.lineNo}'; + } + if (line.acceptedQty < 0) { + return 'Accepted quantity must be zero or more for line ${line.lineNo}'; + } + } + return null; + } + + Future _submit() async { + final formState = _formKey.currentState; + if (formState == null) return; + + if (!formState.validate()) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please fix the highlighted errors before saving'), + ), + ); + return; + } + + if (!widget.isEditing) { + if (_selectedPoId == null || _warehouseId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please select PO and warehouse')), + ); + return; + } + final lineError = _lineItemsError(); + if (lineError != null) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(lineError))); + return; + } + } + + setState(() => _isSubmitting = true); + try { + final GrnModel saved; + if (widget.isEditing) { + saved = await ref + .read(grnFormProvider(widget.grnId).notifier) + .submitUpdate(widget.grnId!, _buildUpdatePayload()); + } else { + saved = await ref + .read(grnFormProvider(null).notifier) + .submitCreate(_buildCreatePayload()); + } + + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(widget.isEditing ? 'GRN updated' : 'GRN created'), + ), + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final destination = widget.isEditing + ? '${RouteConstants.grn}/${saved.id}' + : RouteConstants.grn; + goAndDismissOverlays(context, destination); + }); + } catch (e) { + if (!mounted) return; + final message = + e is Failure ? validationErrorMessage(e) : e.toString(); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message))); + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + Future _pickDate({ + required DateTime? current, + required ValueChanged onPicked, + }) async { + final picked = await showDatePicker( + context: context, + initialDate: current ?? DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime(2100), + ); + if (picked != null) onPicked(picked); + } + + @override + Widget build(BuildContext context) { + final lookupsAsync = ref.watch(grnLookupsProvider); + final existingAsync = widget.isEditing + ? ref.watch(grnFormProvider(widget.grnId)) + : const AsyncData(null); + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go( + widget.isEditing + ? '${RouteConstants.grn}/${widget.grnId}' + : RouteConstants.grn, + ), + ), + title: Text(widget.isEditing ? 'Edit GRN' : 'Create GRN'), + ), + body: lookupsAsync.when( + loading: () => const AppLoadingView(message: 'Loading form...'), + error: (e, _) => ErrorView.fromFailure( + e is Failure ? e : Failure.unknown(message: e.toString()), + onRetry: () => ref.invalidate(grnLookupsProvider), + ), + data: (lookups) => existingAsync.when( + loading: () => const AppLoadingView(message: 'Loading GRN...'), + error: (e, _) => ErrorView.fromFailure( + e is Failure ? e : Failure.unknown(message: e.toString()), + onRetry: () => ref.invalidate(grnFormProvider(widget.grnId)), + ), + data: (existing) => _buildFormBody(lookups: lookups, existing: existing), + ), + ), + ); + } + + Widget _buildFormBody({ + required GrnLookups lookups, + GrnModel? existing, + }) { + if (widget.isEditing && existing != null) { + final signature = _grnSignature(existing); + if (_populatedSignature != signature) { + _populatedSignature = signature; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _populateFromGrn(existing); + }); + } + } + + if (widget.isEditing && existing != null && !existing.canEdit) { + return ErrorView.fromFailure( + const Failure.validation(message: 'This GRN cannot be edited'), + onRetry: () => context.go('${RouteConstants.grn}/${existing.id}'), + ); + } + + final warehouseIds = + lookups.warehouses.map((e) => _parseId(e.id)).whereType(); + final poOptions = lookups.receivablePurchaseOrders + .map( + (po) => AppDropdownOption( + value: po.id, + label: po.poNo ?? 'PO #${po.id}', + ), + ) + .toList(); + + return SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(24), + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1200), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!widget.isEditing) + const PageHeader( + title: 'Create Goods Received Note', + subtitle: 'Receive items against an approved purchase order', + ) + else if (existing?.grnNumber != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + existing!.grnNumber!, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormRowThree( + children: [ + _DateField( + label: 'GRN Date *', + value: _grnDate, + enabled: !widget.isEditing, + onTap: widget.isEditing + ? null + : () => _pickDate( + current: _grnDate, + onPicked: (d) => setState(() => _grnDate = d), + ), + ), + if (!widget.isEditing) + AppSearchableDropdown( + label: 'Purchase Order *', + value: _selectedPoId, + searchHint: 'Search PO...', + options: poOptions, + onChanged: (v) async { + setState(() => _selectedPoId = v); + if (v == null) { + for (final line in _lines) { + line.dispose(); + } + setState(() => _lines.clear()); + return; + } + try { + final po = await ref.read( + grnPurchaseOrderProvider(v).future, + ); + if (mounted && po != null) _loadLinesFromPo(po); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } + }, + validator: (v) => + v == null ? 'Purchase order is required' : null, + ) + else + InputDecorator( + decoration: const InputDecoration( + labelText: 'Purchase Order', + ), + child: Text(existing?.poNumber ?? '—'), + ), + AppSearchableDropdown( + label: 'Warehouse *', + value: _dropdownValue(_warehouseId, warehouseIds), + searchHint: 'Search warehouse...', + options: _intOptions(lookups.warehouses), + onChanged: widget.isEditing + ? (_) {} + : (v) => setState(() => _warehouseId = v), + validator: widget.isEditing + ? null + : (v) => v == null ? 'Warehouse is required' : null, + enabled: !widget.isEditing, + ), + ], + ), + FormRowThree( + children: [ + AppTextField( + label: 'Vendor Invoice No', + controller: _vendorInvoiceNoController, + ), + _DateField( + label: 'Vendor Invoice Date', + value: _vendorInvoiceDate, + onTap: () => _pickDate( + current: _vendorInvoiceDate, + onPicked: (d) => + setState(() => _vendorInvoiceDate = d), + ), + ), + AppTextField( + label: 'Vendor Invoice Amount', + controller: _vendorInvoiceAmountController, + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + ), + ], + ), + FormRowThree( + children: [ + AppTextField( + label: 'Vehicle No', + controller: _vehicleNoController, + ), + AppTextField( + label: 'LR No', + controller: _lrNoController, + ), + _DateField( + label: 'LR Date', + value: _lrDate, + onTap: () => _pickDate( + current: _lrDate, + onPicked: (d) => setState(() => _lrDate = d), + ), + ), + ], + ), + AppTextField( + label: 'Remarks', + controller: _remarksController, + maxLines: 3, + ), + if (!widget.isEditing) ...[ + const SizedBox(height: 24), + Text( + 'Line Items', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 12), + GrnLineItemsEditor( + items: _lines, + assetCategoryOptions: + _assetCategoryOptions(lookups.assetCategories), + onChanged: () => setState(() {}), + ), + ] else ...[ + const SizedBox(height: 16), + Text( + 'Line items cannot be changed after posting.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 24), + Row( + children: [ + Expanded( + child: AppButton( + label: widget.isEditing ? 'Save Changes' : 'Create GRN', + onPressed: _isSubmitting ? null : _submit, + isLoading: _isSubmitting, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +class _DateField extends StatelessWidget { + const _DateField({ + required this.label, + required this.value, + this.onTap, + this.enabled = true, + }); + + final String label; + final DateTime? value; + final VoidCallback? onTap; + final bool enabled; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: enabled ? onTap : null, + borderRadius: BorderRadius.circular(8), + child: InputDecorator( + decoration: InputDecoration( + labelText: label, + suffixIcon: const Icon(Icons.calendar_today_outlined, size: 18), + enabled: enabled, + ), + child: Text( + value != null ? DateFormatter.displayDate(value) : 'Select date', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: value != null + ? null + : Theme.of(context).hintColor, + ), + ), + ), + ); + } +} diff --git a/lib/modules/grn/presentation/screens/grn_list_screen.dart b/lib/modules/grn/presentation/screens/grn_list_screen.dart new file mode 100644 index 0000000..be9ed09 --- /dev/null +++ b/lib/modules/grn/presentation/screens/grn_list_screen.dart @@ -0,0 +1,349 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/formatters.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/models/grn_model.dart'; +import '../../../../shared/providers/permissions_provider.dart'; +import '../../../../shared/widgets/app_card.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/can_permission.dart'; +import '../../../../shared/widgets/app_table_action_icon.dart'; +import '../../../../shared/widgets/app_table_shell.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/grn_provider.dart'; +import '../widgets/grn_status_chip.dart'; + +class GrnListScreen extends ConsumerStatefulWidget { + const GrnListScreen({super.key}); + + @override + ConsumerState createState() => _GrnListScreenState(); +} + +class _GrnListScreenState extends ConsumerState { + final _searchController = TextEditingController(); + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final listAsync = ref.watch(grnListProvider); + final canEdit = ref.can('grn', PermissionAction.update); + + return Padding( + padding: const EdgeInsets.all(24), + child: listAsync.when( + loading: () => const AppLoadingView(message: 'Loading GRNs...'), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(grnListProvider), + ), + data: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Goods Received Notes', + subtitle: 'Record and track purchase order receipts', + actions: [ + CanPermission( + module: 'grn', + action: PermissionAction.create, + child: ElevatedButton.icon( + onPressed: () => context.go(RouteConstants.grnAdd), + icon: const Icon(Icons.add), + label: const Text('Create GRN'), + ), + ), + ], + ), + const SizedBox(height: 16), + Expanded( + child: AppTableShell( + toolbar: LayoutBuilder( + builder: (context, constraints) { + return _FiltersBar( + searchController: _searchController, + query: state.query, + wrapped: constraints.maxWidth < 900, + onSearch: ref.read(grnListProvider.notifier).setSearch, + onStatusChanged: + ref.read(grnListProvider.notifier).setStatusFilter, + ); + }, + ), + footer: AppPagination( + currentPage: state.query.page, + totalPages: state.totalPages, + totalItems: state.total, + pageSize: state.query.limit, + onPageChanged: ref.read(grnListProvider.notifier).setPage, + onPageSizeChanged: ref.read(grnListProvider.notifier).setPageSize, + ), + child: RefreshIndicator( + onRefresh: () => ref.read(grnListProvider.notifier).refresh(), + child: state.grns.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const [ + SizedBox( + height: 240, + child: AppEmptyState( + title: 'No GRNs found', + description: + 'Try adjusting filters or create a new goods received note.', + icon: Icons.inventory_2_outlined, + ), + ), + ], + ) + : context.isMobile + ? _GrnCardList( + grns: state.grns, + onView: _viewGrn, + onEdit: canEdit ? _editGrn : null, + ) + : _GrnDataTable( + grns: state.grns, + onView: _viewGrn, + onEdit: canEdit ? _editGrn : null, + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _viewGrn(GrnModel grn) { + context.push('${RouteConstants.grn}/${grn.id}'); + } + + void _editGrn(GrnModel grn) { + if (!grn.canEdit) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Only posted GRNs can be edited')), + ); + return; + } + context.push('${RouteConstants.grn}/${grn.id}/edit'); + } +} + +class _FiltersBar extends StatelessWidget { + const _FiltersBar({ + required this.searchController, + required this.query, + required this.wrapped, + required this.onSearch, + required this.onStatusChanged, + }); + + final TextEditingController searchController; + final GrnListQuery query; + final bool wrapped; + final ValueChanged onSearch; + final ValueChanged onStatusChanged; + + @override + Widget build(BuildContext context) { + final searchField = SizedBox( + width: wrapped ? double.infinity : null, + child: AppSearchField( + controller: searchController, + hint: 'Search GRN number, PO, vendor...', + onChanged: onSearch, + ), + ); + + final statusFilter = SizedBox( + width: wrapped ? double.infinity : 180, + child: AppSearchableDropdown( + label: 'Status', + value: query.status, + searchHint: 'Search status...', + isDense: true, + options: [ + const AppDropdownOption(value: null, label: 'All statuses'), + ...grnStatusOptions.map( + (e) => AppDropdownOption(value: e.$1, label: e.$2), + ), + ], + onChanged: onStatusChanged, + ), + ); + + if (wrapped) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + searchField, + const SizedBox(height: 12), + statusFilter, + ], + ); + } + + return Row( + children: [ + Expanded(flex: 3, child: searchField), + const SizedBox(width: 12), + Expanded(flex: 2, child: statusFilter), + ], + ); + } +} + +class _GrnDataTable extends StatelessWidget { + const _GrnDataTable({ + required this.grns, + required this.onView, + this.onEdit, + }); + + final List grns; + final ValueChanged onView; + final ValueChanged? onEdit; + + @override + Widget build(BuildContext context) { + return AppDataTable( + wrapInCard: false, + columns: [ + AppDataColumn( + label: 'GRN Number', + flex: 2, + cellBuilder: (_, grn) => Text(grn.grnNumber ?? '—'), + ), + AppDataColumn( + label: 'Date', + flex: 1, + cellBuilder: (_, grn) => Text(DateFormatter.displayDate(grn.grnDate)), + ), + AppDataColumn( + label: 'PO Number', + flex: 2, + cellBuilder: (_, grn) => Text(grn.poNumber ?? '—'), + ), + AppDataColumn( + label: 'Vendor', + flex: 2, + cellBuilder: (_, grn) => Text(grn.vendorName ?? '—'), + ), + AppDataColumn( + label: 'Warehouse', + flex: 2, + cellBuilder: (_, grn) => Text(grn.warehouseName ?? '—'), + ), + AppDataColumn( + label: 'Status', + flex: 1, + cellBuilder: (_, grn) => GrnStatusChip(status: grn.status, compact: true), + ), + AppDataColumn( + label: 'Actions', + flex: 1, + cellBuilder: (_, grn) => AppTableActions( + children: [ + AppTableActionIcon( + icon: Icons.visibility_outlined, + tooltip: 'View', + onPressed: () => onView(grn), + ), + if (onEdit != null && grn.canEdit) + AppTableActionIcon( + icon: Icons.edit_outlined, + tooltip: 'Edit', + onPressed: () => onEdit!(grn), + ), + ], + ), + ), + ], + rows: grns, + ); + } +} + +class _GrnCardList extends StatelessWidget { + const _GrnCardList({ + required this.grns, + required this.onView, + this.onEdit, + }); + + final List grns; + final ValueChanged onView; + final ValueChanged? onEdit; + + @override + Widget build(BuildContext context) { + return ListView.separated( + physics: const AlwaysScrollableScrollPhysics(), + itemCount: grns.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final grn = grns[index]; + return AppCard( + onTap: () => onView(grn), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + grn.grnNumber ?? 'GRN #${grn.id}', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + ), + GrnStatusChip(status: grn.status, compact: true), + ], + ), + const SizedBox(height: 8), + Text('PO: ${grn.poNumber ?? '—'}'), + Text('Vendor: ${grn.vendorName ?? '—'}'), + Text('Date: ${DateFormatter.displayDate(grn.grnDate)}'), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + AppTableActionIcon( + icon: Icons.visibility_outlined, + tooltip: 'View', + onPressed: () => onView(grn), + ), + if (onEdit != null && grn.canEdit) + AppTableActionIcon( + icon: Icons.edit_outlined, + tooltip: 'Edit', + onPressed: () => onEdit!(grn), + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/modules/grn/presentation/widgets/grn_line_items_editor.dart b/lib/modules/grn/presentation/widgets/grn_line_items_editor.dart new file mode 100644 index 0000000..c28a750 --- /dev/null +++ b/lib/modules/grn/presentation/widgets/grn_line_items_editor.dart @@ -0,0 +1,358 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import '../../../../core/theme/app_colors.dart'; +import '../../../../shared/models/grn_model.dart'; +import '../../../../shared/models/purchase_order_model.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_text_field.dart'; + +String _formatQty(double value) { + if (value % 1 == 0) return value.toInt().toString(); + return value.toStringAsFixed(2); +} + +class GrnLineItemDraft { + GrnLineItemDraft({ + required this.poItemId, + required this.lineNo, + required this.itemName, + required this.orderedQty, + required this.receivedQty, + required this.remainingQty, + TextEditingController? acceptedQtyController, + TextEditingController? damagedQtyController, + TextEditingController? shortQtyController, + TextEditingController? excessQtyController, + TextEditingController? batchNoController, + TextEditingController? expiryDateController, + TextEditingController? remarksController, + this.assetCategoryId, + }) : acceptedQtyController = acceptedQtyController ?? + TextEditingController( + text: remainingQty > 0 ? _formatQty(remainingQty) : '', + ), + damagedQtyController = damagedQtyController ?? TextEditingController(), + shortQtyController = shortQtyController ?? TextEditingController(), + excessQtyController = excessQtyController ?? TextEditingController(), + batchNoController = batchNoController ?? TextEditingController(), + expiryDateController = expiryDateController ?? TextEditingController(), + remarksController = remarksController ?? TextEditingController(); + + final String poItemId; + final int lineNo; + final String itemName; + final double orderedQty; + final double receivedQty; + final double remainingQty; + final TextEditingController acceptedQtyController; + final TextEditingController damagedQtyController; + final TextEditingController shortQtyController; + final TextEditingController excessQtyController; + final TextEditingController batchNoController; + final TextEditingController expiryDateController; + final TextEditingController remarksController; + String? assetCategoryId; + + double get acceptedQty => double.tryParse(acceptedQtyController.text.trim()) ?? 0; + double get damagedQty => double.tryParse(damagedQtyController.text.trim()) ?? 0; + double get shortQty => double.tryParse(shortQtyController.text.trim()) ?? 0; + double get excessQty => double.tryParse(excessQtyController.text.trim()) ?? 0; + double get currentQty => acceptedQty + damagedQty + shortQty + excessQty; + + void dispose() { + acceptedQtyController.dispose(); + damagedQtyController.dispose(); + shortQtyController.dispose(); + excessQtyController.dispose(); + batchNoController.dispose(); + expiryDateController.dispose(); + remarksController.dispose(); + } + + Map toPayload() { + final poItemIdValue = int.tryParse(poItemId) ?? poItemId; + return { + 'po_item_id': poItemIdValue, + 'line_no': lineNo, + 'current_qty': currentQty, + 'accepted_qty': acceptedQty, + if (damagedQty > 0) 'damaged_qty': damagedQty, + if (shortQty > 0) 'short_qty': shortQty, + if (excessQty > 0) 'excess_qty': excessQty, + if (batchNoController.text.trim().isNotEmpty) + 'batch_no': batchNoController.text.trim(), + if (expiryDateController.text.trim().isNotEmpty) + 'expiry_date': expiryDateController.text.trim(), + if (assetCategoryId != null && assetCategoryId!.isNotEmpty) + 'asset_category_id': int.tryParse(assetCategoryId!) ?? assetCategoryId, + if (remarksController.text.trim().isNotEmpty) + 'remarks': remarksController.text.trim(), + }; + } +} + +List draftsFromPurchaseOrder(PurchaseOrderModel po) { + return po.items.map((item) { + final ordered = item.orderedQty ?? 0; + final received = item.receivedQty ?? 0; + final remaining = (ordered - received).clamp(0.0, double.infinity); + return GrnLineItemDraft( + poItemId: item.id, + lineNo: item.lineNo ?? 1, + itemName: item.itemName ?? 'Item ${item.lineNo}', + orderedQty: ordered, + receivedQty: received, + remainingQty: remaining, + ); + }).toList(); +} + +class GrnLineItemsEditor extends StatelessWidget { + const GrnLineItemsEditor({ + super.key, + required this.items, + required this.onChanged, + this.assetCategoryOptions = const [], + this.readOnly = false, + }); + + final List items; + final VoidCallback onChanged; + final List> assetCategoryOptions; + final bool readOnly; + + @override + Widget build(BuildContext context) { + if (items.isEmpty) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: AppColors.lightSurface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.2)), + ), + child: Text( + readOnly + ? 'No line items.' + : 'Select a purchase order to load receivable line items.', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: AppColors.textSecondary, + ), + ), + ); + } + + return Column( + children: [ + for (var i = 0; i < items.length; i++) + Padding( + padding: EdgeInsets.only(bottom: i == items.length - 1 ? 0 : 12), + child: _GrnLineItemCard( + item: items[i], + assetCategoryOptions: assetCategoryOptions, + readOnly: readOnly, + onChanged: onChanged, + ), + ), + ], + ); + } +} + +class _GrnLineItemCard extends StatelessWidget { + const _GrnLineItemCard({ + required this.item, + required this.onChanged, + required this.assetCategoryOptions, + required this.readOnly, + }); + + final GrnLineItemDraft item; + final VoidCallback onChanged; + final List> assetCategoryOptions; + final bool readOnly; + + static final _qtyFormatters = [ + FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,4}')), + ]; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.lightSurface, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.textSecondary.withValues(alpha: 0.2)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text( + 'Line ${item.lineNo}', + style: Theme.of(context).textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + item.itemName, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + 'Ordered: ${_formatQty(item.orderedQty)} · ' + 'Already received: ${_formatQty(item.receivedQty)} · ' + 'Remaining: ${_formatQty(item.remainingQty)}', + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: AppColors.textSecondary, + ), + ), + if (!readOnly) ...[ + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: AppTextField( + label: 'Accepted Qty *', + controller: item.acceptedQtyController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: _qtyFormatters, + onChanged: (_) => onChanged(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: AppTextField( + label: 'Damaged Qty', + controller: item.damagedQtyController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: _qtyFormatters, + onChanged: (_) => onChanged(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: AppTextField( + label: 'Short Qty', + controller: item.shortQtyController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: _qtyFormatters, + onChanged: (_) => onChanged(), + ), + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: AppTextField( + label: 'Excess Qty', + controller: item.excessQtyController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: _qtyFormatters, + onChanged: (_) => onChanged(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: AppTextField( + label: 'Batch No', + controller: item.batchNoController, + onChanged: (_) => onChanged(), + ), + ), + const SizedBox(width: 12), + Expanded( + child: AppTextField( + label: 'Expiry Date', + controller: item.expiryDateController, + hint: 'YYYY-MM-DD', + onChanged: (_) => onChanged(), + ), + ), + ], + ), + if (assetCategoryOptions.isNotEmpty) ...[ + const SizedBox(height: 12), + AppDropdown( + label: 'Asset Category', + value: item.assetCategoryId, + options: assetCategoryOptions, + onChanged: (v) { + item.assetCategoryId = v; + onChanged(); + }, + ), + ], + const SizedBox(height: 12), + AppTextField( + label: 'Remarks', + controller: item.remarksController, + maxLines: 2, + onChanged: (_) => onChanged(), + ), + ] else ...[ + const SizedBox(height: 8), + Text( + 'Accepted: ${_formatQty(item.acceptedQty)} · ' + 'Current: ${_formatQty(item.currentQty)}', + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ], + ), + ); + } +} + +class GrnItemsTable extends StatelessWidget { + const GrnItemsTable({super.key, required this.items}); + + final List items; + + @override + Widget build(BuildContext context) { + if (items.isEmpty) { + return const SizedBox.shrink(); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: DataTable( + headingRowColor: WidgetStateProperty.all(AppColors.lightSurface), + columns: const [ + DataColumn(label: Text('Line')), + DataColumn(label: Text('Item')), + DataColumn(label: Text('Accepted')), + DataColumn(label: Text('Damaged')), + DataColumn(label: Text('Short')), + DataColumn(label: Text('Excess')), + DataColumn(label: Text('Batch')), + ], + rows: items.map((item) { + return DataRow( + cells: [ + DataCell(Text('${item.lineNo}')), + DataCell(Text(item.itemName ?? '—')), + DataCell(Text(_formatQty(item.acceptedQty ?? 0))), + DataCell(Text(_formatQty(item.damagedQty ?? 0))), + DataCell(Text(_formatQty(item.shortQty ?? 0))), + DataCell(Text(_formatQty(item.excessQty ?? 0))), + DataCell(Text(item.batchNo ?? '—')), + ], + ); + }).toList(), + ), + ); + } +} diff --git a/lib/modules/grn/presentation/widgets/grn_status_chip.dart b/lib/modules/grn/presentation/widgets/grn_status_chip.dart new file mode 100644 index 0000000..84b56f5 --- /dev/null +++ b/lib/modules/grn/presentation/widgets/grn_status_chip.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +import '../../../../shared/models/grn_model.dart'; + +class GrnStatusChip extends StatelessWidget { + const GrnStatusChip({ + 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.toUpperCase()) { + case 'POSTED': + return (Colors.green.shade700, grnStatusLabel(raw)); + case 'CANCELLED': + return (Colors.grey.shade700, grnStatusLabel(raw)); + default: + return (Colors.blueGrey, grnStatusLabel(raw)); + } + } +} diff --git a/lib/modules/master_data/domain/entities/master_definition.dart b/lib/modules/master_data/domain/entities/master_definition.dart index e80b03a..304fe57 100644 --- a/lib/modules/master_data/domain/entities/master_definition.dart +++ b/lib/modules/master_data/domain/entities/master_definition.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; enum MasterFieldType { text, number, boolean, dropdown } +const brandTypes = ['OWN', 'OEM', 'THIRD_PARTY']; + class MasterFieldDef { const MasterFieldDef({ required this.key, @@ -10,6 +12,7 @@ class MasterFieldDef { this.required = false, this.showInList = false, this.optionsMasterKey, + this.staticOptions, this.multiline = false, }); @@ -20,6 +23,8 @@ class MasterFieldDef { final bool showInList; /// Master key used to populate dropdown options (e.g. `plants` for plant_id). final String? optionsMasterKey; + /// Fixed dropdown choices (e.g. brand type) — no API lookup. + final List? staticOptions; final bool multiline; } @@ -209,7 +214,14 @@ const masterDefinitions = [ fields: [ MasterFieldDef(key: 'code', label: 'Code', required: true, showInList: true), MasterFieldDef(key: 'name', label: 'Name', required: true, showInList: true), - MasterFieldDef(key: 'brand_type', label: 'Brand Type', required: true, showInList: true), + MasterFieldDef( + key: 'brand_type', + label: 'Brand Type', + type: MasterFieldType.dropdown, + required: true, + showInList: true, + staticOptions: brandTypes, + ), MasterFieldDef(key: 'contact_person', label: 'Contact Person'), MasterFieldDef(key: 'phone', label: 'Phone'), MasterFieldDef(key: 'email', label: 'Email'), @@ -412,6 +424,16 @@ List get masterCategories => masterDefinitions.map((def) => def.category).toSet().toList(); String masterRecordLabel(Map row) { + final ratePct = row['rate_pct']; + if (ratePct != null) { + final rate = ratePct is num + ? ratePct.toDouble() + : double.tryParse(ratePct.toString()); + if (rate != null) { + return rate % 1 == 0 ? '${rate.toInt()}%' : '$rate%'; + } + } + for (final key in ['name', 'item_name', 'code', 'item_code', 'description']) { final value = row[key]; if (value != null && value.toString().trim().isNotEmpty) { @@ -432,6 +454,9 @@ String masterCellValue(Map row, MasterFieldDef field) { if (field.type == MasterFieldType.dropdown) { final label = row['${field.key}_label']; if (label != null && label.toString().isNotEmpty) return label.toString(); + if (field.staticOptions != null) { + return value.toString().replaceAll('_', ' '); + } } return value.toString(); diff --git a/lib/modules/master_data/presentation/screens/master_list_screen.dart b/lib/modules/master_data/presentation/screens/master_list_screen.dart index 8a26265..b46dbf2 100644 --- a/lib/modules/master_data/presentation/screens/master_list_screen.dart +++ b/lib/modules/master_data/presentation/screens/master_list_screen.dart @@ -9,6 +9,7 @@ import '../../../../shared/utils/file_download_helper.dart'; import '../../../../shared/providers/permissions_provider.dart'; import '../../../../shared/widgets/app_card.dart'; import '../../../../shared/widgets/app_confirmation_dialog.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_export_bar.dart'; @@ -327,181 +328,67 @@ class _MasterListTable extends StatelessWidget { final ValueChanged onEdit; final ValueChanged> onDelete; - static const double _horizontalPadding = 16; - static const double _columnSpacing = 16; - static const double _statusWidth = 110; - static const double _actionsWidth = 96; + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AppDataTable>( + wrapInCard: false, + columns: [ + ...definition.listFields.map( + (field) => AppDataColumn>( + label: field.label, + flex: _columnFlex(field), + cellBuilder: (_, row) => Text(masterCellValue(row, field)), + ), + ), + AppDataColumn( + label: 'Status', + flex: 1, + cellBuilder: (_, row) => AppStatusChip( + status: masterStatusValue(row), + compact: true, + ), + ), + AppDataColumn( + label: 'Actions', + flex: 1, + alignment: Alignment.centerRight, + cellBuilder: (_, row) { + final id = row['id']?.toString(); + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (canEdit) + IconButton( + tooltip: 'Edit', + icon: const Icon(Icons.edit_outlined), + onPressed: id == null ? null : () => onEdit(id), + ), + if (canDelete) + IconButton( + tooltip: 'Delete', + icon: Icon( + Icons.delete_outline, + color: theme.colorScheme.error, + ), + onPressed: isDeleting ? null : () => onDelete(row), + ), + ], + ); + }, + ), + ], + rows: items, + ); + } int _columnFlex(MasterFieldDef field) { return switch (field.key) { 'code' || 'item_code' || 'series_code' => 1, - 'name' || - 'item_name' || - 'description' || - 'term_name' => - 3, + 'name' || 'item_name' || 'description' || 'term_name' => 3, _ => 2, }; } - - TextStyle? _headerStyle(BuildContext context) { - return Theme.of(context).textTheme.labelSmall?.copyWith( - fontWeight: FontWeight.w700, - letterSpacing: 0.6, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ); - } - - Widget _tableRow({ - required BuildContext context, - required List fieldCells, - required Widget statusCell, - required Widget actionsCell, - Color? backgroundColor, - BoxDecoration? decoration, - EdgeInsetsGeometry padding = const EdgeInsets.symmetric( - horizontal: _horizontalPadding, - vertical: 12, - ), - }) { - final fields = definition.listFields; - final children = []; - - for (var i = 0; i < fields.length; i++) { - if (i > 0) { - children.add(const SizedBox(width: _columnSpacing)); - } - children.add( - Expanded( - flex: _columnFlex(fields[i]), - child: Align( - alignment: Alignment.centerLeft, - child: fieldCells[i], - ), - ), - ); - } - - children.addAll([ - const SizedBox(width: _columnSpacing), - SizedBox( - width: _statusWidth, - child: Align( - alignment: Alignment.centerLeft, - child: statusCell, - ), - ), - const SizedBox(width: _columnSpacing), - SizedBox( - width: _actionsWidth, - child: Align( - alignment: Alignment.centerLeft, - child: actionsCell, - ), - ), - ]); - - return Container( - width: double.infinity, - padding: padding, - color: backgroundColor, - decoration: decoration, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: children, - ), - ); - } - - Widget _headerRow(BuildContext context, Color headerColor) { - return _tableRow( - context: context, - backgroundColor: headerColor, - fieldCells: definition.listFields - .map( - (field) => Text( - field.label.toUpperCase(), - style: _headerStyle(context), - ), - ) - .toList(), - statusCell: Text('STATUS', style: _headerStyle(context)), - actionsCell: Text('ACTIONS', style: _headerStyle(context)), - ); - } - - Widget _dataRow(BuildContext context, Map row, ThemeData theme) { - final id = row['id']?.toString(); - - return _tableRow( - context: context, - padding: const EdgeInsets.symmetric( - horizontal: _horizontalPadding, - vertical: 4, - ), - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: theme.colorScheme.outline.withValues(alpha: 0.08), - ), - ), - ), - fieldCells: definition.listFields - .map( - (field) => Text(masterCellValue(row, field)), - ) - .toList(), - statusCell: AppStatusChip( - status: masterStatusValue(row), - compact: true, - ), - actionsCell: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (canEdit) - IconButton( - tooltip: 'Edit', - icon: const Icon(Icons.edit_outlined, size: 20), - onPressed: id == null ? null : () => onEdit(id), - ), - if (canDelete) - IconButton( - tooltip: 'Delete', - icon: Icon( - Icons.delete_outline, - size: 20, - color: theme.colorScheme.error, - ), - onPressed: isDeleting ? null : () => onDelete(row), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - final headerColor = theme.colorScheme.surfaceContainerHighest - .withValues(alpha: 0.4); - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _headerRow(context, headerColor), - const Divider(height: 1), - Expanded( - child: SingleChildScrollView( - physics: const AlwaysScrollableScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: items - .map((row) => _dataRow(context, row, theme)) - .toList(), - ), - ), - ), - ], - ); - } } diff --git a/lib/modules/master_data/presentation/widgets/master_form_panel.dart b/lib/modules/master_data/presentation/widgets/master_form_panel.dart index c7ed639..0ba05f0 100644 --- a/lib/modules/master_data/presentation/widgets/master_form_panel.dart +++ b/lib/modules/master_data/presentation/widgets/master_form_panel.dart @@ -95,15 +95,20 @@ class _MasterFormPanelState extends ConsumerState { ); case MasterFieldType.dropdown: - final options = formState.dropdownOptions[field.optionsMasterKey] ?? - const >[]; - final dropdownOptions = >[]; - for (final item in options) { - final id = item['id']?.toString(); - if (id == null || id.isEmpty) continue; - dropdownOptions.add( - AppDropdownOption(value: id, label: masterRecordLabel(item)), - ); + final List> dropdownOptions; + if (field.staticOptions != null) { + dropdownOptions = stringDropdownOptions(field.staticOptions!); + } else { + final options = formState.dropdownOptions[field.optionsMasterKey] ?? + const >[]; + dropdownOptions = >[]; + for (final item in options) { + final id = item['id']?.toString(); + if (id == null || id.isEmpty) continue; + dropdownOptions.add( + AppDropdownOption(value: id, label: masterRecordLabel(item)), + ); + } } return AppSearchableDropdown( @@ -123,7 +128,7 @@ class _MasterFormPanelState extends ConsumerState { case MasterFieldType.number: return TextFormField( - key: ValueKey('${field.key}-${value ?? ''}'), + key: ValueKey(field.key), initialValue: value?.toString(), keyboardType: const TextInputType.numberWithOptions(decimal: true), decoration: InputDecoration(labelText: _fieldLabel(field)), @@ -134,19 +139,44 @@ class _MasterFormPanelState extends ConsumerState { ); case MasterFieldType.text: + final formatters = Validators.inputFormattersForFieldKey(field.key); return TextFormField( - key: ValueKey('${field.key}-${value ?? ''}'), + key: ValueKey(field.key), initialValue: value?.toString(), maxLines: field.multiline ? 3 : 1, + keyboardType: _keyboardTypeForFieldKey(field.key), + inputFormatters: formatters.isEmpty ? null : formatters, decoration: InputDecoration(labelText: _fieldLabel(field)), - validator: field.required - ? (v) => Validators.required(v, fieldName: field.label) - : null, + validator: (v) => Validators.forFieldKey( + field.key, + v, + required: field.required, + fieldName: field.label, + ), onChanged: (text) => notifier.updateValue(field.key, text), ); } } + TextInputType? _keyboardTypeForFieldKey(String key) { + final normalizedKey = key.trim().toLowerCase(); + if (normalizedKey == 'phone' || normalizedKey == 'mobile') { + return TextInputType.phone; + } + if (normalizedKey == 'email') { + return TextInputType.emailAddress; + } + if (normalizedKey == 'pincode' || + normalizedKey == 'postal_code' || + normalizedKey == 'zip') { + return TextInputType.number; + } + if (normalizedKey == 'account_number' || normalizedKey == 'account_no') { + return TextInputType.number; + } + return null; + } + List _buildFieldLayout( BuildContext context, MasterFormState formState, diff --git a/lib/modules/masters/data/datasources/master_remote_data_source.dart b/lib/modules/masters/data/datasources/master_remote_data_source.dart index 2e459b2..624044c 100644 --- a/lib/modules/masters/data/datasources/master_remote_data_source.dart +++ b/lib/modules/masters/data/datasources/master_remote_data_source.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/constants/api_endpoints.dart'; +import '../../../../core/constants/app_constants.dart'; import '../../../../core/network/dio_client.dart'; import '../../../../shared/models/user_management_models.dart'; @@ -24,10 +25,35 @@ class MasterRemoteDataSource { Future> listDesignations() => _listOptions(ApiEndpoints.designations); + Future> listPaymentTerms() => + _listOptions(ApiEndpoints.paymentTerms); + + Future> listDeliveryTerms() => + _listOptions(ApiEndpoints.deliveryTerms); + + Future> listWarehouses() => + _listOptions(ApiEndpoints.warehouses); + + Future> listBrands() => + _listOptions(ApiEndpoints.brands); + + Future> listUom() => _listOptions(ApiEndpoints.uom); + + Future> listItems() => _listOptions(ApiEndpoints.items); + + Future> listGstRates() => + _listOptions(ApiEndpoints.gstRates); + + Future> listAssetCategories() => + _listOptions(ApiEndpoints.assetCategories); + Future> _listOptions(String endpoint) async { final response = await dio.get( endpoint, - queryParameters: const {'limit': 100, 'is_active': true}, + queryParameters: { + 'limit': AppConstants.maxPageSize, + 'is_active': true, + }, ); final body = response.data as Map; final raw = body['data']; @@ -44,12 +70,45 @@ class MasterRemoteDataSource { .map( (item) => FilterOptionModel( id: item['id']?.toString() ?? '', - name: item['name'] as String? ?? '', + name: _optionLabel(item), ), ) .where((item) => item.id.isNotEmpty && item.name.isNotEmpty) .toList(); } + + String _optionLabel(Map item) { + final ratePct = item['rate_pct']; + if (ratePct != null) { + final rate = ratePct is num + ? ratePct.toDouble() + : double.tryParse(ratePct.toString()); + if (rate != null) { + final rateLabel = + rate % 1 == 0 ? '${rate.toInt()}%' : '$rate%'; + final desc = item['description']; + if (desc is String && desc.trim().isNotEmpty) { + return '$rateLabel — ${desc.trim()}'; + } + return rateLabel; + } + } + + for (final key in [ + 'name', + 'item_name', + 'term_name', + 'vendor_name', + 'code', + 'description', + ]) { + final value = item[key]; + if (value is String && value.trim().isNotEmpty) { + return value.trim(); + } + } + return ''; + } } /// Users whose role name is exactly "Manager" (for reporting_to dropdown). diff --git a/lib/modules/purchase_orders/data/datasources/purchase_order_remote_data_source.dart b/lib/modules/purchase_orders/data/datasources/purchase_order_remote_data_source.dart new file mode 100644 index 0000000..412f7ca --- /dev/null +++ b/lib/modules/purchase_orders/data/datasources/purchase_order_remote_data_source.dart @@ -0,0 +1,193 @@ +import 'package:dio/dio.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/purchase_order_model.dart'; + +class PurchaseOrderRemoteDataSource { + PurchaseOrderRemoteDataSource({required this.dio}); + + final Dio dio; + + Future> getPurchaseOrders( + PurchaseOrderListQuery query, + ) async { + final response = await dio.get( + ApiEndpoints.purchaseOrders, + queryParameters: _queryToMap(query), + ); + return _parsePaginated(response.data, PurchaseOrderModel.fromJson); + } + + Future getPurchaseOrderById(String id) async { + final response = await dio.get(ApiEndpoints.purchaseOrderById(id)); + return PurchaseOrderModel.fromJson( + response.data['data'] as Map, + ); + } + + Future createPurchaseOrder(Map data) async { + final response = await dio.post(ApiEndpoints.purchaseOrders, data: data); + return PurchaseOrderModel.fromJson( + response.data['data'] as Map, + ); + } + + Future updatePurchaseOrder( + String id, + Map data, + ) async { + final response = await dio.put(ApiEndpoints.purchaseOrderById(id), data: data); + return PurchaseOrderModel.fromJson( + response.data['data'] as Map, + ); + } + + Future deletePurchaseOrder(String id) async { + await dio.delete(ApiEndpoints.purchaseOrderById(id)); + } + + Future submitPurchaseOrder( + String id, { + String? remarks, + }) async { + final response = await dio.post( + ApiEndpoints.purchaseOrderSubmit(id), + data: remarks != null ? {'remarks': remarks} : {}, + ); + return PurchaseOrderModel.fromJson( + response.data['data'] as Map, + ); + } + + Future approvePurchaseOrder( + String id, { + String? remarks, + }) async { + final response = await dio.post( + ApiEndpoints.purchaseOrderApprove(id), + data: remarks != null ? {'remarks': remarks} : {}, + ); + return PurchaseOrderModel.fromJson( + response.data['data'] as Map, + ); + } + + Future rejectPurchaseOrder( + String id, { + required String remarks, + }) async { + final response = await dio.post( + ApiEndpoints.purchaseOrderReject(id), + data: {'remarks': remarks}, + ); + return PurchaseOrderModel.fromJson( + response.data['data'] as Map, + ); + } + + Future amendPurchaseOrder( + String id, { + Map? data, + }) async { + final response = await dio.post( + ApiEndpoints.purchaseOrderAmend(id), + data: data ?? {}, + ); + return PurchaseOrderModel.fromJson( + response.data['data'] as Map, + ); + } + + Future cancelPurchaseOrder( + String id, { + String? remarks, + }) async { + final response = await dio.post( + ApiEndpoints.purchaseOrderCancel(id), + data: remarks != null ? {'remarks': remarks} : {}, + ); + return PurchaseOrderModel.fromJson( + response.data['data'] as Map, + ); + } + + Future> downloadPurchaseOrderPdf(String id) async { + final response = await dio.get>( + ApiEndpoints.purchaseOrderPdf(id), + options: Options(responseType: ResponseType.bytes), + ); + return response.data ?? []; + } + + Map _queryToMap(PurchaseOrderListQuery 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.poType != null) 'po_type': query.poType, + if (query.vendorId != null) 'vendor_id': query.vendorId, + if (query.plantId != null) 'plant_id': query.plantId, + if (query.dateFrom != null) 'date_from': query.dateFrom, + if (query.dateTo != null) 'date_to': query.dateTo, + }; + } + + PaginatedResponse _parsePaginated( + dynamic body, + T Function(Map) fromJson, + ) { + if (body is! Map) { + return const PaginatedResponse( + items: [], + page: 1, + limit: 20, + total: 0, + totalPages: 1, + ); + } + + final raw = body['data']; + final meta = body['meta'] as Map? ?? {}; + + if (raw is List) { + final items = raw.whereType>().map(fromJson).toList(); + final limit = (meta['limit'] as num?)?.toInt() ?? items.length; + final total = (meta['total'] as num?)?.toInt() ?? items.length; + return PaginatedResponse( + items: items, + page: (meta['page'] as num?)?.toInt() ?? 1, + limit: limit, + total: total, + totalPages: + limit > 0 ? ((total + limit - 1) / limit).ceil().clamp(1, 999999) : 1, + ); + } + + if (raw is Map) { + final list = raw['items']; + if (list is List) { + final items = list.whereType>().map(fromJson).toList(); + final limit = (meta['limit'] as num?)?.toInt() ?? 20; + final total = (meta['total'] as num?)?.toInt() ?? items.length; + return PaginatedResponse( + items: items, + page: (meta['page'] as num?)?.toInt() ?? 1, + limit: limit, + total: total, + totalPages: + limit > 0 ? ((total + limit - 1) / limit).ceil().clamp(1, 999999) : 1, + ); + } + } + + return const PaginatedResponse( + items: [], + page: 1, + limit: 20, + total: 0, + totalPages: 1, + ); + } +} diff --git a/lib/modules/purchase_orders/data/repositories/purchase_order_repository_impl.dart b/lib/modules/purchase_orders/data/repositories/purchase_order_repository_impl.dart new file mode 100644 index 0000000..797f9b9 --- /dev/null +++ b/lib/modules/purchase_orders/data/repositories/purchase_order_repository_impl.dart @@ -0,0 +1,102 @@ +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/purchase_order_model.dart'; +import '../../domain/repositories/purchase_order_repository.dart'; +import '../datasources/purchase_order_remote_data_source.dart'; + +final purchaseOrderRemoteDataSourceProvider = + Provider((ref) { + return PurchaseOrderRemoteDataSource(dio: ref.watch(dioProvider)); +}); + +final purchaseOrderRepositoryProvider = Provider((ref) { + return PurchaseOrderRepositoryImpl( + dataSource: ref.watch(purchaseOrderRemoteDataSourceProvider), + ); +}); + +class PurchaseOrderRepositoryImpl implements PurchaseOrderRepository { + PurchaseOrderRepositoryImpl({required this.dataSource}); + + final PurchaseOrderRemoteDataSource dataSource; + + @override + Future>> getPurchaseOrders( + PurchaseOrderListQuery query, + ) { + return safeApiCall(() => dataSource.getPurchaseOrders(query)); + } + + @override + Future> getPurchaseOrderById(String id) { + return safeApiCall(() => dataSource.getPurchaseOrderById(id)); + } + + @override + Future> createPurchaseOrder(Map data) { + return safeApiCall(() => dataSource.createPurchaseOrder(data)); + } + + @override + Future> updatePurchaseOrder( + String id, + Map data, + ) { + return safeApiCall(() => dataSource.updatePurchaseOrder(id, data)); + } + + @override + Future> deletePurchaseOrder(String id) { + return safeApiCall(() => dataSource.deletePurchaseOrder(id)); + } + + @override + Future> submitPurchaseOrder( + String id, { + String? remarks, + }) { + return safeApiCall(() => dataSource.submitPurchaseOrder(id, remarks: remarks)); + } + + @override + Future> approvePurchaseOrder( + String id, { + String? remarks, + }) { + return safeApiCall(() => dataSource.approvePurchaseOrder(id, remarks: remarks)); + } + + @override + Future> rejectPurchaseOrder( + String id, { + required String remarks, + }) { + return safeApiCall( + () => dataSource.rejectPurchaseOrder(id, remarks: remarks), + ); + } + + @override + Future> amendPurchaseOrder( + String id, { + Map? data, + }) { + return safeApiCall(() => dataSource.amendPurchaseOrder(id, data: data)); + } + + @override + Future> cancelPurchaseOrder( + String id, { + String? remarks, + }) { + return safeApiCall(() => dataSource.cancelPurchaseOrder(id, remarks: remarks)); + } + + @override + Future>> downloadPurchaseOrderPdf(String id) { + return safeApiCall(() => dataSource.downloadPurchaseOrderPdf(id)); + } +} diff --git a/lib/modules/purchase_orders/domain/repositories/purchase_order_repository.dart b/lib/modules/purchase_orders/domain/repositories/purchase_order_repository.dart new file mode 100644 index 0000000..3a84d9d --- /dev/null +++ b/lib/modules/purchase_orders/domain/repositories/purchase_order_repository.dart @@ -0,0 +1,28 @@ +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/purchase_order_model.dart'; + +abstract class PurchaseOrderRepository { + Future>> getPurchaseOrders( + PurchaseOrderListQuery query, + ); + Future> getPurchaseOrderById(String id); + Future> createPurchaseOrder(Map data); + Future> updatePurchaseOrder( + String id, + Map data, + ); + Future> deletePurchaseOrder(String id); + Future> submitPurchaseOrder(String id, {String? remarks}); + Future> approvePurchaseOrder(String id, {String? remarks}); + Future> rejectPurchaseOrder( + String id, { + required String remarks, + }); + Future> amendPurchaseOrder( + String id, { + Map? data, + }); + Future> cancelPurchaseOrder(String id, {String? remarks}); + Future>> downloadPurchaseOrderPdf(String id); +} diff --git a/lib/modules/purchase_orders/presentation/providers/purchase_order_lookups_provider.dart b/lib/modules/purchase_orders/presentation/providers/purchase_order_lookups_provider.dart new file mode 100644 index 0000000..5b26520 --- /dev/null +++ b/lib/modules/purchase_orders/presentation/providers/purchase_order_lookups_provider.dart @@ -0,0 +1,105 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/constants/app_constants.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/models/vendor_model.dart'; +import '../../../masters/data/datasources/master_remote_data_source.dart'; +import '../../../vendors/data/repositories/vendor_repository_impl.dart'; +import '../../../vendors/domain/repositories/vendor_repository.dart'; + +class PurchaseOrderLookups { + const PurchaseOrderLookups({ + this.vendors = const [], + this.plants = const [], + this.warehouses = const [], + this.brands = const [], + this.paymentTerms = const [], + this.deliveryTerms = const [], + this.items = const [], + this.uom = const [], + this.gstRates = const [], + }); + + final List vendors; + final List plants; + final List warehouses; + final List brands; + final List paymentTerms; + final List deliveryTerms; + final List items; + final List uom; + final List gstRates; +} + +final purchaseOrderLookupsProvider = + FutureProvider.autoDispose((ref) async { + final master = ref.watch(masterRemoteDataSourceProvider); + final vendorRepo = ref.watch(vendorRepositoryProvider); + + final vendors = await _safeOptions(() => _fetchActiveVendors(vendorRepo)); + + final results = await Future.wait([ + _safeOptions(master.listPlants), + _safeOptions(master.listWarehouses), + _safeOptions(master.listBrands), + _safeOptions(master.listPaymentTerms), + _safeOptions(master.listDeliveryTerms), + _safeOptions(master.listItems), + _safeOptions(master.listUom), + _safeOptions(master.listGstRates), + ]); + + return PurchaseOrderLookups( + vendors: vendors, + plants: results[0], + warehouses: results[1], + brands: results[2], + paymentTerms: results[3], + deliveryTerms: results[4], + items: results[5], + uom: results[6], + gstRates: results[7], + ); +}); + +Future> _safeOptions( + Future> Function() load, +) async { + try { + return await load(); + } catch (_) { + return const []; + } +} + +Future> _fetchActiveVendors( + VendorRepository vendorRepo, +) async { + final vendors = []; + var page = 1; + var totalPages = 1; + + while (page <= totalPages) { + final result = await vendorRepo.getVendors( + VendorListQuery( + page: page, + limit: AppConstants.maxPageSize, + isActive: true, + ), + ); + if (result.failure != null) { + throw result.failure!; + } + + final data = result.data!; + vendors.addAll( + data.items.map( + (vendor) => FilterOptionModel(id: vendor.id, name: vendor.vendorName), + ), + ); + totalPages = data.totalPages; + page++; + } + + return vendors; +} diff --git a/lib/modules/purchase_orders/presentation/providers/purchase_orders_provider.dart b/lib/modules/purchase_orders/presentation/providers/purchase_orders_provider.dart new file mode 100644 index 0000000..5c0cef4 --- /dev/null +++ b/lib/modules/purchase_orders/presentation/providers/purchase_orders_provider.dart @@ -0,0 +1,246 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/models/purchase_order_model.dart'; +import '../../data/repositories/purchase_order_repository_impl.dart'; + +class PurchaseOrdersListState { + const PurchaseOrdersListState({ + this.orders = const [], + this.query = const PurchaseOrderListQuery(limit: 20), + this.total = 0, + this.totalPages = 1, + this.isRefreshing = false, + this.actionError, + this.actionSuccess, + }); + + final List orders; + final PurchaseOrderListQuery query; + final int total; + final int totalPages; + final bool isRefreshing; + final String? actionError; + final String? actionSuccess; + + PurchaseOrdersListState copyWith({ + List? orders, + PurchaseOrderListQuery? query, + int? total, + int? totalPages, + bool? isRefreshing, + String? actionError, + String? actionSuccess, + bool clearMessages = false, + }) { + return PurchaseOrdersListState( + orders: orders ?? this.orders, + 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 purchaseOrdersListProvider = AsyncNotifierProvider.autoDispose< + PurchaseOrdersListNotifier, PurchaseOrdersListState>( + PurchaseOrdersListNotifier.new, +); + +class PurchaseOrdersListNotifier + extends AutoDisposeAsyncNotifier { + @override + Future build() async { + return _load(const PurchaseOrderListQuery(limit: 20)); + } + + Future _load(PurchaseOrderListQuery query) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.getPurchaseOrders(query); + if (result.failure != null) throw result.failure!; + final page = result.data!; + return PurchaseOrdersListState( + orders: page.items, + query: query, + total: page.total, + totalPages: page.totalPages, + ); + } + + Future refresh() async { + final current = state.valueOrNull ?? const PurchaseOrdersListState(); + state = AsyncData(current.copyWith(isRefreshing: true, clearMessages: true)); + try { + state = AsyncData(await _load(current.query)); + } catch (e, st) { + state = AsyncError(e, st); + } + } + + Future applyQuery(PurchaseOrderListQuery query) async { + final previous = state.valueOrNull; + if (previous == null) { + state = const AsyncLoading(); + } + try { + state = AsyncData(await _load(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 setStatusFilter(String? status) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(status: status, page: 1)); + } + + void setPoTypeFilter(String? poType) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(poType: poType, 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)); + } + + Future deletePurchaseOrder(String id) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.deletePurchaseOrder(id); + if (result.failure != null) { + final current = state.valueOrNull; + if (current != null) { + state = AsyncData(current.copyWith(actionError: result.failure!.message)); + } + return false; + } + await refresh(); + final current = state.valueOrNull; + if (current != null) { + state = AsyncData(current.copyWith(actionSuccess: 'Purchase order deleted')); + } + return true; + } +} + +final purchaseOrderDetailProvider = AsyncNotifierProvider.family< + PurchaseOrderDetailNotifier, PurchaseOrderModel, String>( + PurchaseOrderDetailNotifier.new, +); + +class PurchaseOrderDetailNotifier + extends FamilyAsyncNotifier { + @override + Future build(String arg) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.getPurchaseOrderById(arg); + if (result.failure != null) throw result.failure!; + return result.data!; + } + + Future reload() async { + state = const AsyncLoading(); + state = AsyncData(await build(arg)); + } + + Future submit({String? remarks}) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.submitPurchaseOrder(arg, remarks: remarks); + if (result.failure != null) throw result.failure!; + state = AsyncData(result.data!); + ref.invalidate(purchaseOrdersListProvider); + return result.data!; + } + + Future approve({String? remarks}) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.approvePurchaseOrder(arg, remarks: remarks); + if (result.failure != null) throw result.failure!; + state = AsyncData(result.data!); + ref.invalidate(purchaseOrdersListProvider); + return result.data!; + } + + Future reject({required String remarks}) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.rejectPurchaseOrder(arg, remarks: remarks); + if (result.failure != null) throw result.failure!; + state = AsyncData(result.data!); + ref.invalidate(purchaseOrdersListProvider); + return result.data!; + } + + Future amend() async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.amendPurchaseOrder(arg); + if (result.failure != null) throw result.failure!; + ref.invalidate(purchaseOrdersListProvider); + return result.data!; + } + + Future cancel({String? remarks}) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.cancelPurchaseOrder(arg, remarks: remarks); + if (result.failure != null) throw result.failure!; + state = AsyncData(result.data!); + ref.invalidate(purchaseOrdersListProvider); + return result.data!; + } + + Future> downloadPdf() async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.downloadPurchaseOrderPdf(arg); + if (result.failure != null) throw result.failure!; + return result.data ?? []; + } +} + +final purchaseOrderFormProvider = AsyncNotifierProvider.family< + PurchaseOrderFormNotifier, PurchaseOrderModel?, String?>( + PurchaseOrderFormNotifier.new, +); + +class PurchaseOrderFormNotifier extends FamilyAsyncNotifier { + @override + Future build(String? arg) async { + if (arg == null) return null; + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.getPurchaseOrderById(arg); + if (result.failure != null) throw result.failure!; + return result.data; + } + + Future submitCreate(Map data) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.createPurchaseOrder(data); + if (result.failure != null) throw result.failure!; + ref.invalidate(purchaseOrdersListProvider); + return result.data!; + } + + Future submitUpdate(String id, Map data) async { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.updatePurchaseOrder(id, data); + if (result.failure != null) throw result.failure!; + ref.invalidate(purchaseOrdersListProvider); + ref.invalidate(purchaseOrderDetailProvider(id)); + return result.data!; + } +} diff --git a/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart b/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart new file mode 100644 index 0000000..5e14925 --- /dev/null +++ b/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart @@ -0,0 +1,492 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/formatters.dart'; +import '../../../../shared/models/purchase_order_model.dart'; +import '../../../../shared/providers/permissions_provider.dart'; +import '../../../../shared/utils/file_download_helper.dart'; +import '../../../../shared/widgets/app_card.dart'; +import '../../../../shared/widgets/app_confirmation_dialog.dart'; +import '../../../../shared/widgets/app_data_table.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/purchase_orders_provider.dart'; +import '../../data/repositories/purchase_order_repository_impl.dart'; +import '../widgets/po_status_chip.dart'; + +class PurchaseOrderDetailScreen extends ConsumerStatefulWidget { + const PurchaseOrderDetailScreen({super.key, required this.purchaseOrderId}); + + final String purchaseOrderId; + + @override + ConsumerState createState() => + _PurchaseOrderDetailScreenState(); +} + +class _PurchaseOrderDetailScreenState + extends ConsumerState { + bool _isWorking = false; + + @override + Widget build(BuildContext context) { + final detailAsync = ref.watch(purchaseOrderDetailProvider(widget.purchaseOrderId)); + final canEdit = ref.can('purchase_orders', PermissionAction.update); + final canDelete = ref.can('purchase_orders', PermissionAction.delete); + final canApprove = ref.can('purchase_orders', PermissionAction.approve); + final canExport = ref.can('purchase_orders', PermissionAction.export); + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + surfaceTintColor: Colors.transparent, + leading: IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => context.go(RouteConstants.purchaseOrders), + ), + title: detailAsync.maybeWhen( + data: (order) => Text(order.poNo ?? 'Purchase Order #${order.id}'), + orElse: () => const Text('Purchase Order'), + ), + ), + body: detailAsync.when( + loading: () => const AppLoadingView(message: 'Loading purchase order...'), + error: (e, _) => ErrorView.fromFailure( + e is Failure ? e : Failure.unknown(message: e.toString()), + onRetry: () => + ref.invalidate(purchaseOrderDetailProvider(widget.purchaseOrderId)), + ), + data: (order) => SingleChildScrollView( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: order.poNo ?? 'Purchase Order #${order.id}', + subtitle: + '${poTypeLabel(order.poType)} · ${order.vendorName ?? '—'}', + actions: [ + if (canExport) + OutlinedButton.icon( + onPressed: _isWorking ? null : () => _downloadPdf(order), + icon: const Icon(Icons.picture_as_pdf_outlined), + label: const Text('PDF'), + ), + if (canEdit && order.canEdit) ...[ + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: _isWorking + ? null + : () => context.push( + '${RouteConstants.purchaseOrders}/${order.id}/edit', + ), + icon: const Icon(Icons.edit_outlined), + label: const Text('Edit'), + ), + ], + if (canEdit && order.canSubmit) ...[ + const SizedBox(width: 8), + FilledButton.icon( + onPressed: _isWorking ? null : () => _submit(order), + icon: const Icon(Icons.send_outlined), + label: const Text('Submit'), + ), + ], + if (canApprove && order.canApprove) ...[ + const SizedBox(width: 8), + FilledButton.icon( + onPressed: _isWorking ? null : () => _approve(order), + icon: const Icon(Icons.check_circle_outline), + label: const Text('Approve'), + ), + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: _isWorking ? null : () => _reject(order), + icon: const Icon(Icons.cancel_outlined), + label: const Text('Reject'), + ), + ], + if (canEdit && order.canAmend) ...[ + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: _isWorking ? null : () => _amend(order), + icon: const Icon(Icons.history_edu_outlined), + label: const Text('Amend'), + ), + ], + if (canEdit && order.canCancel) ...[ + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: _isWorking ? null : () => _cancel(order), + icon: const Icon(Icons.block_outlined), + label: const Text('Cancel'), + ), + ], + if (canDelete && order.canDelete) ...[ + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: _isWorking ? null : _delete, + icon: const Icon(Icons.delete_outline), + label: const Text('Delete'), + ), + ], + ], + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + PoStatusChip(status: order.status), + if (order.revisionNo != null && order.revisionNo! > 0) + Chip(label: Text('Revision ${order.revisionNo}')), + ], + ), + const SizedBox(height: 16), + _OverviewCard(order: order), + const SizedBox(height: 16), + _LineItemsCard(items: order.items), + ], + ), + ), + ), + ); + } + + Future _runWorkflow(Future Function() action, String success) async { + setState(() => _isWorking = true); + try { + await action(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(success))); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } finally { + if (mounted) setState(() => _isWorking = false); + } + } + + Future _submit(PurchaseOrderModel order) async { + await _runWorkflow( + () => ref + .read(purchaseOrderDetailProvider(widget.purchaseOrderId).notifier) + .submit(), + 'Purchase order submitted for approval', + ); + } + + Future _approve(PurchaseOrderModel order) async { + await _runWorkflow( + () => ref + .read(purchaseOrderDetailProvider(widget.purchaseOrderId).notifier) + .approve(), + 'Purchase order approved', + ); + } + + Future _reject(PurchaseOrderModel order) async { + final remarksController = TextEditingController(); + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Reject Purchase Order'), + content: AppTextField( + controller: remarksController, + label: 'Remarks *', + maxLines: 3, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Reject'), + ), + ], + ), + ); + if (confirmed != true || !mounted) return; + final remarks = remarksController.text.trim(); + remarksController.dispose(); + if (remarks.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Rejection remarks are required')), + ); + return; + } + await _runWorkflow( + () => ref + .read(purchaseOrderDetailProvider(widget.purchaseOrderId).notifier) + .reject(remarks: remarks), + 'Purchase order rejected', + ); + } + + Future _amend(PurchaseOrderModel order) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Amend Purchase Order', + message: + 'This will create a new draft revision. Continue?', + confirmLabel: 'Amend', + ); + if (confirmed != true || !mounted) return; + + setState(() => _isWorking = true); + try { + final amended = await ref + .read(purchaseOrderDetailProvider(widget.purchaseOrderId).notifier) + .amend(); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Amendment draft created')), + ); + context.go('${RouteConstants.purchaseOrders}/${amended.id}'); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } finally { + if (mounted) setState(() => _isWorking = false); + } + } + + Future _cancel(PurchaseOrderModel order) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Cancel Purchase Order', + message: 'Cancel ${order.poNo ?? order.id}?', + confirmLabel: 'Cancel PO', + isDestructive: true, + ); + if (confirmed != true || !mounted) return; + await _runWorkflow( + () => ref + .read(purchaseOrderDetailProvider(widget.purchaseOrderId).notifier) + .cancel(), + 'Purchase order cancelled', + ); + } + + Future _delete() async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Delete Purchase Order', + message: 'Soft delete this purchase order?', + confirmLabel: 'Delete', + isDestructive: true, + ); + if (confirmed != true || !mounted) return; + + setState(() => _isWorking = true); + try { + final repository = ref.read(purchaseOrderRepositoryProvider); + final result = await repository.deletePurchaseOrder(widget.purchaseOrderId); + if (result.failure != null) throw result.failure!; + ref.invalidate(purchaseOrdersListProvider); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Purchase order deleted')), + ); + context.go(RouteConstants.purchaseOrders); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } finally { + if (mounted) setState(() => _isWorking = false); + } + } + + Future _downloadPdf(PurchaseOrderModel order) async { + setState(() => _isWorking = true); + try { + final bytes = await ref + .read(purchaseOrderDetailProvider(widget.purchaseOrderId).notifier) + .downloadPdf(); + if (bytes.isEmpty) throw Exception('Empty PDF response'); + await downloadFile( + bytes: bytes, + fileName: '${order.poNo ?? 'PO-${order.id}'}.pdf', + ); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } finally { + if (mounted) setState(() => _isWorking = false); + } + } +} + +class _OverviewCard extends StatelessWidget { + const _OverviewCard({required this.order}); + + final PurchaseOrderModel order; + + @override + Widget build(BuildContext context) { + return AppCard( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Overview', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + _infoGrid([ + _Info('PO Date', DateFormatter.displayDate(order.poDate)), + _Info('Expected Delivery', + DateFormatter.displayDate(order.expectedDeliveryDate)), + _Info('Vendor', order.vendorName ?? '—'), + _Info('Plant', order.plantName ?? '—'), + _Info('Warehouse', order.warehouseName ?? '—'), + _Info('Type', poTypeLabel(order.poType)), + _Info('Taxable', CurrencyFormatter.format(order.taxableAmount)), + _Info('Tax', CurrencyFormatter.format(order.taxAmount)), + _Info('Freight', CurrencyFormatter.format(order.freightCharges)), + _Info('Other Charges', CurrencyFormatter.format(order.otherCharges)), + _Info('Discount', CurrencyFormatter.format(order.discountAmount)), + _Info('Total', CurrencyFormatter.format(order.totalAmount)), + ]), + if (order.termsAndConditions?.isNotEmpty == true) ...[ + const SizedBox(height: 16), + Text('Terms & Conditions', + style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 4), + Text(order.termsAndConditions!), + ], + if (order.remarks?.isNotEmpty == true) ...[ + const SizedBox(height: 16), + Text('Remarks', style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 4), + Text(order.remarks!), + ], + ], + ), + ), + ); + } + + Widget _infoGrid(List<_Info> rows) { + return LayoutBuilder( + builder: (context, constraints) { + final crossAxisCount = constraints.maxWidth > 900 ? 3 : 2; + return Wrap( + spacing: 24, + runSpacing: 12, + children: rows + .map( + (row) => SizedBox( + width: (constraints.maxWidth / crossAxisCount) - 24, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + row.label, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + Text(row.value), + ], + ), + ), + ) + .toList(), + ); + }, + ); + } +} + +class _Info { + const _Info(this.label, this.value); + final String label; + final String value; +} + +class _LineItemsCard extends StatelessWidget { + const _LineItemsCard({required this.items}); + + final List items; + + @override + Widget build(BuildContext context) { + return AppCard( + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Line Items', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + if (items.isEmpty) + Text( + 'No line items', + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ) + else + AppDataTable( + wrapInCard: false, + shrinkWrap: true, + columns: [ + AppDataColumn( + label: '#', + flex: 1, + cellBuilder: (_, item) => Text('${item.lineNo ?? '—'}'), + ), + AppDataColumn( + label: 'Item', + flex: 3, + cellBuilder: (_, item) => Text( + item.itemName ?? item.itemCode ?? '—', + ), + ), + AppDataColumn( + label: 'Qty', + flex: 1, + cellBuilder: (_, item) => Text('${item.orderedQty ?? '—'}'), + ), + AppDataColumn( + label: 'UOM', + flex: 1, + cellBuilder: (_, item) => Text(item.uomName ?? '—'), + ), + AppDataColumn( + label: 'Rate', + flex: 1, + cellBuilder: (_, item) => + Text(CurrencyFormatter.format(item.rate)), + ), + AppDataColumn( + label: 'Amount', + flex: 1, + cellBuilder: (_, item) => + Text(CurrencyFormatter.format(item.lineAmount)), + ), + ], + rows: items, + ), + ], + ), + ), + ); + } +} diff --git a/lib/modules/purchase_orders/presentation/screens/purchase_order_form_screen.dart b/lib/modules/purchase_orders/presentation/screens/purchase_order_form_screen.dart new file mode 100644 index 0000000..e595d47 --- /dev/null +++ b/lib/modules/purchase_orders/presentation/screens/purchase_order_form_screen.dart @@ -0,0 +1,593 @@ +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/network/api_handler.dart'; +import '../../../../core/utils/formatters.dart'; +import '../../../../shared/models/purchase_order_model.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/utils/navigation_utils.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 '../../../../shared/widgets/app_side_panel.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../../data/repositories/purchase_order_repository_impl.dart'; +import '../providers/purchase_order_lookups_provider.dart'; +import '../providers/purchase_orders_provider.dart'; +import '../widgets/purchase_order_line_items_editor.dart'; + +class PurchaseOrderFormScreen extends ConsumerStatefulWidget { + const PurchaseOrderFormScreen({super.key, this.purchaseOrderId}); + + final String? purchaseOrderId; + + bool get isEditing => purchaseOrderId != null; + + @override + ConsumerState createState() => + _PurchaseOrderFormScreenState(); +} + +class _PurchaseOrderFormScreenState extends ConsumerState { + final _formKey = GlobalKey(); + final _scrollController = ScrollController(); + final _discountController = TextEditingController(); + final _freightController = TextEditingController(); + final _otherChargesController = TextEditingController(); + final _termsController = TextEditingController(); + final _remarksController = TextEditingController(); + + DateTime? _poDate; + DateTime? _expectedDeliveryDate; + String? _poType; + int? _vendorId; + int? _plantId; + int? _warehouseId; + int? _brandId; + int? _paymentTermId; + int? _deliveryTermId; + final List _lines = []; + bool _isSubmitting = false; + String? _populatedSignature; + + @override + void initState() { + super.initState(); + if (!widget.isEditing) { + _poDate = DateTime.now(); + _lines.add(PoLineItemDraft(lineNo: 1)); + } + } + + @override + void dispose() { + _scrollController.dispose(); + _discountController.dispose(); + _freightController.dispose(); + _otherChargesController.dispose(); + _termsController.dispose(); + _remarksController.dispose(); + for (final line in _lines) { + line.dispose(); + } + super.dispose(); + } + + String _orderSignature(PurchaseOrderModel order) => + '${order.id}:${order.updatedAt?.toIso8601String()}:${order.items.length}'; + + void _populateFromOrder(PurchaseOrderModel order) { + setState(() { + _poDate = order.poDate ?? DateTime.now(); + _expectedDeliveryDate = order.expectedDeliveryDate; + _poType = order.poType; + _vendorId = order.vendorId; + _plantId = order.plantId; + _warehouseId = order.warehouseId; + _brandId = order.brandId; + _paymentTermId = order.paymentTermId; + _deliveryTermId = order.deliveryTermId; + _discountController.text = order.discountAmount?.toString() ?? ''; + _freightController.text = order.freightCharges?.toString() ?? ''; + _otherChargesController.text = order.otherCharges?.toString() ?? ''; + _termsController.text = order.termsAndConditions ?? ''; + _remarksController.text = order.remarks ?? ''; + for (final line in _lines) { + line.dispose(); + } + _lines + ..clear() + ..addAll( + order.items.isNotEmpty + ? order.items.map(PoLineItemDraft.fromModel).toList() + : [PoLineItemDraft(lineNo: 1)], + ); + }); + } + + void _addLine() { + setState(() { + _lines.add(PoLineItemDraft(lineNo: _lines.length + 1)); + }); + } + + void _removeLine(int index) { + setState(() { + _lines[index].dispose(); + _lines.removeAt(index); + for (var i = 0; i < _lines.length; i++) { + _lines[i].lineNo = i + 1; + } + }); + } + + int? _dropdownValue(int? selected, Iterable validIds) { + if (selected == null) return null; + return validIds.contains(selected) ? selected : null; + } + + int? _parseId(String value) => int.tryParse(value.trim()); + + List> _intOptions(List options) { + return options + .map((e) { + final id = _parseId(e.id); + if (id == null) return null; + return AppDropdownOption(value: id, label: e.name); + }) + .whereType>() + .toList(); + } + + List> _nullableIntOptions(List options) { + return [ + const AppDropdownOption(value: null, label: 'None'), + ...options.map( + (e) { + final id = _parseId(e.id); + if (id == null) return null; + return AppDropdownOption(value: id, label: e.name); + }, + ), + ].whereType>().toList(); + } + + Map _buildPayload() { + return { + 'po_date': DateFormatter.toApiDate(_poDate ?? DateTime.now()), + 'po_type': _poType, + 'vendor_id': _vendorId, + 'plant_id': _plantId, + if (_warehouseId != null) 'warehouse_id': _warehouseId, + if (_brandId != null) 'brand_id': _brandId, + if (_paymentTermId != null) 'payment_term_id': _paymentTermId, + if (_deliveryTermId != null) 'delivery_term_id': _deliveryTermId, + if (_expectedDeliveryDate != null) + 'expected_delivery_date': + DateFormatter.toApiDate(_expectedDeliveryDate!), + if (_discountController.text.trim().isNotEmpty) + 'discount_amount': double.tryParse(_discountController.text.trim()), + if (_freightController.text.trim().isNotEmpty) + 'freight_charges': double.tryParse(_freightController.text.trim()), + if (_otherChargesController.text.trim().isNotEmpty) + 'other_charges': double.tryParse(_otherChargesController.text.trim()), + if (_termsController.text.trim().isNotEmpty) + 'terms_and_conditions': _termsController.text.trim(), + if (_remarksController.text.trim().isNotEmpty) + 'remarks': _remarksController.text.trim(), + 'items': _lines.map((line) => line.toPayload()).toList(), + }; + } + + String? _lineItemsError() { + for (final line in _lines) { + if (line.itemId == null) return 'Each line item must have an item selected'; + if (line.uomId == null) return 'Each line item must have a UOM selected'; + final qty = double.tryParse(line.qtyController.text.trim()); + if (qty == null || qty <= 0) { + return 'Enter a valid quantity for line ${line.lineNo}'; + } + final rate = double.tryParse(line.rateController.text.trim()); + if (rate == null || rate < 0) { + return 'Enter a valid rate for line ${line.lineNo}'; + } + } + return null; + } + + Future _submit() async { + final formState = _formKey.currentState; + if (formState == null) return; + + final isValid = formState.validate(); + if (!isValid) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please fix the highlighted errors before saving'), + ), + ); + return; + } + + if (_poType == null || _vendorId == null || _plantId == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please complete all required fields')), + ); + return; + } + + final lineItemsError = _lineItemsError(); + if (lineItemsError != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(lineItemsError)), + ); + return; + } + + if (_lines.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Add at least one line item')), + ); + return; + } + + setState(() => _isSubmitting = true); + try { + final payload = _buildPayload(); + final PurchaseOrderModel saved; + if (widget.isEditing) { + final notifier = + ref.read(purchaseOrderFormProvider(widget.purchaseOrderId).notifier); + saved = await notifier.submitUpdate(widget.purchaseOrderId!, payload); + } else { + final result = + await ref.read(purchaseOrderRepositoryProvider).createPurchaseOrder(payload); + if (result.failure != null) throw result.failure!; + saved = result.data!; + ref.invalidate(purchaseOrdersListProvider); + } + + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + widget.isEditing ? 'Purchase order updated' : 'Purchase order created', + ), + ), + ); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final destination = widget.isEditing + ? '${RouteConstants.purchaseOrders}/${saved.id}' + : RouteConstants.purchaseOrders; + goAndDismissOverlays(context, destination); + }); + } catch (e) { + if (!mounted) return; + final message = e is Failure + ? validationErrorMessage(e) + : e.toString(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message)), + ); + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + Future _pickDate({ + required DateTime? current, + required ValueChanged onPicked, + }) async { + final picked = await showDatePicker( + context: context, + initialDate: current ?? DateTime.now(), + firstDate: DateTime(2020), + lastDate: DateTime(2100), + ); + if (picked != null) onPicked(picked); + } + + Widget _buildFormBody({ + required PurchaseOrderLookups lookups, + PurchaseOrderModel? existing, + }) { + if (widget.isEditing && existing != null) { + final signature = _orderSignature(existing); + if (_populatedSignature != signature) { + _populatedSignature = signature; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _populateFromOrder(existing); + }); + } + } + + if (widget.isEditing && existing != null && !existing.canEdit) { + return ErrorView.fromFailure( + const Failure.validation( + message: 'This purchase order cannot be edited', + ), + onRetry: () => context.go( + '${RouteConstants.purchaseOrders}/${existing.id}', + ), + ); + } + + final vendorIds = + lookups.vendors.map((e) => _parseId(e.id)).whereType(); + final plantIds = + lookups.plants.map((e) => _parseId(e.id)).whereType(); + + return SingleChildScrollView( + controller: _scrollController, + padding: const EdgeInsets.all(24), + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 1200), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (!widget.isEditing) + PageHeader( + title: 'Create Purchase Order', + subtitle: 'Fill header details and add line items', + ) + else if (existing?.poNo != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + existing!.poNo!, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + FormRowThree( + children: [ + _DateField( + label: 'PO Date *', + value: _poDate, + onTap: () => _pickDate( + current: _poDate, + onPicked: (d) => setState(() => _poDate = d), + ), + ), + AppSearchableDropdown( + label: 'PO Type *', + value: _poType, + searchHint: 'Search type...', + options: poTypeOptions + .map( + (e) => + AppDropdownOption(value: e.$1, label: e.$2), + ) + .toList(), + onChanged: (v) => setState(() => _poType = v), + validator: (v) => + v == null ? 'PO type is required' : null, + ), + AppSearchableDropdown( + label: 'Vendor *', + value: _dropdownValue(_vendorId, vendorIds), + searchHint: 'Search vendor...', + options: _intOptions(lookups.vendors), + onChanged: (v) => setState(() => _vendorId = v), + validator: (v) => + v == null ? 'Vendor is required' : null, + ), + ], + ), + FormRowThree( + children: [ + AppSearchableDropdown( + label: 'Plant *', + value: _dropdownValue(_plantId, plantIds), + searchHint: 'Search plant...', + options: _intOptions(lookups.plants), + onChanged: (v) => setState(() => _plantId = v), + validator: (v) => + v == null ? 'Plant is required' : null, + ), + AppSearchableDropdown( + label: 'Warehouse', + value: _warehouseId, + searchHint: 'Search warehouse...', + options: _nullableIntOptions(lookups.warehouses), + onChanged: (v) => setState(() => _warehouseId = v), + ), + AppSearchableDropdown( + label: 'Brand', + value: _brandId, + searchHint: 'Search brand...', + options: _nullableIntOptions(lookups.brands), + onChanged: (v) => setState(() => _brandId = v), + ), + ], + ), + FormRowThree( + children: [ + AppSearchableDropdown( + label: 'Payment Term', + value: _paymentTermId, + searchHint: 'Search payment term...', + options: _nullableIntOptions(lookups.paymentTerms), + onChanged: (v) => setState(() => _paymentTermId = v), + ), + AppSearchableDropdown( + label: 'Delivery Term', + value: _deliveryTermId, + searchHint: 'Search delivery term...', + options: _nullableIntOptions(lookups.deliveryTerms), + onChanged: (v) => setState(() => _deliveryTermId = v), + ), + _DateField( + label: 'Expected Delivery', + value: _expectedDeliveryDate, + onTap: () => _pickDate( + current: _expectedDeliveryDate, + onPicked: (d) => + setState(() => _expectedDeliveryDate = d), + ), + ), + ], + ), + FormRowThree( + children: [ + AppTextField( + controller: _discountController, + label: 'Discount Amount', + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + ), + AppTextField( + controller: _freightController, + label: 'Freight Charges', + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + ), + AppTextField( + controller: _otherChargesController, + label: 'Other Charges', + keyboardType: const TextInputType.numberWithOptions( + decimal: true, + ), + ), + ], + ), + const SizedBox(height: 4), + AppTextField( + controller: _termsController, + label: 'Terms & Conditions', + maxLines: 3, + ), + const SizedBox(height: 12), + AppTextField( + controller: _remarksController, + label: 'Remarks', + maxLines: 2, + ), + const SizedBox(height: 24), + PurchaseOrderLineItemsEditor( + lines: _lines, + items: lookups.items, + uom: lookups.uom, + gstRates: lookups.gstRates, + onAddLine: _addLine, + onRemoveLine: _removeLine, + ), + const SizedBox(height: 24), + Row( + children: [ + OutlinedButton( + onPressed: _isSubmitting + ? null + : () => context.pop(), + child: const Text('Cancel'), + ), + const SizedBox(width: 12), + Expanded( + child: AppButton( + label: widget.isEditing ? 'Update PO' : 'Create PO', + expand: false, + isLoading: _isSubmitting, + onPressed: _isSubmitting ? null : _submit, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + final lookupsAsync = ref.watch(purchaseOrderLookupsProvider); + + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.surface, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.surface, + surfaceTintColor: Colors.transparent, + title: Text( + widget.isEditing ? 'Edit Purchase Order' : 'Create Purchase Order', + ), + ), + body: lookupsAsync.when( + loading: () => const AppLoadingView(message: 'Loading form options...'), + error: (e, _) => ErrorView.fromFailure( + e is Failure ? e : Failure.unknown(message: e.toString()), + onRetry: () => ref.invalidate(purchaseOrderLookupsProvider), + ), + data: (lookups) { + if (!widget.isEditing) { + return _buildFormBody(lookups: lookups); + } + + final formAsync = + ref.watch(purchaseOrderFormProvider(widget.purchaseOrderId)); + return formAsync.when( + loading: () => + const AppLoadingView(message: 'Loading purchase order...'), + error: (e, _) => ErrorView.fromFailure( + e is Failure ? e : Failure.unknown(message: e.toString()), + onRetry: () => ref.invalidate( + purchaseOrderFormProvider(widget.purchaseOrderId), + ), + ), + data: (existing) => + _buildFormBody(lookups: lookups, existing: existing), + ); + }, + ), + ); + } +} + +class _DateField extends StatelessWidget { + const _DateField({ + required this.label, + required this.value, + required this.onTap, + }); + + final String label; + final DateTime? value; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: InputDecorator( + decoration: InputDecoration( + labelText: label, + suffixIcon: const Icon(Icons.calendar_today_outlined), + ), + child: Text( + value != null ? DateFormatter.displayDate(value) : 'Select date', + ), + ), + ); + } +} diff --git a/lib/modules/purchase_orders/presentation/screens/purchase_order_list_screen.dart b/lib/modules/purchase_orders/presentation/screens/purchase_order_list_screen.dart new file mode 100644 index 0000000..57f0616 --- /dev/null +++ b/lib/modules/purchase_orders/presentation/screens/purchase_order_list_screen.dart @@ -0,0 +1,398 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/formatters.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/models/purchase_order_model.dart'; +import '../../../../shared/providers/permissions_provider.dart'; +import '../../../../shared/widgets/app_card.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/can_permission.dart'; +import '../../../../shared/widgets/app_table_action_icon.dart'; +import '../../../../shared/widgets/app_table_shell.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/purchase_orders_provider.dart'; +import '../widgets/po_status_chip.dart'; + +class PurchaseOrderListScreen extends ConsumerStatefulWidget { + const PurchaseOrderListScreen({super.key}); + + @override + ConsumerState createState() => + _PurchaseOrderListScreenState(); +} + +class _PurchaseOrderListScreenState extends ConsumerState { + final _searchController = TextEditingController(); + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final ordersAsync = ref.watch(purchaseOrdersListProvider); + final canEdit = ref.can('purchase_orders', PermissionAction.update); + final canDelete = ref.can('purchase_orders', PermissionAction.delete); + + ref.listen(purchaseOrdersListProvider, (prev, next) { + final error = next.valueOrNull?.actionError; + final success = next.valueOrNull?.actionSuccess; + if (error != null && error != prev?.valueOrNull?.actionError) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); + } + if (success != null && success != prev?.valueOrNull?.actionSuccess) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(success))); + } + }); + + return Padding( + padding: const EdgeInsets.all(24), + child: ordersAsync.when( + loading: () => const AppLoadingView(message: 'Loading purchase orders...'), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(purchaseOrdersListProvider), + ), + data: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Purchase Orders', + subtitle: 'Create, approve and track procurement orders', + actions: [ + CanPermission( + module: 'purchase_orders', + action: PermissionAction.create, + child: ElevatedButton.icon( + onPressed: () => context.go(RouteConstants.purchaseOrderAdd), + icon: const Icon(Icons.add), + label: const Text('Create PO'), + ), + ), + ], + ), + const SizedBox(height: 16), + Expanded( + child: AppTableShell( + toolbar: LayoutBuilder( + builder: (context, constraints) { + return _FiltersBar( + searchController: _searchController, + query: state.query, + wrapped: constraints.maxWidth < 900, + onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch, + onStatusChanged: + ref.read(purchaseOrdersListProvider.notifier).setStatusFilter, + onPoTypeChanged: + ref.read(purchaseOrdersListProvider.notifier).setPoTypeFilter, + ); + }, + ), + footer: AppPagination( + currentPage: state.query.page, + totalPages: state.totalPages, + totalItems: state.total, + pageSize: state.query.limit, + onPageChanged: + ref.read(purchaseOrdersListProvider.notifier).setPage, + onPageSizeChanged: + ref.read(purchaseOrdersListProvider.notifier).setPageSize, + ), + child: RefreshIndicator( + onRefresh: () => + ref.read(purchaseOrdersListProvider.notifier).refresh(), + child: state.orders.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const [ + SizedBox( + height: 240, + child: AppEmptyState( + title: 'No purchase orders found', + description: + 'Try adjusting filters or create a new purchase order.', + icon: Icons.receipt_long_outlined, + ), + ), + ], + ) + : context.isMobile + ? _PoCardList( + orders: state.orders, + onView: _viewOrder, + onEdit: canEdit ? _editOrder : null, + onDelete: canDelete ? _deleteOrder : null, + ) + : _PoDataTable( + orders: state.orders, + onView: _viewOrder, + onEdit: canEdit ? _editOrder : null, + onDelete: canDelete ? _deleteOrder : null, + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _viewOrder(PurchaseOrderModel order) { + context.push('${RouteConstants.purchaseOrders}/${order.id}'); + } + + void _editOrder(PurchaseOrderModel order) { + if (!order.canEdit) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Only draft or rejected orders can be edited'), + ), + ); + return; + } + context.push('${RouteConstants.purchaseOrders}/${order.id}/edit'); + } + + Future _deleteOrder(PurchaseOrderModel order) async { + if (!order.canDelete) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Only draft or rejected orders can be deleted'), + ), + ); + return; + } + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Delete Purchase Order', + message: 'Delete ${order.poNo ?? order.id}?', + confirmLabel: 'Delete', + isDestructive: true, + ); + if (confirmed != true || !mounted) return; + await ref.read(purchaseOrdersListProvider.notifier).deletePurchaseOrder(order.id); + } +} + +class _FiltersBar extends StatelessWidget { + const _FiltersBar({ + required this.searchController, + required this.query, + required this.wrapped, + required this.onSearch, + required this.onStatusChanged, + required this.onPoTypeChanged, + }); + + final TextEditingController searchController; + final PurchaseOrderListQuery query; + final bool wrapped; + final ValueChanged onSearch; + final ValueChanged onStatusChanged; + final ValueChanged onPoTypeChanged; + + @override + Widget build(BuildContext context) { + final searchField = SizedBox( + width: wrapped ? double.infinity : null, + child: AppSearchField( + controller: searchController, + hint: 'Search PO number, vendor...', + onChanged: onSearch, + ), + ); + + final filters = [ + SizedBox( + width: wrapped ? double.infinity : 180, + child: AppSearchableDropdown( + label: 'Status', + value: query.status, + searchHint: 'Search status...', + isDense: true, + options: [ + const AppDropdownOption(value: null, label: 'All statuses'), + ...poStatusOptions.map( + (e) => AppDropdownOption(value: e.$1, label: e.$2), + ), + ], + onChanged: onStatusChanged, + ), + ), + SizedBox( + width: wrapped ? double.infinity : 200, + child: AppSearchableDropdown( + label: 'PO Type', + value: query.poType, + searchHint: 'Search type...', + isDense: true, + options: [ + const AppDropdownOption(value: null, label: 'All types'), + ...poTypeOptions.map( + (e) => AppDropdownOption(value: e.$1, label: e.$2), + ), + ], + onChanged: onPoTypeChanged, + ), + ), + ]; + + if (wrapped) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + searchField, + const SizedBox(height: 12), + ...filters.expand((f) => [f, const SizedBox(height: 12)]).toList()..removeLast(), + ], + ); + } + + 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]), + ], + ); + } +} + +class _PoDataTable extends StatelessWidget { + const _PoDataTable({ + required this.orders, + required this.onView, + this.onEdit, + this.onDelete, + }); + + final List orders; + final ValueChanged onView; + final ValueChanged? onEdit; + final ValueChanged? onDelete; + + @override + Widget build(BuildContext context) { + return AppDataTable( + wrapInCard: false, + columns: [ + AppDataColumn( + label: 'PO Number', + flex: 2, + cellBuilder: (_, order) => Text(order.poNo ?? '—'), + ), + AppDataColumn( + label: 'Date', + flex: 1, + cellBuilder: (_, order) => Text(DateFormatter.displayDate(order.poDate)), + ), + AppDataColumn( + label: 'Vendor', + flex: 2, + cellBuilder: (_, order) => Text(order.vendorName ?? '—'), + ), + AppDataColumn( + label: 'Plant', + flex: 2, + cellBuilder: (_, order) => Text(order.plantName ?? '—'), + ), + AppDataColumn( + label: 'Type', + flex: 2, + cellBuilder: (_, order) => Text(poTypeLabel(order.poType)), + ), + AppDataColumn( + label: 'Total', + flex: 1, + cellBuilder: (_, order) => + Text(CurrencyFormatter.format(order.totalAmount)), + ), + AppDataColumn( + label: 'Status', + flex: 1, + cellBuilder: (_, order) => PoStatusChip(status: order.status), + ), + AppDataColumn( + label: 'Actions', + flex: 1, + alignment: Alignment.centerRight, + cellBuilder: (_, order) => AppTableActions( + children: [ + AppTableActionIcon( + tooltip: 'View', + icon: Icons.visibility_outlined, + onPressed: () => onView(order), + ), + if (onEdit != null && order.canEdit) + AppTableActionIcon( + tooltip: 'Edit', + icon: Icons.edit_outlined, + onPressed: () => onEdit!(order), + ), + if (onDelete != null && order.canDelete) + AppTableActionIcon( + tooltip: 'Delete', + icon: Icons.delete_outline, + onPressed: () => onDelete!(order), + ), + ], + ), + ), + ], + rows: orders, + ); + } +} + +class _PoCardList extends StatelessWidget { + const _PoCardList({ + required this.orders, + required this.onView, + this.onEdit, + this.onDelete, + }); + + final List orders; + final ValueChanged onView; + final ValueChanged? onEdit; + final ValueChanged? onDelete; + + @override + Widget build(BuildContext context) { + return ListView.separated( + itemCount: orders.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final order = orders[index]; + return AppCard( + child: ListTile( + title: Text(order.poNo ?? 'PO #${order.id}'), + subtitle: Text( + '${order.vendorName ?? '—'} · ${poTypeLabel(order.poType)}', + ), + trailing: PoStatusChip(status: order.status, compact: true), + onTap: () => onView(order), + ), + ); + }, + ); + } +} diff --git a/lib/modules/purchase_orders/presentation/widgets/po_status_chip.dart b/lib/modules/purchase_orders/presentation/widgets/po_status_chip.dart new file mode 100644 index 0000000..efcffe6 --- /dev/null +++ b/lib/modules/purchase_orders/presentation/widgets/po_status_chip.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; + +import '../../../../shared/models/purchase_order_model.dart'; + +class PoStatusChip extends StatelessWidget { + const PoStatusChip({ + 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.toUpperCase()) { + case 'DRAFT': + return (Colors.blueGrey.shade700, poStatusLabel(raw)); + case 'SUBMITTED': + case 'PENDING_APPROVAL': + case 'PENDING': + return (Colors.orange.shade800, poStatusLabel(raw)); + case 'APPROVED': + return (Colors.green.shade700, poStatusLabel(raw)); + case 'REJECTED': + return (Colors.red.shade700, poStatusLabel(raw)); + case 'CANCELLED': + return (Colors.grey.shade700, poStatusLabel(raw)); + case 'PARTIALLY_RECEIVED': + return (Colors.teal.shade700, poStatusLabel(raw)); + case 'FULLY_RECEIVED': + return (Colors.indigo.shade700, poStatusLabel(raw)); + default: + return (Colors.blueGrey, poStatusLabel(raw)); + } + } +} diff --git a/lib/modules/purchase_orders/presentation/widgets/purchase_order_line_items_editor.dart b/lib/modules/purchase_orders/presentation/widgets/purchase_order_line_items_editor.dart new file mode 100644 index 0000000..15a1dd2 --- /dev/null +++ b/lib/modules/purchase_orders/presentation/widgets/purchase_order_line_items_editor.dart @@ -0,0 +1,297 @@ +import 'package:flutter/material.dart'; + +import '../../../../shared/models/purchase_order_model.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; +import '../../../../shared/widgets/app_side_panel.dart'; +import '../../../../shared/widgets/app_text_field.dart'; + +class PoLineItemDraft { + PoLineItemDraft({ + this.itemId, + required this.lineNo, + TextEditingController? qtyController, + this.uomId, + TextEditingController? rateController, + TextEditingController? discountController, + this.gstRateId, + TextEditingController? remarksController, + }) : qtyController = qtyController ?? TextEditingController(), + rateController = rateController ?? TextEditingController(), + discountController = discountController ?? TextEditingController(text: '0'), + remarksController = remarksController ?? TextEditingController(); + + int? itemId; + int lineNo; + final TextEditingController qtyController; + int? uomId; + final TextEditingController rateController; + final TextEditingController discountController; + int? gstRateId; + final TextEditingController remarksController; + + factory PoLineItemDraft.fromModel(PurchaseOrderItemModel item) { + return PoLineItemDraft( + itemId: item.itemId, + lineNo: item.lineNo ?? 1, + qtyController: TextEditingController(text: item.orderedQty?.toString() ?? ''), + uomId: item.uomId, + rateController: TextEditingController(text: item.rate?.toString() ?? ''), + discountController: + TextEditingController(text: item.discountPct?.toString() ?? '0'), + gstRateId: item.gstRateId, + remarksController: TextEditingController(text: item.remarks ?? ''), + ); + } + + void dispose() { + qtyController.dispose(); + rateController.dispose(); + discountController.dispose(); + remarksController.dispose(); + } + + Map toPayload() { + final qty = double.tryParse(qtyController.text.trim()); + final rate = double.tryParse(rateController.text.trim()); + if (qty == null || rate == null) { + throw const FormatException('Invalid line item quantity or rate'); + } + return { + 'item_id': itemId, + 'line_no': lineNo, + 'ordered_qty': qty, + 'uom_id': uomId, + 'rate': rate, + 'discount_pct': double.tryParse(discountController.text.trim()) ?? 0, + if (gstRateId != null) 'gst_rate_id': gstRateId, + if (remarksController.text.trim().isNotEmpty) + 'remarks': remarksController.text.trim(), + }; + } +} + +class PurchaseOrderLineItemsEditor extends StatefulWidget { + const PurchaseOrderLineItemsEditor({ + super.key, + required this.lines, + required this.items, + required this.uom, + required this.gstRates, + required this.onAddLine, + required this.onRemoveLine, + }); + + final List lines; + final List items; + final List uom; + final List gstRates; + final VoidCallback onAddLine; + final ValueChanged onRemoveLine; + + @override + State createState() => + _PurchaseOrderLineItemsEditorState(); +} + +class _PurchaseOrderLineItemsEditorState extends State { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('Line Items', style: theme.textTheme.titleMedium), + const Spacer(), + TextButton.icon( + onPressed: widget.onAddLine, + icon: const Icon(Icons.add), + label: const Text('Add line'), + ), + ], + ), + const SizedBox(height: 8), + if (widget.lines.isEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + border: Border.all(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + 'Add at least one line item', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ) + else + ...widget.lines.asMap().entries.map((entry) { + final index = entry.key; + final line = entry.value; + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: _LineItemCard( + key: ValueKey('po-line-${line.lineNo}-$index'), + line: line, + items: widget.items, + uom: widget.uom, + gstRates: widget.gstRates, + onRemove: widget.lines.length > 1 + ? () => widget.onRemoveLine(index) + : null, + ), + ); + }), + ], + ); + } +} + +class _LineItemCard extends StatelessWidget { + const _LineItemCard({ + super.key, + required this.line, + required this.items, + required this.uom, + required this.gstRates, + this.onRemove, + }); + + final PoLineItemDraft line; + final List items; + final List uom; + final List gstRates; + final VoidCallback? onRemove; + + int? _parseId(String value) => int.tryParse(value.trim()); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final itemOptions = items + .map((e) { + final id = _parseId(e.id); + if (id == null) return null; + return AppDropdownOption(value: id, label: e.name); + }) + .whereType>() + .toList(); + final uomOptions = uom + .map((e) { + final id = _parseId(e.id); + if (id == null) return null; + return AppDropdownOption(value: id, label: e.name); + }) + .whereType>() + .toList(); + final gstOptions = [ + const AppDropdownOption(value: null, label: 'No GST'), + ...gstRates.map((e) { + final id = _parseId(e.id); + if (id == null) return null; + return AppDropdownOption(value: id, label: e.name); + }), + ].whereType>().toList(); + + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Text('Line ${line.lineNo}', style: theme.textTheme.titleSmall), + const Spacer(), + if (onRemove != null) + IconButton( + tooltip: 'Remove line', + icon: const Icon(Icons.delete_outline), + onPressed: onRemove, + ), + ], + ), + const SizedBox(height: 12), + FormRowThree( + children: [ + AppSearchableDropdown( + label: 'Item *', + value: line.itemId, + searchHint: 'Search item...', + options: itemOptions, + onChanged: (v) => line.itemId = v, + validator: (v) => v == null ? 'Item is required' : null, + ), + AppTextField( + controller: line.qtyController, + label: 'Quantity *', + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + validator: (v) { + if (v == null || v.trim().isEmpty) { + return 'Quantity is required'; + } + final qty = double.tryParse(v); + if (qty == null || qty <= 0) return 'Enter a valid quantity'; + return null; + }, + ), + AppSearchableDropdown( + label: 'UOM *', + value: line.uomId, + searchHint: 'Search UOM...', + options: uomOptions, + onChanged: (v) => line.uomId = v, + validator: (v) => v == null ? 'UOM is required' : null, + ), + ], + ), + FormRowThree( + children: [ + AppTextField( + controller: line.rateController, + label: 'Rate *', + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + validator: (v) { + if (v == null || v.trim().isEmpty) return 'Rate is required'; + final rate = double.tryParse(v); + if (rate == null || rate < 0) return 'Enter a valid rate'; + return null; + }, + ), + AppTextField( + controller: line.discountController, + label: 'Discount %', + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + ), + AppSearchableDropdown( + label: 'GST Rate', + value: line.gstRateId, + searchHint: 'Search GST rate...', + options: gstOptions, + onChanged: (v) => line.gstRateId = v, + ), + ], + ), + const SizedBox(height: 12), + AppTextField( + controller: line.remarksController, + label: 'Remarks', + ), + ], + ), + ); + } +} diff --git a/lib/modules/rbac/presentation/screens/users_role_management_screen.dart b/lib/modules/rbac/presentation/screens/users_role_management_screen.dart index 33ffb7a..ce94a59 100644 --- a/lib/modules/rbac/presentation/screens/users_role_management_screen.dart +++ b/lib/modules/rbac/presentation/screens/users_role_management_screen.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../../core/utils/formatters.dart'; import '../../../../core/constants/enums.dart'; import '../../../../core/constants/route_constants.dart'; @@ -12,6 +11,7 @@ import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/utils/file_download_helper.dart'; import '../../../../shared/providers/permissions_provider.dart'; import '../../../../shared/widgets/app_confirmation_dialog.dart'; +import '../../../users/presentation/widgets/user_rich_data_table.dart'; import '../../../../shared/widgets/app_dropdown.dart'; import '../../../../shared/widgets/app_card.dart'; import '../../../../shared/widgets/app_loading_view.dart'; @@ -466,17 +466,10 @@ class _UsersTab extends ConsumerStatefulWidget { } class _UsersTabState extends ConsumerState<_UsersTab> { - static const _tableMinWidth = 1120.0; - String? _selectedRoleName; String? _selectedDepartmentName; String? _selectedStatusLabel; - String _formatLastLogin(DateTime? value) { - if (value == null) return '—'; - return DateFormatter.displayDateTime(value); - } - int? _roleIdForFilter(UserFiltersModel? filters) { if (_selectedRoleName == null || filters == null) return null; for (final role in filters.roles) { @@ -729,102 +722,18 @@ class _UsersTabState extends ConsumerState<_UsersTab> { ) else Expanded( - child: LayoutBuilder( - builder: (context, constraints) { - final tableWidth = constraints.maxWidth < _tableMinWidth - ? _tableMinWidth - : constraints.maxWidth; - - return SingleChildScrollView( - scrollDirection: Axis.horizontal, - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ConstrainedBox( - constraints: BoxConstraints(minWidth: tableWidth), - child: DataTable( - horizontalMargin: 20, - columnSpacing: 16, - 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: UserTableActionsHeader(), - ), - ], - 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( - UserTableActionsCell( - child: UserTableActions( - user: user, - canEdit: canEditUser, - canResetPassword: canEditUser, - canDeactivate: canDeleteUser, - onEdit: () => _editUser(user), - onResetPassword: () => - _resetPassword(user), - onDeactivate: () => - _deactivateUser(user), - ), - ), - ), - ], - ); - }).toList(), - ), - ), - ); - }, + child: UserRichDataTable( + wrapInCard: false, + users: usersState.users, + actionsBuilder: (_, user) => UserTableActions( + user: user, + canEdit: canEditUser, + canResetPassword: canEditUser, + canDeactivate: canDeleteUser, + onEdit: () => _editUser(user), + onResetPassword: () => _resetPassword(user), + onDeactivate: () => _deactivateUser(user), + ), ), ), const Divider(height: 1), diff --git a/lib/modules/rbac/presentation/widgets/add_user_panel.dart b/lib/modules/rbac/presentation/widgets/add_user_panel.dart index 1063363..3733b1f 100644 --- a/lib/modules/rbac/presentation/widgets/add_user_panel.dart +++ b/lib/modules/rbac/presentation/widgets/add_user_panel.dart @@ -265,6 +265,8 @@ class _AddUserPanelState extends ConsumerState { label: 'Mobile', hint: '9XXXXXXXXX', keyboardType: TextInputType.phone, + validator: Validators.optionalMobile, + inputFormatters: Validators.mobileInput, ), ), const SizedBox(height: 12), diff --git a/lib/modules/rbac/presentation/widgets/rbac_widgets.dart b/lib/modules/rbac/presentation/widgets/rbac_widgets.dart index 9a9052f..5f17ca6 100644 --- a/lib/modules/rbac/presentation/widgets/rbac_widgets.dart +++ b/lib/modules/rbac/presentation/widgets/rbac_widgets.dart @@ -151,6 +151,72 @@ class RoleBadge extends StatelessWidget { } } +class EmployeeCodeBadge extends StatelessWidget { + const EmployeeCodeBadge({super.key, required this.code}); + + final String code; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.7), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + code, + style: theme.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w600, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ); + } +} + +class UserTableUserCell extends StatelessWidget { + const UserTableUserCell({super.key, required this.user}); + + final ManagedUserModel user; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Row( + children: [ + UserAvatarChip( + name: user.fullName, + initials: user.initialsDisplay, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + user.fullName, + style: const TextStyle(fontWeight: FontWeight.w600), + overflow: TextOverflow.ellipsis, + ), + Text( + user.email, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ); + } +} + class UserAvatarChip extends StatelessWidget { const UserAvatarChip({super.key, required this.name, this.initials}); diff --git a/lib/modules/roles/presentation/screens/role_list_screen.dart b/lib/modules/roles/presentation/screens/role_list_screen.dart index c546951..7850d4a 100644 --- a/lib/modules/roles/presentation/screens/role_list_screen.dart +++ b/lib/modules/roles/presentation/screens/role_list_screen.dart @@ -10,6 +10,7 @@ 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/app_table_shell.dart'; import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../providers/roles_provider.dart'; @@ -52,31 +53,37 @@ class _RoleListScreenState extends ConsumerState { 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), + child: AppTableShell( + toolbar: SizedBox( + width: context.isMobile ? double.infinity : 320, + child: AppSearchField( + controller: _searchController, + hint: 'Search roles...', + onChanged: ref.read(rolesListProvider.notifier).setSearch, + ), + ), + child: RefreshIndicator( + onRefresh: () => ref.read(rolesListProvider.notifier).refresh(), + child: roles.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const [ + SizedBox( + height: 240, + child: 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), + ), ), ), ], @@ -100,21 +107,29 @@ class _RoleDataTable extends StatelessWidget { @override Widget build(BuildContext context) { return AppDataTable( + wrapInCard: false, columns: [ - AppDataColumn(label: 'Role Name', cellBuilder: (_, r) => Text(r.name)), + AppDataColumn(label: 'Role Name', flex: 2, cellBuilder: (_, r) => Text(r.name)), AppDataColumn( label: 'Description', + flex: 3, cellBuilder: (_, r) => Text(r.description ?? '—'), ), AppDataColumn( label: 'Users Count', + flex: 1, cellBuilder: (_, r) => Text('${r.userCount}'), ), AppDataColumn( label: 'Actions', - cellBuilder: (_, r) => TextButton( - onPressed: () => onOpen(r), - child: const Text('View Matrix'), + flex: 1, + alignment: Alignment.centerRight, + cellBuilder: (_, r) => Align( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () => onOpen(r), + child: const Text('View Matrix'), + ), ), ), ], diff --git a/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart b/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart index ee091d8..96c7c29 100644 --- a/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart +++ b/lib/modules/settings/presentation/screens/company_profile_settings_screen.dart @@ -151,7 +151,8 @@ class _CompanyProfileSettingsScreenState AppTextField( controller: _gstController, label: 'GST/VAT Number', - validator: Validators.gstNumber, + validator: Validators.optionalGstin, + inputFormatters: Validators.gstinInput, ), const SizedBox(height: 16), AppTextField( @@ -171,6 +172,8 @@ class _CompanyProfileSettingsScreenState controller: _phoneController, label: 'Phone', keyboardType: TextInputType.phone, + validator: Validators.optionalMobile, + inputFormatters: Validators.mobileInput, ), const SizedBox(height: 16), AppTextField( diff --git a/lib/modules/settings/presentation/widgets/settings_widgets.dart b/lib/modules/settings/presentation/widgets/settings_widgets.dart index 2bd8fa6..992d65c 100644 --- a/lib/modules/settings/presentation/widgets/settings_widgets.dart +++ b/lib/modules/settings/presentation/widgets/settings_widgets.dart @@ -3,6 +3,8 @@ import 'package:go_router/go_router.dart'; import '../../../../core/utils/responsive_utils.dart'; import '../../../../shared/widgets/app_card.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../domain/entities/app_settings.dart'; @@ -138,14 +140,15 @@ class SettingsDropdownField extends StatelessWidget { @override Widget build(BuildContext context) { - return DropdownButtonFormField( + return AppSearchableDropdown( + label: label, value: value, - decoration: InputDecoration(labelText: label), - items: items + searchHint: 'Search ${label.toLowerCase()}...', + options: items .map( - (item) => DropdownMenuItem( + (item) => AppDropdownOption( value: item, - child: Text(itemLabel(item)), + label: itemLabel(item), ), ) .toList(), diff --git a/lib/modules/users/presentation/screens/user_form_screen.dart b/lib/modules/users/presentation/screens/user_form_screen.dart index 8144528..863e819 100644 --- a/lib/modules/users/presentation/screens/user_form_screen.dart +++ b/lib/modules/users/presentation/screens/user_form_screen.dart @@ -194,7 +194,8 @@ class _UserFormScreenState extends ConsumerState { controller: _mobileController, label: 'Mobile', keyboardType: TextInputType.phone, - validator: Validators.phone, + validator: Validators.mobile, + inputFormatters: Validators.mobileInput, ), const SizedBox(height: 16), if (!isEditing) ...[ diff --git a/lib/modules/users/presentation/screens/user_list_screen.dart b/lib/modules/users/presentation/screens/user_list_screen.dart index 65f1a2f..342c574 100644 --- a/lib/modules/users/presentation/screens/user_list_screen.dart +++ b/lib/modules/users/presentation/screens/user_list_screen.dart @@ -8,7 +8,6 @@ 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'; @@ -18,7 +17,9 @@ 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/app_table_shell.dart'; import '../../../../shared/widgets/page_header.dart'; +import '../widgets/user_rich_data_table.dart'; import '../providers/users_provider.dart'; class UserListScreen extends ConsumerStatefulWidget { @@ -68,63 +69,70 @@ class _UserListScreenState extends ConsumerState { _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, - ), + child: AppTableShell( + toolbar: _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), + ), + footer: 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, + ), + child: RefreshIndicator( + onRefresh: () => ref.read(usersListProvider.notifier).refresh(), + child: state.users.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const [ + SizedBox( + height: 240, + child: 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, - ), ], ), ), @@ -209,72 +217,80 @@ class _FiltersBar extends StatelessWidget { @override Widget build(BuildContext context) { - return Wrap( - spacing: 12, - runSpacing: 12, - crossAxisAlignment: WrapCrossAlignment.center, + final searchField = AppSearchField( + controller: searchController, + hint: 'Search users...', + onChanged: onSearch, + ); + + final dropdowns = [ + 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, + ), + 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, + ), + 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, + ), + ]; + + if (context.isMobile) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + searchField, + const SizedBox(height: 12), + ...dropdowns.expand((f) => [f, const SizedBox(height: 12)]).toList() + ..removeLast(), + ], + ); + } + + return Row( 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, - ), - ), + Expanded(flex: 3, child: searchField), + const SizedBox(width: 12), + Expanded(flex: 2, child: dropdowns[0]), + const SizedBox(width: 12), + Expanded(flex: 2, child: dropdowns[1]), + const SizedBox(width: 12), + Expanded(flex: 2, child: dropdowns[2]), ], ); } @@ -303,40 +319,19 @@ class _UserDataTable extends StatelessWidget { @override Widget build(BuildContext context) { - return AppDataTable( + return UserRichDataTable( + users: users, 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, + wrapInCard: false, + actionsBuilder: (_, user) => _UserActions( + user: user, + onView: onView, + onEdit: onEdit, + onToggleStatus: onToggleStatus, + onDeactivate: onDeactivate, + ), ); } } diff --git a/lib/modules/users/presentation/screens/user_profile_screen.dart b/lib/modules/users/presentation/screens/user_profile_screen.dart index b5058cc..b635737 100644 --- a/lib/modules/users/presentation/screens/user_profile_screen.dart +++ b/lib/modules/users/presentation/screens/user_profile_screen.dart @@ -201,7 +201,8 @@ class _UserProfileScreenState extends ConsumerState { controller: _mobileController, label: 'Mobile', keyboardType: TextInputType.phone, - validator: Validators.phone, + validator: Validators.mobile, + inputFormatters: Validators.mobileInput, ), const SizedBox(height: 8), Align( diff --git a/lib/modules/users/presentation/widgets/user_rich_data_table.dart b/lib/modules/users/presentation/widgets/user_rich_data_table.dart new file mode 100644 index 0000000..7e253c8 --- /dev/null +++ b/lib/modules/users/presentation/widgets/user_rich_data_table.dart @@ -0,0 +1,97 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/utils/formatters.dart'; +import '../../../../shared/models/user_management_models.dart'; +import '../../../../shared/widgets/app_data_table.dart'; +import '../../../../shared/widgets/app_status_chip.dart'; +import '../../../rbac/presentation/widgets/rbac_widgets.dart'; + +typedef UserTableActionsBuilder = Widget Function( + BuildContext context, + ManagedUserModel user, +); + +class UserRichDataTable extends StatelessWidget { + const UserRichDataTable({ + super.key, + required this.users, + required this.actionsBuilder, + this.sortColumn, + this.sortAscending = true, + this.onSort, + this.wrapInCard = false, + }); + + final List users; + final UserTableActionsBuilder actionsBuilder; + final String? sortColumn; + final bool sortAscending; + final void Function(String column, bool ascending)? onSort; + final bool wrapInCard; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AppDataTable( + wrapInCard: wrapInCard, + sortColumn: sortColumn, + sortAscending: sortAscending, + onSort: onSort, + columns: [ + AppDataColumn( + label: 'User', + sortKey: 'full_name', + flex: 3, + cellBuilder: (_, user) => UserTableUserCell(user: user), + ), + AppDataColumn( + label: 'Employee Code', + sortKey: 'employee_code', + flex: 1, + cellBuilder: (_, user) => EmployeeCodeBadge(code: user.employeeCode), + ), + AppDataColumn( + label: 'Role', + flex: 2, + cellBuilder: (_, user) => RoleBadge(label: user.roleLabel), + ), + AppDataColumn( + label: 'Department', + flex: 2, + cellBuilder: (_, user) => Text(user.departmentLabel), + ), + AppDataColumn( + label: 'Plant', + flex: 2, + cellBuilder: (_, user) => Text(user.plantLabel), + ), + AppDataColumn( + label: 'Last Login', + flex: 2, + cellBuilder: (_, user) => Text( + DateFormatter.formatUserLastLogin(user.lastLoginAt), + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + AppDataColumn( + label: 'Status', + flex: 1, + cellBuilder: (_, user) => AppStatusChip(status: user.status), + ), + AppDataColumn( + label: 'Actions', + flex: 1, + alignment: Alignment.centerRight, + cellBuilder: (context, user) => Align( + alignment: Alignment.centerRight, + child: actionsBuilder(context, user), + ), + ), + ], + rows: users, + ); + } +} diff --git a/lib/modules/vendors/data/datasources/vendor_remote_data_source.dart b/lib/modules/vendors/data/datasources/vendor_remote_data_source.dart new file mode 100644 index 0000000..218dc69 --- /dev/null +++ b/lib/modules/vendors/data/datasources/vendor_remote_data_source.dart @@ -0,0 +1,229 @@ +import 'package:dio/dio.dart'; + +import '../../../../core/constants/api_endpoints.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/vendor_model.dart'; + +class VendorRemoteDataSource { + VendorRemoteDataSource({required this.dio}); + + final Dio dio; + + Future> getVendors(VendorListQuery query) async { + final response = await dio.get( + ApiEndpoints.vendors, + queryParameters: _queryToMap(query), + ); + return _parsePaginated(response.data, VendorModel.fromJson); + } + + Future getVendorById(String id) async { + final response = await dio.get(ApiEndpoints.vendorById(id)); + return VendorModel.fromJson(response.data['data'] as Map); + } + + Future createVendor(Map data) async { + final response = await dio.post(ApiEndpoints.vendors, data: data); + return VendorModel.fromJson(response.data['data'] as Map); + } + + Future updateVendor(String id, Map data) async { + final response = await dio.put(ApiEndpoints.vendorById(id), data: data); + return VendorModel.fromJson(response.data['data'] as Map); + } + + Future deleteVendor(String id) async { + await dio.delete(ApiEndpoints.vendorById(id)); + } + + Future updateVendorStatus(String id, String status) async { + final response = await dio.patch( + ApiEndpoints.vendorStatus(id), + data: {'status': status}, + ); + return VendorModel.fromJson(response.data['data'] as Map); + } + + Future> getAddresses(String vendorId) async { + final response = await dio.get(ApiEndpoints.vendorAddresses(vendorId)); + return _parseList(response.data, VendorAddressModel.fromJson); + } + + Future createAddress( + String vendorId, + Map data, + ) async { + final response = await dio.post( + ApiEndpoints.vendorAddresses(vendorId), + data: data, + ); + return VendorAddressModel.fromJson( + response.data['data'] as Map, + ); + } + + Future updateAddress( + String vendorId, + String addressId, + Map data, + ) async { + final response = await dio.put( + ApiEndpoints.vendorAddressById(vendorId, addressId), + data: data, + ); + return VendorAddressModel.fromJson( + response.data['data'] as Map, + ); + } + + Future deleteAddress(String vendorId, String addressId) async { + await dio.delete(ApiEndpoints.vendorAddressById(vendorId, addressId)); + } + + Future> getContacts(String vendorId) async { + final response = await dio.get(ApiEndpoints.vendorContacts(vendorId)); + return _parseList(response.data, VendorContactModel.fromJson); + } + + Future createContact( + String vendorId, + Map data, + ) async { + final response = await dio.post( + ApiEndpoints.vendorContacts(vendorId), + data: data, + ); + return VendorContactModel.fromJson( + response.data['data'] as Map, + ); + } + + Future updateContact( + String vendorId, + String contactId, + Map data, + ) async { + final response = await dio.put( + ApiEndpoints.vendorContactById(vendorId, contactId), + data: data, + ); + return VendorContactModel.fromJson( + response.data['data'] as Map, + ); + } + + Future deleteContact(String vendorId, String contactId) async { + await dio.delete(ApiEndpoints.vendorContactById(vendorId, contactId)); + } + + Future> getBankDetails(String vendorId) async { + final response = await dio.get(ApiEndpoints.vendorBankDetails(vendorId)); + return _parseList(response.data, VendorBankDetailModel.fromJson); + } + + Future createBankDetail( + String vendorId, + Map data, + ) async { + final response = await dio.post( + ApiEndpoints.vendorBankDetails(vendorId), + data: data, + ); + return VendorBankDetailModel.fromJson( + response.data['data'] as Map, + ); + } + + Future updateBankDetail( + String vendorId, + String bankDetailId, + Map data, + ) async { + final response = await dio.put( + ApiEndpoints.vendorBankDetailById(vendorId, bankDetailId), + data: data, + ); + return VendorBankDetailModel.fromJson( + response.data['data'] as Map, + ); + } + + Future deleteBankDetail(String vendorId, String bankDetailId) async { + await dio.delete(ApiEndpoints.vendorBankDetailById(vendorId, bankDetailId)); + } + + Map _queryToMap(VendorListQuery 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.vendorType != null) 'vendor_type': query.vendorType, + if (query.isActive != null) 'is_active': query.isActive, + }; + } + + List _parseList( + dynamic body, + T Function(Map) fromJson, + ) { + if (body is! Map) return []; + final raw = body['data']; + if (raw is List) { + return raw.whereType>().map(fromJson).toList(); + } + if (raw is Map) { + final items = raw['items']; + if (items is List) { + return items.whereType>().map(fromJson).toList(); + } + } + return []; + } + + PaginatedResponse _parsePaginated( + dynamic body, + T Function(Map) fromJson, + ) { + if (body is! Map) { + return const PaginatedResponse( + items: [], + page: 1, + limit: 20, + total: 0, + totalPages: 1, + ); + } + + final raw = body['data']; + final meta = body['meta'] as Map? ?? {}; + + if (raw is List) { + final items = raw.whereType>().map(fromJson).toList(); + final limit = (meta['limit'] as num?)?.toInt() ?? items.length; + final total = (meta['total'] as num?)?.toInt() ?? items.length; + return PaginatedResponse( + items: items, + page: (meta['page'] as num?)?.toInt() ?? 1, + limit: limit, + total: total, + totalPages: limit > 0 ? ((total + limit - 1) / limit).ceil().clamp(1, 999999) : 1, + ); + } + + if (raw is Map) { + return PaginatedResponse.fromJson( + raw, + (json) => fromJson(json! as Map), + ); + } + + return const PaginatedResponse( + items: [], + page: 1, + limit: 20, + total: 0, + totalPages: 1, + ); + } +} diff --git a/lib/modules/vendors/data/repositories/vendor_repository_impl.dart b/lib/modules/vendors/data/repositories/vendor_repository_impl.dart new file mode 100644 index 0000000..0ec5055 --- /dev/null +++ b/lib/modules/vendors/data/repositories/vendor_repository_impl.dart @@ -0,0 +1,133 @@ +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/vendor_model.dart'; +import '../../domain/repositories/vendor_repository.dart'; +import '../datasources/vendor_remote_data_source.dart'; + +final vendorRemoteDataSourceProvider = Provider((ref) { + return VendorRemoteDataSource(dio: ref.watch(dioProvider)); +}); + +final vendorRepositoryProvider = Provider((ref) { + return VendorRepositoryImpl(dataSource: ref.watch(vendorRemoteDataSourceProvider)); +}); + +class VendorRepositoryImpl implements VendorRepository { + VendorRepositoryImpl({required this.dataSource}); + + final VendorRemoteDataSource dataSource; + + @override + Future>> getVendors(VendorListQuery query) { + return safeApiCall(() => dataSource.getVendors(query)); + } + + @override + Future> getVendorById(String id) { + return safeApiCall(() => dataSource.getVendorById(id)); + } + + @override + Future> createVendor(Map data) { + return safeApiCall(() => dataSource.createVendor(data)); + } + + @override + Future> updateVendor(String id, Map data) { + return safeApiCall(() => dataSource.updateVendor(id, data)); + } + + @override + Future> deleteVendor(String id) { + return safeApiCall(() => dataSource.deleteVendor(id)); + } + + @override + Future> updateVendorStatus(String id, String status) { + return safeApiCall(() => dataSource.updateVendorStatus(id, status)); + } + + @override + Future>> getAddresses(String vendorId) { + return safeApiCall(() => dataSource.getAddresses(vendorId)); + } + + @override + Future> createAddress( + String vendorId, + Map data, + ) { + return safeApiCall(() => dataSource.createAddress(vendorId, data)); + } + + @override + Future> updateAddress( + String vendorId, + String addressId, + Map data, + ) { + return safeApiCall(() => dataSource.updateAddress(vendorId, addressId, data)); + } + + @override + Future> deleteAddress(String vendorId, String addressId) { + return safeApiCall(() => dataSource.deleteAddress(vendorId, addressId)); + } + + @override + Future>> getContacts(String vendorId) { + return safeApiCall(() => dataSource.getContacts(vendorId)); + } + + @override + Future> createContact( + String vendorId, + Map data, + ) { + return safeApiCall(() => dataSource.createContact(vendorId, data)); + } + + @override + Future> updateContact( + String vendorId, + String contactId, + Map data, + ) { + return safeApiCall(() => dataSource.updateContact(vendorId, contactId, data)); + } + + @override + Future> deleteContact(String vendorId, String contactId) { + return safeApiCall(() => dataSource.deleteContact(vendorId, contactId)); + } + + @override + Future>> getBankDetails(String vendorId) { + return safeApiCall(() => dataSource.getBankDetails(vendorId)); + } + + @override + Future> createBankDetail( + String vendorId, + Map data, + ) { + return safeApiCall(() => dataSource.createBankDetail(vendorId, data)); + } + + @override + Future> updateBankDetail( + String vendorId, + String bankDetailId, + Map data, + ) { + return safeApiCall(() => dataSource.updateBankDetail(vendorId, bankDetailId, data)); + } + + @override + Future> deleteBankDetail(String vendorId, String bankDetailId) { + return safeApiCall(() => dataSource.deleteBankDetail(vendorId, bankDetailId)); + } +} diff --git a/lib/modules/vendors/domain/repositories/vendor_repository.dart b/lib/modules/vendors/domain/repositories/vendor_repository.dart new file mode 100644 index 0000000..00cccff --- /dev/null +++ b/lib/modules/vendors/domain/repositories/vendor_repository.dart @@ -0,0 +1,45 @@ +import '../../../../core/network/api_handler.dart'; +import '../../../../shared/models/api_response.dart'; +import '../../../../shared/models/vendor_model.dart'; + +abstract class VendorRepository { + Future>> getVendors(VendorListQuery query); + Future> getVendorById(String id); + Future> createVendor(Map data); + Future> updateVendor(String id, Map data); + Future> deleteVendor(String id); + Future> updateVendorStatus(String id, String status); + Future>> getAddresses(String vendorId); + Future> createAddress( + String vendorId, + Map data, + ); + Future> updateAddress( + String vendorId, + String addressId, + Map data, + ); + Future> deleteAddress(String vendorId, String addressId); + Future>> getContacts(String vendorId); + Future> createContact( + String vendorId, + Map data, + ); + Future> updateContact( + String vendorId, + String contactId, + Map data, + ); + Future> deleteContact(String vendorId, String contactId); + Future>> getBankDetails(String vendorId); + Future> createBankDetail( + String vendorId, + Map data, + ); + Future> updateBankDetail( + String vendorId, + String bankDetailId, + Map data, + ); + Future> deleteBankDetail(String vendorId, String bankDetailId); +} diff --git a/lib/modules/vendors/presentation/providers/vendors_provider.dart b/lib/modules/vendors/presentation/providers/vendors_provider.dart new file mode 100644 index 0000000..aca8113 --- /dev/null +++ b/lib/modules/vendors/presentation/providers/vendors_provider.dart @@ -0,0 +1,329 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../shared/models/vendor_model.dart'; +import '../../data/repositories/vendor_repository_impl.dart'; + +class VendorsListState { + const VendorsListState({ + this.vendors = const [], + this.query = const VendorListQuery(limit: 20), + this.total = 0, + this.totalPages = 1, + this.isRefreshing = false, + this.actionError, + this.actionSuccess, + }); + + final List vendors; + final VendorListQuery query; + final int total; + final int totalPages; + final bool isRefreshing; + final String? actionError; + final String? actionSuccess; + + VendorsListState copyWith({ + List? vendors, + VendorListQuery? query, + int? total, + int? totalPages, + bool? isRefreshing, + String? actionError, + String? actionSuccess, + bool clearMessages = false, + }) { + return VendorsListState( + vendors: vendors ?? this.vendors, + 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 vendorsListProvider = + AsyncNotifierProvider.autoDispose( + VendorsListNotifier.new, +); + +class VendorsListNotifier extends AutoDisposeAsyncNotifier { + @override + Future build() async { + return _load(const VendorListQuery(limit: 20)); + } + + Future _load(VendorListQuery query) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.getVendors(query); + if (result.failure != null) throw result.failure!; + final page = result.data!; + return VendorsListState( + vendors: page.items, + query: query, + total: page.total, + totalPages: page.totalPages, + ); + } + + Future refresh() async { + final current = state.valueOrNull ?? const VendorsListState(); + state = AsyncData(current.copyWith(isRefreshing: true, clearMessages: true)); + try { + state = AsyncData(await _load(current.query)); + } catch (e, st) { + state = AsyncError(e, st); + } + } + + Future applyQuery(VendorListQuery query) async { + final previous = state.valueOrNull; + if (previous == null) { + state = const AsyncLoading(); + } + try { + state = AsyncData(await _load(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 setStatusFilter(String? status) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(status: status, page: 1)); + } + + void setVendorTypeFilter(String? vendorType) { + final current = state.valueOrNull; + if (current == null) return; + applyQuery(current.query.copyWith(vendorType: vendorType, 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)); + } + + Future deleteVendor(String id) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.deleteVendor(id); + if (result.failure != null) { + final current = state.valueOrNull; + if (current != null) { + state = AsyncData(current.copyWith(actionError: result.failure!.message)); + } + return false; + } + await refresh(); + final current = state.valueOrNull; + if (current != null) { + state = AsyncData(current.copyWith(actionSuccess: 'Vendor deleted')); + } + return true; + } +} + +final vendorDetailProvider = + AsyncNotifierProvider.family( + VendorDetailNotifier.new, +); + +class VendorDetailState { + const VendorDetailState({ + required this.vendor, + this.addresses = const [], + this.contacts = const [], + this.bankDetails = const [], + }); + + final VendorModel vendor; + final List addresses; + final List contacts; + final List bankDetails; + + VendorDetailState copyWith({ + VendorModel? vendor, + List? addresses, + List? contacts, + List? bankDetails, + }) { + return VendorDetailState( + vendor: vendor ?? this.vendor, + addresses: addresses ?? this.addresses, + contacts: contacts ?? this.contacts, + bankDetails: bankDetails ?? this.bankDetails, + ); + } +} + +class VendorDetailNotifier extends FamilyAsyncNotifier { + @override + Future build(String arg) async { + return _loadAll(arg); + } + + Future _loadAll(String vendorId) async { + final repository = ref.read(vendorRepositoryProvider); + final vendorResult = await repository.getVendorById(vendorId); + if (vendorResult.failure != null) throw vendorResult.failure!; + + final vendor = vendorResult.data!; + final addresses = vendor.addresses.isNotEmpty + ? vendor.addresses + : (await repository.getAddresses(vendorId)).data ?? []; + final contacts = vendor.contacts.isNotEmpty + ? vendor.contacts + : (await repository.getContacts(vendorId)).data ?? []; + final bankDetails = vendor.bankDetails.isNotEmpty + ? vendor.bankDetails + : (await repository.getBankDetails(vendorId)).data ?? []; + + return VendorDetailState( + vendor: vendor, + addresses: addresses, + contacts: contacts, + bankDetails: bankDetails, + ); + } + + Future reload() async { + state = const AsyncLoading(); + state = AsyncData(await _loadAll(arg)); + } + + Future updateVendor(Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.updateVendor(arg, data); + if (result.failure != null) throw result.failure!; + await reload(); + ref.invalidate(vendorsListProvider); + return result.data; + } + + Future deleteVendor() async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.deleteVendor(arg); + if (result.failure != null) throw result.failure!; + ref.invalidate(vendorsListProvider); + return true; + } + + Future updateStatus(String status) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.updateVendorStatus(arg, status); + if (result.failure != null) throw result.failure!; + await reload(); + ref.invalidate(vendorsListProvider); + return result.data; + } + + Future createAddress(Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.createAddress(arg, data); + if (result.failure != null) throw result.failure!; + await reload(); + } + + Future updateAddress(String addressId, Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.updateAddress(arg, addressId, data); + if (result.failure != null) throw result.failure!; + await reload(); + } + + Future deleteAddress(String addressId) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.deleteAddress(arg, addressId); + if (result.failure != null) throw result.failure!; + await reload(); + } + + Future createContact(Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.createContact(arg, data); + if (result.failure != null) throw result.failure!; + await reload(); + } + + Future updateContact(String contactId, Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.updateContact(arg, contactId, data); + if (result.failure != null) throw result.failure!; + await reload(); + } + + Future deleteContact(String contactId) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.deleteContact(arg, contactId); + if (result.failure != null) throw result.failure!; + await reload(); + } + + Future createBankDetail(Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.createBankDetail(arg, data); + if (result.failure != null) throw result.failure!; + await reload(); + } + + Future updateBankDetail(String bankDetailId, Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.updateBankDetail(arg, bankDetailId, data); + if (result.failure != null) throw result.failure!; + await reload(); + } + + Future deleteBankDetail(String bankDetailId) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.deleteBankDetail(arg, bankDetailId); + if (result.failure != null) throw result.failure!; + await reload(); + } +} + +final vendorFormProvider = AsyncNotifierProvider.family< + VendorFormNotifier, VendorModel?, String?>(VendorFormNotifier.new); + +class VendorFormNotifier extends FamilyAsyncNotifier { + @override + Future build(String? arg) async { + if (arg == null) return null; + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.getVendorById(arg); + if (result.failure != null) throw result.failure!; + return result.data; + } + + Future submitCreate(Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.createVendor(data); + if (result.failure != null) throw result.failure!; + ref.invalidate(vendorsListProvider); + return result.data!; + } + + Future submitUpdate(String id, Map data) async { + final repository = ref.read(vendorRepositoryProvider); + final result = await repository.updateVendor(id, data); + if (result.failure != null) throw result.failure!; + ref.invalidate(vendorsListProvider); + ref.invalidate(vendorDetailProvider(id)); + return result.data!; + } +} diff --git a/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart b/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart new file mode 100644 index 0000000..7e970e2 --- /dev/null +++ b/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart @@ -0,0 +1,660 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/errors/failure.dart'; +import '../../../../shared/models/vendor_model.dart'; +import '../../../../shared/providers/permissions_provider.dart'; +import '../../../../shared/widgets/app_card.dart'; +import '../../../../shared/widgets/app_confirmation_dialog.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_status_chip.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/vendors_provider.dart'; +import '../widgets/vendor_form_panel.dart'; +import '../widgets/vendor_sub_resource_panels.dart'; + +class VendorDetailScreen extends ConsumerStatefulWidget { + const VendorDetailScreen({super.key, required this.vendorId}); + + final String vendorId; + + @override + ConsumerState createState() => _VendorDetailScreenState(); +} + +class _VendorDetailScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + late final TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 4, vsync: this); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final detailAsync = ref.watch(vendorDetailProvider(widget.vendorId)); + final canEdit = ref.can('vendors', PermissionAction.update); + final canDelete = ref.can('vendors', PermissionAction.delete); + + return detailAsync.when( + loading: () => const AppLoadingView(message: 'Loading vendor details...'), + error: (e, _) => ErrorView.fromFailure( + e is Failure ? e : Failure.unknown(message: e.toString()), + onRetry: () => ref.invalidate(vendorDetailProvider(widget.vendorId)), + ), + data: (state) => Padding( + padding: const EdgeInsets.all(24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: state.vendor.vendorName, + subtitle: state.vendor.vendorCode ?? 'Vendor ID: ${state.vendor.id}', + actions: [ + if (canEdit) ...[ + OutlinedButton.icon( + onPressed: () => _changeStatus(state.vendor), + icon: const Icon(Icons.swap_horiz), + label: const Text('Change Status'), + ), + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: () => openVendorFormPanel( + context, + ref, + vendorId: widget.vendorId, + ), + icon: const Icon(Icons.edit_outlined), + label: const Text('Edit'), + ), + ], + if (canDelete) ...[ + const SizedBox(width: 8), + OutlinedButton.icon( + onPressed: _deleteVendor, + icon: const Icon(Icons.delete_outline), + label: const Text('Delete'), + ), + ], + ], + ), + const SizedBox(height: 16), + TabBar( + controller: _tabController, + tabs: const [ + Tab(text: 'Overview'), + Tab(text: 'Addresses'), + Tab(text: 'Contacts'), + Tab(text: 'Bank Details'), + ], + ), + const SizedBox(height: 16), + Expanded( + child: TabBarView( + controller: _tabController, + children: [ + _OverviewTab(vendor: state.vendor), + _AddressesTab( + vendorId: widget.vendorId, + addresses: state.addresses, + canEdit: canEdit, + ), + _ContactsTab( + vendorId: widget.vendorId, + contacts: state.contacts, + canEdit: canEdit, + ), + _BankDetailsTab( + vendorId: widget.vendorId, + bankDetails: state.bankDetails, + canEdit: canEdit, + ), + ], + ), + ), + ], + ), + ), + ); + } + + Future _changeStatus(VendorModel vendor) async { + String? selected = vendor.status ?? 'active'; + final confirmed = await showDialog( + context: context, + builder: (context) => StatefulBuilder( + builder: (context, setDialogState) => AlertDialog( + title: const Text('Change Vendor Status'), + content: AppDropdown( + label: 'Status', + value: selected, + options: vendorStatusOptions + .map((e) => AppDropdownOption(value: e.$1, label: e.$2)) + .toList(), + onChanged: (v) => setDialogState(() => selected = v), + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('Update'), + ), + ], + ), + ), + ); + if (confirmed != true || selected == null || !mounted) return; + + try { + await ref + .read(vendorDetailProvider(widget.vendorId).notifier) + .updateStatus(selected!); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Vendor status updated')), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + } + + Future _deleteVendor() async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Delete Vendor', + message: 'Soft delete this vendor?', + confirmLabel: 'Delete', + isDestructive: true, + ); + if (confirmed != true || !mounted) return; + + try { + await ref.read(vendorDetailProvider(widget.vendorId).notifier).deleteVendor(); + if (mounted) context.go(RouteConstants.vendors); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + } +} + +class _OverviewTab extends StatelessWidget { + const _OverviewTab({required this.vendor}); + + final VendorModel vendor; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: AppCard( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + _DetailRow(label: 'Vendor Code', value: vendor.vendorCode ?? '—'), + _DetailRow(label: 'Vendor Name', value: vendor.vendorName), + _DetailRow(label: 'Type', value: vendorTypeLabel(vendor.vendorType)), + _DetailRow(label: 'GSTIN', value: vendor.gstin ?? '—'), + _DetailRow(label: 'PAN', value: vendor.pan ?? '—'), + _DetailRow( + label: 'Payment Term', + value: vendor.paymentTermName ?? '—', + ), + _DetailRow( + label: 'Credit Period', + value: vendor.creditPeriodDays != null + ? '${vendor.creditPeriodDays} days' + : '—', + ), + _DetailRow(label: 'Remarks', value: vendor.remarks ?? '—'), + _DetailRow( + label: 'Status', + value: vendorStatusLabel(vendor.status), + ), + _DetailRow( + label: 'Active', + value: vendor.isActive ? 'Yes' : 'No', + ), + ], + ), + ), + ), + ); + } +} + +class _AddressesTab extends ConsumerWidget { + const _AddressesTab({ + required this.vendorId, + required this.addresses, + required this.canEdit, + }); + + final String vendorId; + final List addresses; + final bool canEdit; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (canEdit) + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: () async { + final saved = await openVendorAddressPanel( + context, + vendorId: vendorId, + ); + if (saved == true && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Address saved')), + ); + } + }, + icon: const Icon(Icons.add), + label: const Text('Add Address'), + ), + ), + const SizedBox(height: 12), + Expanded( + child: addresses.isEmpty + ? const AppEmptyState( + title: 'No addresses', + description: 'Add registered, billing or dispatch addresses.', + icon: Icons.location_on_outlined, + ) + : ListView.separated( + itemCount: addresses.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final address = addresses[index]; + return _SubResourceCard( + title: addressTypeOptions + .where((e) => e.$1 == address.addressType) + .map((e) => e.$2) + .firstOrNull ?? + address.addressType ?? + 'Address', + subtitle: [ + address.addressLine1, + address.addressLine2, + address.city, + address.state, + address.pincode, + ].where((e) => e != null && e.isNotEmpty).join(', '), + trailing: canEdit + ? _SubResourceActions( + onEdit: () => openVendorAddressPanel( + context, + vendorId: vendorId, + address: address, + ), + onDelete: () => _deleteAddress( + context, + ref, + vendorId, + address.id, + ), + ) + : null, + chip: AppStatusChip( + status: address.isActive ? 'active' : 'inactive', + compact: true, + ), + ); + }, + ), + ), + ], + ); + } + + Future _deleteAddress( + BuildContext context, + WidgetRef ref, + String vendorId, + String addressId, + ) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Delete Address', + message: 'Remove this address?', + confirmLabel: 'Delete', + isDestructive: true, + ); + if (confirmed != true) return; + try { + await ref.read(vendorDetailProvider(vendorId).notifier).deleteAddress(addressId); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + } +} + +class _ContactsTab extends ConsumerWidget { + const _ContactsTab({ + required this.vendorId, + required this.contacts, + required this.canEdit, + }); + + final String vendorId; + final List contacts; + final bool canEdit; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (canEdit) + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: () async { + final saved = await openVendorContactPanel( + context, + vendorId: vendorId, + ); + if (saved == true && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Contact saved')), + ); + } + }, + icon: const Icon(Icons.add), + label: const Text('Add Contact'), + ), + ), + const SizedBox(height: 12), + Expanded( + child: contacts.isEmpty + ? const AppEmptyState( + title: 'No contacts', + description: 'Add contact persons for this vendor.', + icon: Icons.person_outline, + ) + : ListView.separated( + itemCount: contacts.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final contact = contacts[index]; + return _SubResourceCard( + title: contact.contactName, + subtitle: [ + contact.designation, + contact.phone, + contact.email, + ].where((e) => e != null && e.isNotEmpty).join(' · '), + trailing: canEdit + ? _SubResourceActions( + onEdit: () => openVendorContactPanel( + context, + vendorId: vendorId, + contact: contact, + ), + onDelete: () => _deleteContact( + context, + ref, + vendorId, + contact.id, + ), + ) + : null, + chip: AppStatusChip( + status: contact.isActive ? 'active' : 'inactive', + compact: true, + ), + ); + }, + ), + ), + ], + ); + } + + Future _deleteContact( + BuildContext context, + WidgetRef ref, + String vendorId, + String contactId, + ) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Delete Contact', + message: 'Remove this contact?', + confirmLabel: 'Delete', + isDestructive: true, + ); + if (confirmed != true) return; + try { + await ref.read(vendorDetailProvider(vendorId).notifier).deleteContact(contactId); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + } +} + +class _BankDetailsTab extends ConsumerWidget { + const _BankDetailsTab({ + required this.vendorId, + required this.bankDetails, + required this.canEdit, + }); + + final String vendorId; + final List bankDetails; + final bool canEdit; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (canEdit) + Align( + alignment: Alignment.centerRight, + child: ElevatedButton.icon( + onPressed: () async { + final saved = await openVendorBankDetailPanel( + context, + vendorId: vendorId, + ); + if (saved == true && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Bank detail saved')), + ); + } + }, + icon: const Icon(Icons.add), + label: const Text('Add Bank Detail'), + ), + ), + const SizedBox(height: 12), + Expanded( + child: bankDetails.isEmpty + ? const AppEmptyState( + title: 'No bank details', + description: 'Add bank accounts for payments.', + icon: Icons.account_balance_outlined, + ) + : ListView.separated( + itemCount: bankDetails.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final detail = bankDetails[index]; + return _SubResourceCard( + title: detail.bankName ?? 'Bank Account', + subtitle: [ + detail.branch, + detail.ifsc, + detail.accountHolderName, + if (detail.accountNumber != null) + 'A/C: ${detail.accountNumber}', + ].where((e) => e != null && e.isNotEmpty).join(' · '), + trailing: canEdit + ? _SubResourceActions( + onEdit: () => openVendorBankDetailPanel( + context, + vendorId: vendorId, + bankDetail: detail, + ), + onDelete: () => _deleteBankDetail( + context, + ref, + vendorId, + detail.id, + ), + ) + : null, + chip: AppStatusChip( + status: detail.isActive ? 'active' : 'inactive', + compact: true, + ), + ); + }, + ), + ), + ], + ); + } + + Future _deleteBankDetail( + BuildContext context, + WidgetRef ref, + String vendorId, + String bankDetailId, + ) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Delete Bank Detail', + message: 'Remove this bank account?', + confirmLabel: 'Delete', + isDestructive: true, + ); + if (confirmed != true) return; + try { + await ref + .read(vendorDetailProvider(vendorId).notifier) + .deleteBankDetail(bankDetailId); + } catch (e) { + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } + } +} + +class _SubResourceCard extends StatelessWidget { + const _SubResourceCard({ + required this.title, + required this.subtitle, + this.trailing, + this.chip, + }); + + final String title; + final String subtitle; + final Widget? trailing; + final Widget? chip; + + @override + Widget build(BuildContext context) { + return AppCard( + child: ListTile( + title: Text(title), + subtitle: subtitle.isNotEmpty ? Text(subtitle) : null, + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (chip != null) ...[chip!, const SizedBox(width: 8)], + if (trailing != null) trailing!, + ], + ), + ), + ); + } +} + +class _SubResourceActions extends StatelessWidget { + const _SubResourceActions({required this.onEdit, required this.onDelete}); + + final VoidCallback onEdit; + final VoidCallback onDelete; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + tooltip: 'Edit', + icon: const Icon(Icons.edit_outlined, size: 20), + onPressed: onEdit, + ), + IconButton( + tooltip: 'Delete', + icon: const Icon(Icons.delete_outline, size: 20), + onPressed: onDelete, + ), + ], + ); + } +} + +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( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: 160, + child: Text( + label, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ), + Expanded(child: Text(value)), + ], + ), + ); + } +} diff --git a/lib/modules/vendors/presentation/screens/vendor_list_screen.dart b/lib/modules/vendors/presentation/screens/vendor_list_screen.dart new file mode 100644 index 0000000..4b33250 --- /dev/null +++ b/lib/modules/vendors/presentation/screens/vendor_list_screen.dart @@ -0,0 +1,399 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/constants/enums.dart'; +import '../../../../core/constants/route_constants.dart'; +import '../../../../core/errors/failure.dart'; +import '../../../../core/utils/responsive_utils.dart'; +import '../../../../shared/models/vendor_model.dart'; +import '../../../../shared/providers/permissions_provider.dart'; +import '../../../../shared/widgets/app_card.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_searchable_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_status_chip.dart'; +import '../../../../shared/widgets/app_table_action_icon.dart'; +import '../../../../shared/widgets/can_permission.dart'; +import '../../../../shared/widgets/app_table_shell.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../../shared/widgets/page_header.dart'; +import '../providers/vendors_provider.dart'; +import '../widgets/vendor_form_panel.dart'; + +class VendorListScreen extends ConsumerStatefulWidget { + const VendorListScreen({super.key}); + + @override + ConsumerState createState() => _VendorListScreenState(); +} + +class _VendorListScreenState extends ConsumerState { + final _searchController = TextEditingController(); + + @override + void dispose() { + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final vendorsAsync = ref.watch(vendorsListProvider); + final canEdit = ref.can('vendors', PermissionAction.update); + final canDelete = ref.can('vendors', PermissionAction.delete); + + ref.listen(vendorsListProvider, (prev, next) { + final error = next.valueOrNull?.actionError; + final success = next.valueOrNull?.actionSuccess; + if (error != null && error != prev?.valueOrNull?.actionError) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(error))); + } + if (success != null && success != prev?.valueOrNull?.actionSuccess) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(success))); + } + }); + + return Padding( + padding: const EdgeInsets.all(24), + child: vendorsAsync.when( + loading: () => const AppLoadingView(message: 'Loading vendors...'), + error: (error, _) => ErrorView.fromFailure( + error is Failure ? error : Failure.unknown(message: error.toString()), + onRetry: () => ref.invalidate(vendorsListProvider), + ), + data: (state) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + PageHeader( + title: 'Vendors', + subtitle: 'Manage suppliers, service providers and vendor master data', + actions: [ + CanPermission( + module: 'vendors', + action: PermissionAction.create, + child: ElevatedButton.icon( + onPressed: () => openVendorFormPanel(context, ref), + icon: const Icon(Icons.add), + label: const Text('Add Vendor'), + ), + ), + ], + ), + const SizedBox(height: 16), + Expanded( + child: AppTableShell( + toolbar: LayoutBuilder( + builder: (context, constraints) { + return _FiltersBar( + searchController: _searchController, + query: state.query, + wrapped: constraints.maxWidth < 900, + onSearch: ref.read(vendorsListProvider.notifier).setSearch, + onStatusChanged: + ref.read(vendorsListProvider.notifier).setStatusFilter, + onVendorTypeChanged: + ref.read(vendorsListProvider.notifier).setVendorTypeFilter, + ); + }, + ), + footer: AppPagination( + currentPage: state.query.page, + totalPages: state.totalPages, + totalItems: state.total, + pageSize: state.query.limit, + onPageChanged: ref.read(vendorsListProvider.notifier).setPage, + onPageSizeChanged: + ref.read(vendorsListProvider.notifier).setPageSize, + ), + child: RefreshIndicator( + onRefresh: () => ref.read(vendorsListProvider.notifier).refresh(), + child: state.vendors.isEmpty + ? ListView( + physics: const AlwaysScrollableScrollPhysics(), + children: const [ + SizedBox( + height: 240, + child: AppEmptyState( + title: 'No vendors found', + description: + 'Try adjusting filters or add a new vendor.', + icon: Icons.store_outlined, + ), + ), + ], + ) + : context.isMobile + ? _VendorCardList( + vendors: state.vendors, + onView: _viewVendor, + onEdit: canEdit ? _editVendor : null, + onDelete: canDelete ? _deleteVendor : null, + ) + : _VendorDataTable( + vendors: state.vendors, + onView: _viewVendor, + onEdit: canEdit ? _editVendor : null, + onDelete: canDelete ? _deleteVendor : null, + ), + ), + ), + ), + ], + ), + ), + ); + } + + void _viewVendor(VendorModel vendor) { + context.push('${RouteConstants.vendors}/${vendor.id}'); + } + + void _editVendor(VendorModel vendor) { + openVendorFormPanel(context, ref, vendorId: vendor.id); + } + + Future _deleteVendor(VendorModel vendor) async { + final confirmed = await showAppConfirmationDialog( + context: context, + title: 'Delete Vendor', + message: 'Soft delete "${vendor.vendorName}"?', + confirmLabel: 'Delete', + isDestructive: true, + ); + if (confirmed != true || !mounted) return; + await ref.read(vendorsListProvider.notifier).deleteVendor(vendor.id); + } +} + +class _FiltersBar extends StatelessWidget { + const _FiltersBar({ + required this.searchController, + required this.query, + required this.wrapped, + required this.onSearch, + required this.onStatusChanged, + required this.onVendorTypeChanged, + }); + + final TextEditingController searchController; + final VendorListQuery query; + final bool wrapped; + final ValueChanged onSearch; + final ValueChanged onStatusChanged; + final ValueChanged onVendorTypeChanged; + + @override + Widget build(BuildContext context) { + final searchField = SizedBox( + width: wrapped ? double.infinity : null, + child: AppSearchField( + controller: searchController, + hint: 'Search vendors...', + onChanged: onSearch, + ), + ); + + final filters = [ + SizedBox( + width: wrapped ? double.infinity : 180, + child: AppSearchableDropdown( + label: 'Status', + value: query.status, + searchHint: 'Search status...', + isDense: true, + options: [ + const AppDropdownOption(value: null, label: 'All statuses'), + ...vendorStatusOptions.map( + (e) => AppDropdownOption(value: e.$1, label: e.$2), + ), + ], + onChanged: onStatusChanged, + ), + ), + SizedBox( + width: wrapped ? double.infinity : 200, + child: AppSearchableDropdown( + label: 'Vendor Type', + value: query.vendorType, + searchHint: 'Search vendor type...', + isDense: true, + options: [ + const AppDropdownOption(value: null, label: 'All types'), + ...vendorTypeOptions.map( + (e) => AppDropdownOption(value: e.$1, label: e.$2), + ), + ], + onChanged: onVendorTypeChanged, + ), + ), + ]; + + if (wrapped) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + searchField, + const SizedBox(height: 12), + ...filters.expand((filter) => [filter, const SizedBox(height: 12)]).toList() + ..removeLast(), + ], + ); + } + + 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]), + ], + ); + } +} + +class _VendorDataTable extends StatelessWidget { + const _VendorDataTable({ + required this.vendors, + required this.onView, + this.onEdit, + this.onDelete, + }); + + final List vendors; + final ValueChanged onView; + final ValueChanged? onEdit; + final ValueChanged? onDelete; + + @override + Widget build(BuildContext context) { + return AppDataTable( + wrapInCard: false, + columns: [ + AppDataColumn( + label: 'Code', + flex: 1, + cellBuilder: (_, vendor) => Text(vendor.vendorCode ?? '—'), + ), + AppDataColumn( + label: 'Name', + flex: 2, + cellBuilder: (_, vendor) => Text(vendor.vendorName), + ), + AppDataColumn( + label: 'Type', + flex: 2, + cellBuilder: (_, vendor) => Text(vendorTypeLabel(vendor.vendorType)), + ), + AppDataColumn( + label: 'GSTIN', + flex: 2, + cellBuilder: (_, vendor) => Text(vendor.gstin ?? '—'), + ), + AppDataColumn( + label: 'Status', + flex: 1, + cellBuilder: (_, vendor) => AppStatusChip( + status: vendor.status ?? (vendor.isActive ? 'active' : 'inactive'), + ), + ), + AppDataColumn( + label: 'Actions', + flex: 1, + alignment: Alignment.centerRight, + cellBuilder: (_, vendor) => AppTableActions( + children: [ + AppTableActionIcon( + tooltip: 'View', + icon: Icons.visibility_outlined, + onPressed: () => onView(vendor), + ), + if (onEdit != null) + AppTableActionIcon( + tooltip: 'Edit', + icon: Icons.edit_outlined, + onPressed: () => onEdit!(vendor), + ), + if (onDelete != null) + AppTableActionIcon( + tooltip: 'Delete', + icon: Icons.delete_outline, + onPressed: () => onDelete!(vendor), + ), + ], + ), + ), + ], + rows: vendors, + ); + } +} + +class _VendorCardList extends StatelessWidget { + const _VendorCardList({ + required this.vendors, + required this.onView, + this.onEdit, + this.onDelete, + }); + + final List vendors; + final ValueChanged onView; + final ValueChanged? onEdit; + final ValueChanged? onDelete; + + @override + Widget build(BuildContext context) { + return ListView.separated( + itemCount: vendors.length, + separatorBuilder: (_, __) => const SizedBox(height: 12), + itemBuilder: (context, index) { + final vendor = vendors[index]; + return AppCard( + child: ListTile( + title: Text(vendor.vendorName), + subtitle: Text( + '${vendor.vendorCode ?? '—'} · ${vendorTypeLabel(vendor.vendorType)}', + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + AppStatusChip( + status: vendor.status ?? + (vendor.isActive ? 'active' : 'inactive'), + compact: true, + ), + PopupMenuButton( + onSelected: (action) { + switch (action) { + case 'view': + onView(vendor); + case 'edit': + onEdit?.call(vendor); + case 'delete': + onDelete?.call(vendor); + } + }, + itemBuilder: (context) => [ + const PopupMenuItem(value: 'view', child: Text('View')), + if (onEdit != null) + const PopupMenuItem(value: 'edit', child: Text('Edit')), + if (onDelete != null) + const PopupMenuItem(value: 'delete', child: Text('Delete')), + ], + ), + ], + ), + onTap: () => onView(vendor), + ), + ); + }, + ); + } +} diff --git a/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart b/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart new file mode 100644 index 0000000..1b6e5fe --- /dev/null +++ b/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart @@ -0,0 +1,288 @@ +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/models/vendor_model.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_searchable_dropdown.dart'; +import '../../../../shared/widgets/app_loading_view.dart'; +import '../../../../shared/widgets/app_side_panel.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/error_view.dart'; +import '../../../masters/data/datasources/master_remote_data_source.dart'; +import '../providers/vendors_provider.dart'; + +Future openVendorFormPanel( + BuildContext context, + WidgetRef ref, { + String? vendorId, +}) async { + ref.invalidate(vendorFormProvider(vendorId)); + final saved = await showSidePanel( + context, + VendorFormPanel(vendorId: vendorId), + width: 560, + ); + if (saved == true && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + vendorId == null + ? 'Vendor created successfully' + : 'Vendor updated successfully', + ), + ), + ); + } +} + +class VendorFormPanel extends ConsumerStatefulWidget { + const VendorFormPanel({super.key, this.vendorId}); + + final String? vendorId; + + bool get isEditing => vendorId != null; + + @override + ConsumerState createState() => _VendorFormPanelState(); +} + +class _VendorFormPanelState extends ConsumerState { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _gstinController = TextEditingController(); + final _panController = TextEditingController(); + final _creditDaysController = TextEditingController(); + final _remarksController = TextEditingController(); + String? _vendorType; + int? _paymentTermId; + bool _isActive = true; + bool _isSubmitting = false; + String? _populatedSignature; + + @override + void dispose() { + _nameController.dispose(); + _gstinController.dispose(); + _panController.dispose(); + _creditDaysController.dispose(); + _remarksController.dispose(); + super.dispose(); + } + + String _vendorSignature(VendorModel vendor) => + '${vendor.id}:${vendor.vendorType}:${vendor.paymentTermId}:' + '${vendor.creditPeriodDays}:${vendor.isActive}:${vendor.vendorName}'; + + void _populateFromVendor(VendorModel vendor) { + setState(() { + _nameController.text = vendor.vendorName; + _vendorType = vendor.vendorType; + _gstinController.text = vendor.gstin ?? ''; + _panController.text = vendor.pan ?? ''; + _paymentTermId = vendor.paymentTermId; + _creditDaysController.text = + vendor.creditPeriodDays?.toString() ?? ''; + _remarksController.text = vendor.remarks ?? ''; + _isActive = vendor.isActive; + }); + } + + Map _buildPayload() { + return { + 'vendor_name': _nameController.text.trim(), + 'vendor_type': _vendorType, + if (_gstinController.text.trim().isNotEmpty) + 'gstin': _gstinController.text.trim(), + if (_panController.text.trim().isNotEmpty) 'pan': _panController.text.trim(), + if (_paymentTermId != null) 'payment_term_id': _paymentTermId, + if (_creditDaysController.text.trim().isNotEmpty) + 'credit_period_days': int.tryParse(_creditDaysController.text.trim()), + if (_remarksController.text.trim().isNotEmpty) + 'remarks': _remarksController.text.trim(), + 'is_active': _isActive, + }; + } + + Future _submit() async { + if (!_formKey.currentState!.validate()) return; + if (_vendorType == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please select vendor type')), + ); + return; + } + + setState(() => _isSubmitting = true); + try { + final notifier = ref.read(vendorFormProvider(widget.vendorId).notifier); + final payload = _buildPayload(); + if (widget.isEditing) { + await notifier.submitUpdate(widget.vendorId!, payload); + } else { + await notifier.submitCreate(payload); + } + if (mounted) Navigator.of(context, rootNavigator: true).pop(true); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(e.toString())), + ); + } + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + @override + Widget build(BuildContext context) { + final paymentTermsAsync = ref.watch(vendorPaymentTermsProvider); + + if (widget.isEditing) { + ref.listen(vendorFormProvider(widget.vendorId), (prev, next) { + next.whenData((vendor) { + if (vendor == null || !mounted) return; + final signature = _vendorSignature(vendor); + if (_populatedSignature != signature) { + _populatedSignature = signature; + _populateFromVendor(vendor); + } + }); + }); + } + + final formAsync = + widget.isEditing ? ref.watch(vendorFormProvider(widget.vendorId)) : null; + + return SidePanelScaffold( + title: widget.isEditing ? 'Edit vendor' : 'Add vendor', + footer: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: _isSubmitting + ? null + : () => Navigator.of(context, rootNavigator: true).pop(), + child: const Text('Cancel'), + ), + const SizedBox(width: 12), + AppButton( + label: widget.isEditing ? 'Update vendor' : 'Save vendor', + expand: false, + icon: Icons.check, + isLoading: _isSubmitting, + onPressed: _isSubmitting ? null : _submit, + ), + ], + ), + child: widget.isEditing && formAsync != null + ? formAsync.when( + loading: () => const AppLoadingView(message: 'Loading vendor...'), + error: (e, _) => ErrorView.fromFailure( + e is Failure ? e : Failure.unknown(message: e.toString()), + onRetry: () => ref.invalidate(vendorFormProvider(widget.vendorId)), + ), + data: (_) => _buildForm(paymentTermsAsync), + ) + : _buildForm(paymentTermsAsync), + ); + } + + Widget _buildForm(AsyncValue> paymentTermsAsync) { + return Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AppTextField( + controller: _nameController, + label: 'Vendor Name *', + validator: (v) => Validators.required(v, fieldName: 'Vendor name'), + ), + const SizedBox(height: 12), + AppDropdown( + label: 'Vendor Type *', + value: _vendorType, + options: vendorTypeOptions + .map((e) => AppDropdownOption(value: e.$1, label: e.$2)) + .toList(), + onChanged: (v) => setState(() => _vendorType = v), + validator: (v) => v == null ? 'Vendor type is required' : null, + ), + const SizedBox(height: 12), + SidePanelFormRow( + left: AppTextField( + controller: _gstinController, + label: 'GSTIN', + validator: Validators.optionalGstin, + inputFormatters: Validators.gstinInput, + ), + right: AppTextField( + controller: _panController, + label: 'PAN', + validator: Validators.optionalPan, + inputFormatters: Validators.panInput, + ), + ), + const SizedBox(height: 12), + SidePanelFormRow( + left: paymentTermsAsync.when( + loading: () => const LinearProgressIndicator(), + error: (_, __) => const Text('Failed to load payment terms'), + data: (terms) => _paymentTermDropdown(terms), + ), + right: AppTextField( + controller: _creditDaysController, + label: 'Credit Period (days)', + keyboardType: TextInputType.number, + ), + ), + const SizedBox(height: 12), + AppTextField( + controller: _remarksController, + label: 'Remarks', + maxLines: 3, + ), + const SizedBox(height: 12), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Active'), + value: _isActive, + onChanged: (v) => setState(() => _isActive = v), + ), + ], + ), + ); + } + + Widget _paymentTermDropdown(List terms) { + final termIds = terms.map((t) => int.tryParse(t.id)).whereType().toList(); + final value = _paymentTermId != null && termIds.contains(_paymentTermId) + ? _paymentTermId + : null; + return AppSearchableDropdown( + label: 'Payment Term', + value: value, + searchHint: 'Search payment term...', + options: terms + .map( + (t) => AppDropdownOption( + value: int.tryParse(t.id) ?? 0, + label: t.name, + ), + ) + .where((option) => option.value != 0) + .toList(), + onChanged: (v) => setState(() => _paymentTermId = v), + ); + } +} + +final vendorPaymentTermsProvider = + FutureProvider>((ref) async { + final dataSource = ref.watch(masterRemoteDataSourceProvider); + return dataSource.listPaymentTerms(); +}); diff --git a/lib/modules/vendors/presentation/widgets/vendor_sub_resource_panels.dart b/lib/modules/vendors/presentation/widgets/vendor_sub_resource_panels.dart new file mode 100644 index 0000000..6e4adcf --- /dev/null +++ b/lib/modules/vendors/presentation/widgets/vendor_sub_resource_panels.dart @@ -0,0 +1,538 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../core/utils/validators.dart'; +import '../../../../shared/models/vendor_model.dart'; +import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_side_panel.dart'; +import '../../../../shared/widgets/app_text_field.dart'; +import '../providers/vendors_provider.dart'; + +Future openVendorAddressPanel( + BuildContext context, { + required String vendorId, + VendorAddressModel? address, +}) { + return showSidePanel( + context, + VendorAddressPanel(vendorId: vendorId, address: address), + width: 520, + ); +} + +Future openVendorContactPanel( + BuildContext context, { + required String vendorId, + VendorContactModel? contact, +}) { + return showSidePanel( + context, + VendorContactPanel(vendorId: vendorId, contact: contact), + width: 480, + ); +} + +Future openVendorBankDetailPanel( + BuildContext context, { + required String vendorId, + VendorBankDetailModel? bankDetail, +}) { + return showSidePanel( + context, + VendorBankDetailPanel(vendorId: vendorId, bankDetail: bankDetail), + width: 520, + ); +} + +class VendorAddressPanel extends ConsumerStatefulWidget { + const VendorAddressPanel({super.key, required this.vendorId, this.address}); + + final String vendorId; + final VendorAddressModel? address; + + bool get isEditing => address != null; + + @override + ConsumerState createState() => _VendorAddressPanelState(); +} + +class _VendorAddressPanelState extends ConsumerState { + final _formKey = GlobalKey(); + final _line1Controller = TextEditingController(); + final _line2Controller = TextEditingController(); + final _cityController = TextEditingController(); + final _stateController = TextEditingController(); + final _pincodeController = TextEditingController(); + final _countryController = TextEditingController(text: 'India'); + final _gstinController = TextEditingController(); + String? _addressType; + bool _isActive = true; + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + final address = widget.address; + if (address != null) { + _addressType = address.addressType; + _line1Controller.text = address.addressLine1 ?? ''; + _line2Controller.text = address.addressLine2 ?? ''; + _cityController.text = address.city ?? ''; + _stateController.text = address.state ?? ''; + _pincodeController.text = address.pincode ?? ''; + _countryController.text = address.country ?? 'India'; + _gstinController.text = address.gstin ?? ''; + _isActive = address.isActive; + } + } + + @override + void dispose() { + _line1Controller.dispose(); + _line2Controller.dispose(); + _cityController.dispose(); + _stateController.dispose(); + _pincodeController.dispose(); + _countryController.dispose(); + _gstinController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + if (_addressType == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Please select address type')), + ); + return; + } + + final payload = { + 'address_type': _addressType, + if (_line1Controller.text.trim().isNotEmpty) + 'address_line1': _line1Controller.text.trim(), + if (_line2Controller.text.trim().isNotEmpty) + 'address_line2': _line2Controller.text.trim(), + if (_cityController.text.trim().isNotEmpty) 'city': _cityController.text.trim(), + if (_stateController.text.trim().isNotEmpty) + 'state': _stateController.text.trim(), + if (_pincodeController.text.trim().isNotEmpty) + 'pincode': _pincodeController.text.trim(), + if (_countryController.text.trim().isNotEmpty) + 'country': _countryController.text.trim(), + if (_gstinController.text.trim().isNotEmpty) + 'gstin': _gstinController.text.trim(), + 'is_active': _isActive, + }; + + setState(() => _isSubmitting = true); + try { + final notifier = ref.read(vendorDetailProvider(widget.vendorId).notifier); + if (widget.isEditing) { + await notifier.updateAddress(widget.address!.id, payload); + } else { + await notifier.createAddress(payload); + } + if (mounted) Navigator.of(context, rootNavigator: true).pop(true); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + @override + Widget build(BuildContext context) { + return SidePanelScaffold( + title: widget.isEditing ? 'Edit address' : 'Add address', + footer: _panelFooter( + context, + isSubmitting: _isSubmitting, + saveLabel: widget.isEditing ? 'Update address' : 'Save address', + onSave: _save, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + AppDropdown( + label: 'Address Type *', + value: _addressType, + options: addressTypeOptions + .map((e) => AppDropdownOption(value: e.$1, label: e.$2)) + .toList(), + onChanged: (v) => setState(() => _addressType = v), + validator: (v) => v == null ? 'Address type is required' : null, + ), + const SizedBox(height: 12), + AppTextField(controller: _line1Controller, label: 'Address Line 1'), + const SizedBox(height: 12), + AppTextField(controller: _line2Controller, label: 'Address Line 2'), + const SizedBox(height: 12), + SidePanelFormRow( + left: AppTextField(controller: _cityController, label: 'City'), + right: AppTextField(controller: _stateController, label: 'State'), + ), + const SizedBox(height: 12), + SidePanelFormRow( + left: AppTextField( + controller: _pincodeController, + label: 'Pincode', + keyboardType: TextInputType.number, + validator: Validators.optionalPincode, + inputFormatters: Validators.pincodeInput, + ), + right: AppTextField(controller: _countryController, label: 'Country'), + ), + const SizedBox(height: 12), + AppTextField( + controller: _gstinController, + label: 'GSTIN', + validator: Validators.optionalGstin, + inputFormatters: Validators.gstinInput, + ), + const SizedBox(height: 12), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Active'), + value: _isActive, + onChanged: (v) => setState(() => _isActive = v), + ), + ], + ), + ), + ); + } +} + +class VendorContactPanel extends ConsumerStatefulWidget { + const VendorContactPanel({super.key, required this.vendorId, this.contact}); + + final String vendorId; + final VendorContactModel? contact; + + bool get isEditing => contact != null; + + @override + ConsumerState createState() => _VendorContactPanelState(); +} + +class _VendorContactPanelState extends ConsumerState { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + final _designationController = TextEditingController(); + final _phoneController = TextEditingController(); + final _emailController = TextEditingController(); + bool _isPrimary = false; + bool _isActive = true; + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + final contact = widget.contact; + if (contact != null) { + _nameController.text = contact.contactName; + _designationController.text = contact.designation ?? ''; + _phoneController.text = contact.phone ?? ''; + _emailController.text = contact.email ?? ''; + _isPrimary = contact.isPrimary; + _isActive = contact.isActive; + } + } + + @override + void dispose() { + _nameController.dispose(); + _designationController.dispose(); + _phoneController.dispose(); + _emailController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + final payload = { + 'contact_name': _nameController.text.trim(), + if (_designationController.text.trim().isNotEmpty) + 'designation': _designationController.text.trim(), + if (_phoneController.text.trim().isNotEmpty) + 'phone': _phoneController.text.trim(), + if (_emailController.text.trim().isNotEmpty) + 'email': _emailController.text.trim(), + 'is_primary': _isPrimary, + 'is_active': _isActive, + }; + + setState(() => _isSubmitting = true); + try { + final notifier = ref.read(vendorDetailProvider(widget.vendorId).notifier); + if (widget.isEditing) { + await notifier.updateContact(widget.contact!.id, payload); + } else { + await notifier.createContact(payload); + } + if (mounted) Navigator.of(context, rootNavigator: true).pop(true); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + @override + Widget build(BuildContext context) { + return SidePanelScaffold( + title: widget.isEditing ? 'Edit contact' : 'Add contact', + footer: _panelFooter( + context, + isSubmitting: _isSubmitting, + saveLabel: widget.isEditing ? 'Update contact' : 'Save contact', + onSave: _save, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _nameController, + label: 'Contact Name *', + validator: (v) => Validators.required(v, fieldName: 'Contact name'), + ), + const SizedBox(height: 12), + AppTextField(controller: _designationController, label: 'Designation'), + const SizedBox(height: 12), + SidePanelFormRow( + left: AppTextField( + controller: _phoneController, + label: 'Phone', + keyboardType: TextInputType.phone, + validator: Validators.optionalMobile, + inputFormatters: Validators.mobileInput, + ), + right: AppTextField( + controller: _emailController, + label: 'Email', + keyboardType: TextInputType.emailAddress, + validator: Validators.optionalEmail, + ), + ), + const SizedBox(height: 12), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Primary contact'), + value: _isPrimary, + onChanged: (v) => setState(() => _isPrimary = v), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Active'), + value: _isActive, + onChanged: (v) => setState(() => _isActive = v), + ), + ], + ), + ), + ); + } +} + +class VendorBankDetailPanel extends ConsumerStatefulWidget { + const VendorBankDetailPanel({ + super.key, + required this.vendorId, + this.bankDetail, + }); + + final String vendorId; + final VendorBankDetailModel? bankDetail; + + bool get isEditing => bankDetail != null; + + @override + ConsumerState createState() => + _VendorBankDetailPanelState(); +} + +class _VendorBankDetailPanelState extends ConsumerState { + final _formKey = GlobalKey(); + final _bankNameController = TextEditingController(); + final _branchController = TextEditingController(); + final _accountNumberController = TextEditingController(); + final _ifscController = TextEditingController(); + final _holderNameController = TextEditingController(); + String? _accountType; + bool _isPrimary = false; + bool _isActive = true; + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + final detail = widget.bankDetail; + if (detail != null) { + _bankNameController.text = detail.bankName ?? ''; + _branchController.text = detail.branch ?? ''; + _accountNumberController.text = detail.accountNumber ?? ''; + _ifscController.text = detail.ifsc ?? ''; + _holderNameController.text = detail.accountHolderName ?? ''; + _accountType = detail.accountType; + _isPrimary = detail.isPrimary; + _isActive = detail.isActive; + } else { + _accountType = 'CURRENT'; + } + } + + @override + void dispose() { + _bankNameController.dispose(); + _branchController.dispose(); + _accountNumberController.dispose(); + _ifscController.dispose(); + _holderNameController.dispose(); + super.dispose(); + } + + Future _save() async { + if (!_formKey.currentState!.validate()) return; + + final payload = { + 'bank_name': _bankNameController.text.trim(), + if (_branchController.text.trim().isNotEmpty) + 'branch': _branchController.text.trim(), + if (!widget.isEditing || _accountNumberController.text.trim().isNotEmpty) + 'account_number': _accountNumberController.text.trim(), + 'ifsc': _ifscController.text.trim(), + 'account_holder_name': _holderNameController.text.trim(), + if (_accountType != null) 'account_type': _accountType, + 'is_primary': _isPrimary, + 'is_active': _isActive, + }; + + setState(() => _isSubmitting = true); + try { + final notifier = ref.read(vendorDetailProvider(widget.vendorId).notifier); + if (widget.isEditing) { + await notifier.updateBankDetail(widget.bankDetail!.id, payload); + } else { + await notifier.createBankDetail(payload); + } + if (mounted) Navigator.of(context, rootNavigator: true).pop(true); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(e.toString()))); + } + } finally { + if (mounted) setState(() => _isSubmitting = false); + } + } + + @override + Widget build(BuildContext context) { + return SidePanelScaffold( + title: widget.isEditing ? 'Edit bank detail' : 'Add bank detail', + footer: _panelFooter( + context, + isSubmitting: _isSubmitting, + saveLabel: widget.isEditing ? 'Update bank detail' : 'Save bank detail', + onSave: _save, + ), + child: Form( + key: _formKey, + child: Column( + children: [ + AppTextField( + controller: _bankNameController, + label: 'Bank Name *', + validator: (v) => Validators.required(v, fieldName: 'Bank name'), + ), + const SizedBox(height: 12), + AppTextField(controller: _branchController, label: 'Branch'), + const SizedBox(height: 12), + AppTextField( + controller: _accountNumberController, + label: widget.isEditing ? 'Account Number' : 'Account Number *', + obscureText: true, + keyboardType: TextInputType.number, + inputFormatters: Validators.accountNumberInput, + validator: widget.isEditing + ? Validators.optionalAccountNumber + : Validators.accountNumber, + ), + const SizedBox(height: 12), + SidePanelFormRow( + left: AppTextField( + controller: _ifscController, + label: 'IFSC *', + validator: Validators.ifsc, + inputFormatters: Validators.ifscInput, + ), + right: AppTextField( + controller: _holderNameController, + label: 'Account Holder *', + validator: (v) => + Validators.required(v, fieldName: 'Account holder name'), + ), + ), + const SizedBox(height: 12), + AppDropdown( + label: 'Account Type', + value: _accountType, + options: accountTypeOptions + .map((e) => AppDropdownOption(value: e.$1, label: e.$2)) + .toList(), + onChanged: (v) => setState(() => _accountType = v), + ), + const SizedBox(height: 12), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Primary account'), + value: _isPrimary, + onChanged: (v) => setState(() => _isPrimary = v), + ), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Active'), + value: _isActive, + onChanged: (v) => setState(() => _isActive = v), + ), + ], + ), + ), + ); + } +} + +Widget _panelFooter( + BuildContext context, { + required bool isSubmitting, + required String saveLabel, + required VoidCallback onSave, +}) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + OutlinedButton( + onPressed: isSubmitting + ? null + : () => Navigator.of(context, rootNavigator: true).pop(), + child: const Text('Cancel'), + ), + const SizedBox(width: 12), + AppButton( + label: saveLabel, + expand: false, + icon: Icons.check, + isLoading: isSubmitting, + onPressed: isSubmitting ? null : onSave, + ), + ], + ); +} diff --git a/lib/shared/models/grn_model.dart b/lib/shared/models/grn_model.dart new file mode 100644 index 0000000..283304c --- /dev/null +++ b/lib/shared/models/grn_model.dart @@ -0,0 +1,190 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'grn_model.freezed.dart'; +part 'grn_model.g.dart'; + +String _idFromJson(Object? value) => value?.toString() ?? ''; + +int? _intFromJsonNullable(Object? value) { + if (value == null) return null; + if (value is int) return value; + if (value is num) return value.toInt(); + return int.tryParse(value.toString()); +} + +double? _doubleFromJsonNullable(Object? value) { + if (value == null) return null; + if (value is double) return value; + if (value is num) return value.toDouble(); + return double.tryParse(value.toString()); +} + +DateTime? _dateFromJsonNullable(Object? value) { + if (value == null) return null; + if (value is DateTime) return value; + return DateTime.tryParse(value.toString()); +} + +Object? _readNestedName(Map json, String flatKey, String nestedKey) { + final flat = json[flatKey]; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json[nestedKey]; + if (nested is Map) { + return nested['name'] ?? nested['vendor_name']; + } + return null; +} + +Object? _readVendorName(Map json, String key) => + _readNestedName(json, 'vendor_name', 'vendor'); + +Object? _readWarehouseName(Map json, String key) => + _readNestedName(json, 'warehouse_name', 'warehouse'); + +Object? _readPoNumber(Map json, String key) { + final grnNumber = json['grn_number']; + if (grnNumber != null && grnNumber.toString().trim().isNotEmpty) { + return grnNumber.toString(); + } + final grnNo = json['grn_no']; + if (grnNo != null && grnNo.toString().trim().isNotEmpty) { + return grnNo.toString(); + } + return null; +} + +Object? _readPoRefNumber(Map json, String key) { + final flat = json['po_number']; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json['purchase_order'] ?? json['po']; + if (nested is Map) { + return nested['po_number'] ?? nested['po_no']; + } + return null; +} + +Object? _readItemName(Map json, String key) { + final flat = json['item_name']; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json['item']; + if (nested is Map) return nested['name'] ?? nested['item_name']; + return null; +} + +Object? _readItemCode(Map json, String key) { + final flat = json['item_code']; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json['item']; + if (nested is Map) return nested['code'] ?? nested['item_code']; + return null; +} + +Object? _readUomName(Map json, String key) { + final flat = json['uom_name']; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json['uom']; + if (nested is Map) return nested['name']; + return null; +} + +@freezed +class GrnModel with _$GrnModel { + const GrnModel._(); + + const factory GrnModel({ + @JsonKey(fromJson: _idFromJson) required String id, + @JsonKey(name: 'grn_number', readValue: _readPoNumber) String? grnNumber, + @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) DateTime? grnDate, + @Default('POSTED') String status, + @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) int? poId, + @JsonKey(name: 'po_number', readValue: _readPoRefNumber) String? poNumber, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) String? vendorName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) int? warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) String? warehouseName, + @JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo, + @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) + DateTime? vendorInvoiceDate, + @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) + double? vendorInvoiceAmount, + @JsonKey(name: 'vehicle_no') String? vehicleNo, + @JsonKey(name: 'lr_no') String? lrNo, + @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) DateTime? lrDate, + @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) int? receivedBy, + @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) + int? qualityCheckedBy, + String? remarks, + @JsonKey(name: 'cancellation_reason') String? cancellationReason, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) DateTime? updatedAt, + @Default([]) List items, + }) = _GrnModel; + + factory GrnModel.fromJson(Map json) => _$GrnModelFromJson(json); + + bool get canEdit => status.toUpperCase() == 'POSTED'; + + bool get canCancel => status.toUpperCase() == 'POSTED'; +} + +@freezed +class GrnItemModel with _$GrnItemModel { + const factory GrnItemModel({ + @JsonKey(fromJson: _idFromJson) required String id, + @JsonKey(name: 'grn_id', fromJson: _idFromJson) String? grnId, + @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) int? poItemId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) int? itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) String? itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) String? itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) int? lineNo, + @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) double? currentQty, + @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) double? acceptedQty, + @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) double? damagedQty, + @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) double? shortQty, + @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) double? excessQty, + @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) double? rejectedQty, + @JsonKey(name: 'rejection_reason') String? rejectionReason, + @JsonKey(fromJson: _doubleFromJsonNullable) double? rate, + @JsonKey(name: 'batch_no') String? batchNo, + @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) DateTime? mfgDate, + @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) DateTime? expiryDate, + @JsonKey(name: 'storage_location') String? storageLocation, + @JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable) + int? assetCategoryId, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName, + String? remarks, + }) = _GrnItemModel; + + factory GrnItemModel.fromJson(Map json) => + _$GrnItemModelFromJson(json); +} + +@freezed +class GrnListQuery with _$GrnListQuery { + const factory GrnListQuery({ + @Default(1) int page, + @Default(20) int limit, + String? search, + String? status, + int? poId, + int? vendorId, + int? warehouseId, + String? dateFrom, + String? dateTo, + }) = _GrnListQuery; +} + +const grnStatusOptions = [ + ('POSTED', 'Posted'), + ('CANCELLED', 'Cancelled'), +]; + +String grnStatusLabel(String? value) { + if (value == null) return '—'; + return grnStatusOptions + .where((e) => e.$1 == value.toUpperCase()) + .map((e) => e.$2) + .firstOrNull ?? + value.replaceAll('_', ' '); +} diff --git a/lib/shared/models/grn_model.freezed.dart b/lib/shared/models/grn_model.freezed.dart new file mode 100644 index 0000000..6f339c9 --- /dev/null +++ b/lib/shared/models/grn_model.freezed.dart @@ -0,0 +1,1833 @@ +// 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 'grn_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', +); + +GrnModel _$GrnModelFromJson(Map json) { + return _GrnModel.fromJson(json); +} + +/// @nodoc +mixin _$GrnModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'grn_number', readValue: _readPoNumber) + String? get grnNumber => throw _privateConstructorUsedError; + @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) + DateTime? get grnDate => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) + int? get poId => throw _privateConstructorUsedError; + @JsonKey(name: 'po_number', readValue: _readPoRefNumber) + String? get poNumber => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) + int? get vendorId => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + String? get vendorName => throw _privateConstructorUsedError; + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + int? get warehouseId => throw _privateConstructorUsedError; + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + String? get warehouseName => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_invoice_no') + String? get vendorInvoiceNo => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) + DateTime? get vendorInvoiceDate => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) + double? get vendorInvoiceAmount => throw _privateConstructorUsedError; + @JsonKey(name: 'vehicle_no') + String? get vehicleNo => throw _privateConstructorUsedError; + @JsonKey(name: 'lr_no') + String? get lrNo => throw _privateConstructorUsedError; + @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) + DateTime? get lrDate => throw _privateConstructorUsedError; + @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) + int? get receivedBy => throw _privateConstructorUsedError; + @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) + int? get qualityCheckedBy => throw _privateConstructorUsedError; + String? get remarks => throw _privateConstructorUsedError; + @JsonKey(name: 'cancellation_reason') + String? get cancellationReason => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? get updatedAt => throw _privateConstructorUsedError; + List get items => throw _privateConstructorUsedError; + + /// Serializes this GrnModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GrnModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GrnModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GrnModelCopyWith<$Res> { + factory $GrnModelCopyWith(GrnModel value, $Res Function(GrnModel) then) = + _$GrnModelCopyWithImpl<$Res, GrnModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'grn_number', readValue: _readPoNumber) String? grnNumber, + @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) + DateTime? grnDate, + String status, + @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) int? poId, + @JsonKey(name: 'po_number', readValue: _readPoRefNumber) String? poNumber, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + String? vendorName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + int? warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + String? warehouseName, + @JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo, + @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) + DateTime? vendorInvoiceDate, + @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) + double? vendorInvoiceAmount, + @JsonKey(name: 'vehicle_no') String? vehicleNo, + @JsonKey(name: 'lr_no') String? lrNo, + @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) DateTime? lrDate, + @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) + int? receivedBy, + @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) + int? qualityCheckedBy, + String? remarks, + @JsonKey(name: 'cancellation_reason') String? cancellationReason, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? updatedAt, + List items, + }); +} + +/// @nodoc +class _$GrnModelCopyWithImpl<$Res, $Val extends GrnModel> + implements $GrnModelCopyWith<$Res> { + _$GrnModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GrnModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? grnNumber = freezed, + Object? grnDate = freezed, + Object? status = null, + Object? poId = freezed, + Object? poNumber = freezed, + Object? vendorId = freezed, + Object? vendorName = freezed, + Object? warehouseId = freezed, + Object? warehouseName = freezed, + Object? vendorInvoiceNo = freezed, + Object? vendorInvoiceDate = freezed, + Object? vendorInvoiceAmount = freezed, + Object? vehicleNo = freezed, + Object? lrNo = freezed, + Object? lrDate = freezed, + Object? receivedBy = freezed, + Object? qualityCheckedBy = freezed, + Object? remarks = freezed, + Object? cancellationReason = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? items = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + grnNumber: freezed == grnNumber + ? _value.grnNumber + : grnNumber // ignore: cast_nullable_to_non_nullable + as String?, + grnDate: freezed == grnDate + ? _value.grnDate + : grnDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + poId: freezed == poId + ? _value.poId + : poId // ignore: cast_nullable_to_non_nullable + as int?, + poNumber: freezed == poNumber + ? _value.poNumber + : poNumber // ignore: cast_nullable_to_non_nullable + as String?, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as int?, + vendorName: freezed == vendorName + ? _value.vendorName + : vendorName // ignore: cast_nullable_to_non_nullable + as String?, + warehouseId: freezed == warehouseId + ? _value.warehouseId + : warehouseId // ignore: cast_nullable_to_non_nullable + as int?, + warehouseName: freezed == warehouseName + ? _value.warehouseName + : warehouseName // ignore: cast_nullable_to_non_nullable + as String?, + vendorInvoiceNo: freezed == vendorInvoiceNo + ? _value.vendorInvoiceNo + : vendorInvoiceNo // ignore: cast_nullable_to_non_nullable + as String?, + vendorInvoiceDate: freezed == vendorInvoiceDate + ? _value.vendorInvoiceDate + : vendorInvoiceDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + vendorInvoiceAmount: freezed == vendorInvoiceAmount + ? _value.vendorInvoiceAmount + : vendorInvoiceAmount // ignore: cast_nullable_to_non_nullable + as double?, + vehicleNo: freezed == vehicleNo + ? _value.vehicleNo + : vehicleNo // ignore: cast_nullable_to_non_nullable + as String?, + lrNo: freezed == lrNo + ? _value.lrNo + : lrNo // ignore: cast_nullable_to_non_nullable + as String?, + lrDate: freezed == lrDate + ? _value.lrDate + : lrDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + receivedBy: freezed == receivedBy + ? _value.receivedBy + : receivedBy // ignore: cast_nullable_to_non_nullable + as int?, + qualityCheckedBy: freezed == qualityCheckedBy + ? _value.qualityCheckedBy + : qualityCheckedBy // ignore: cast_nullable_to_non_nullable + as int?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + cancellationReason: freezed == cancellationReason + ? _value.cancellationReason + : cancellationReason // 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?, + items: null == items + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$GrnModelImplCopyWith<$Res> + implements $GrnModelCopyWith<$Res> { + factory _$$GrnModelImplCopyWith( + _$GrnModelImpl value, + $Res Function(_$GrnModelImpl) then, + ) = __$$GrnModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'grn_number', readValue: _readPoNumber) String? grnNumber, + @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) + DateTime? grnDate, + String status, + @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) int? poId, + @JsonKey(name: 'po_number', readValue: _readPoRefNumber) String? poNumber, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + String? vendorName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + int? warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + String? warehouseName, + @JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo, + @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) + DateTime? vendorInvoiceDate, + @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) + double? vendorInvoiceAmount, + @JsonKey(name: 'vehicle_no') String? vehicleNo, + @JsonKey(name: 'lr_no') String? lrNo, + @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) DateTime? lrDate, + @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) + int? receivedBy, + @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) + int? qualityCheckedBy, + String? remarks, + @JsonKey(name: 'cancellation_reason') String? cancellationReason, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? updatedAt, + List items, + }); +} + +/// @nodoc +class __$$GrnModelImplCopyWithImpl<$Res> + extends _$GrnModelCopyWithImpl<$Res, _$GrnModelImpl> + implements _$$GrnModelImplCopyWith<$Res> { + __$$GrnModelImplCopyWithImpl( + _$GrnModelImpl _value, + $Res Function(_$GrnModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GrnModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? grnNumber = freezed, + Object? grnDate = freezed, + Object? status = null, + Object? poId = freezed, + Object? poNumber = freezed, + Object? vendorId = freezed, + Object? vendorName = freezed, + Object? warehouseId = freezed, + Object? warehouseName = freezed, + Object? vendorInvoiceNo = freezed, + Object? vendorInvoiceDate = freezed, + Object? vendorInvoiceAmount = freezed, + Object? vehicleNo = freezed, + Object? lrNo = freezed, + Object? lrDate = freezed, + Object? receivedBy = freezed, + Object? qualityCheckedBy = freezed, + Object? remarks = freezed, + Object? cancellationReason = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? items = null, + }) { + return _then( + _$GrnModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + grnNumber: freezed == grnNumber + ? _value.grnNumber + : grnNumber // ignore: cast_nullable_to_non_nullable + as String?, + grnDate: freezed == grnDate + ? _value.grnDate + : grnDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + poId: freezed == poId + ? _value.poId + : poId // ignore: cast_nullable_to_non_nullable + as int?, + poNumber: freezed == poNumber + ? _value.poNumber + : poNumber // ignore: cast_nullable_to_non_nullable + as String?, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as int?, + vendorName: freezed == vendorName + ? _value.vendorName + : vendorName // ignore: cast_nullable_to_non_nullable + as String?, + warehouseId: freezed == warehouseId + ? _value.warehouseId + : warehouseId // ignore: cast_nullable_to_non_nullable + as int?, + warehouseName: freezed == warehouseName + ? _value.warehouseName + : warehouseName // ignore: cast_nullable_to_non_nullable + as String?, + vendorInvoiceNo: freezed == vendorInvoiceNo + ? _value.vendorInvoiceNo + : vendorInvoiceNo // ignore: cast_nullable_to_non_nullable + as String?, + vendorInvoiceDate: freezed == vendorInvoiceDate + ? _value.vendorInvoiceDate + : vendorInvoiceDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + vendorInvoiceAmount: freezed == vendorInvoiceAmount + ? _value.vendorInvoiceAmount + : vendorInvoiceAmount // ignore: cast_nullable_to_non_nullable + as double?, + vehicleNo: freezed == vehicleNo + ? _value.vehicleNo + : vehicleNo // ignore: cast_nullable_to_non_nullable + as String?, + lrNo: freezed == lrNo + ? _value.lrNo + : lrNo // ignore: cast_nullable_to_non_nullable + as String?, + lrDate: freezed == lrDate + ? _value.lrDate + : lrDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + receivedBy: freezed == receivedBy + ? _value.receivedBy + : receivedBy // ignore: cast_nullable_to_non_nullable + as int?, + qualityCheckedBy: freezed == qualityCheckedBy + ? _value.qualityCheckedBy + : qualityCheckedBy // ignore: cast_nullable_to_non_nullable + as int?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + cancellationReason: freezed == cancellationReason + ? _value.cancellationReason + : cancellationReason // 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?, + items: null == items + ? _value._items + : items // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$GrnModelImpl extends _GrnModel { + const _$GrnModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + @JsonKey(name: 'grn_number', readValue: _readPoNumber) this.grnNumber, + @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) this.grnDate, + this.status = 'POSTED', + @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) this.poId, + @JsonKey(name: 'po_number', readValue: _readPoRefNumber) this.poNumber, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) this.vendorName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + this.warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + this.warehouseName, + @JsonKey(name: 'vendor_invoice_no') this.vendorInvoiceNo, + @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) + this.vendorInvoiceDate, + @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) + this.vendorInvoiceAmount, + @JsonKey(name: 'vehicle_no') this.vehicleNo, + @JsonKey(name: 'lr_no') this.lrNo, + @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) this.lrDate, + @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) + this.receivedBy, + @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) + this.qualityCheckedBy, + this.remarks, + @JsonKey(name: 'cancellation_reason') this.cancellationReason, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + this.createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + this.updatedAt, + final List items = const [], + }) : _items = items, + super._(); + + factory _$GrnModelImpl.fromJson(Map json) => + _$$GrnModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + @JsonKey(name: 'grn_number', readValue: _readPoNumber) + final String? grnNumber; + @override + @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) + final DateTime? grnDate; + @override + @JsonKey() + final String status; + @override + @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) + final int? poId; + @override + @JsonKey(name: 'po_number', readValue: _readPoRefNumber) + final String? poNumber; + @override + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) + final int? vendorId; + @override + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + final String? vendorName; + @override + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + final int? warehouseId; + @override + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + final String? warehouseName; + @override + @JsonKey(name: 'vendor_invoice_no') + final String? vendorInvoiceNo; + @override + @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) + final DateTime? vendorInvoiceDate; + @override + @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) + final double? vendorInvoiceAmount; + @override + @JsonKey(name: 'vehicle_no') + final String? vehicleNo; + @override + @JsonKey(name: 'lr_no') + final String? lrNo; + @override + @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) + final DateTime? lrDate; + @override + @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) + final int? receivedBy; + @override + @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) + final int? qualityCheckedBy; + @override + final String? remarks; + @override + @JsonKey(name: 'cancellation_reason') + final String? cancellationReason; + @override + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + final DateTime? createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + final DateTime? updatedAt; + final List _items; + @override + @JsonKey() + List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } + + @override + String toString() { + return 'GrnModel(id: $id, grnNumber: $grnNumber, grnDate: $grnDate, status: $status, poId: $poId, poNumber: $poNumber, vendorId: $vendorId, vendorName: $vendorName, warehouseId: $warehouseId, warehouseName: $warehouseName, vendorInvoiceNo: $vendorInvoiceNo, vendorInvoiceDate: $vendorInvoiceDate, vendorInvoiceAmount: $vendorInvoiceAmount, vehicleNo: $vehicleNo, lrNo: $lrNo, lrDate: $lrDate, receivedBy: $receivedBy, qualityCheckedBy: $qualityCheckedBy, remarks: $remarks, cancellationReason: $cancellationReason, createdAt: $createdAt, updatedAt: $updatedAt, items: $items)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GrnModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.grnNumber, grnNumber) || + other.grnNumber == grnNumber) && + (identical(other.grnDate, grnDate) || other.grnDate == grnDate) && + (identical(other.status, status) || other.status == status) && + (identical(other.poId, poId) || other.poId == poId) && + (identical(other.poNumber, poNumber) || + other.poNumber == poNumber) && + (identical(other.vendorId, vendorId) || + other.vendorId == vendorId) && + (identical(other.vendorName, vendorName) || + other.vendorName == vendorName) && + (identical(other.warehouseId, warehouseId) || + other.warehouseId == warehouseId) && + (identical(other.warehouseName, warehouseName) || + other.warehouseName == warehouseName) && + (identical(other.vendorInvoiceNo, vendorInvoiceNo) || + other.vendorInvoiceNo == vendorInvoiceNo) && + (identical(other.vendorInvoiceDate, vendorInvoiceDate) || + other.vendorInvoiceDate == vendorInvoiceDate) && + (identical(other.vendorInvoiceAmount, vendorInvoiceAmount) || + other.vendorInvoiceAmount == vendorInvoiceAmount) && + (identical(other.vehicleNo, vehicleNo) || + other.vehicleNo == vehicleNo) && + (identical(other.lrNo, lrNo) || other.lrNo == lrNo) && + (identical(other.lrDate, lrDate) || other.lrDate == lrDate) && + (identical(other.receivedBy, receivedBy) || + other.receivedBy == receivedBy) && + (identical(other.qualityCheckedBy, qualityCheckedBy) || + other.qualityCheckedBy == qualityCheckedBy) && + (identical(other.remarks, remarks) || other.remarks == remarks) && + (identical(other.cancellationReason, cancellationReason) || + other.cancellationReason == cancellationReason) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + const DeepCollectionEquality().equals(other._items, _items)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hashAll([ + runtimeType, + id, + grnNumber, + grnDate, + status, + poId, + poNumber, + vendorId, + vendorName, + warehouseId, + warehouseName, + vendorInvoiceNo, + vendorInvoiceDate, + vendorInvoiceAmount, + vehicleNo, + lrNo, + lrDate, + receivedBy, + qualityCheckedBy, + remarks, + cancellationReason, + createdAt, + updatedAt, + const DeepCollectionEquality().hash(_items), + ]); + + /// Create a copy of GrnModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GrnModelImplCopyWith<_$GrnModelImpl> get copyWith => + __$$GrnModelImplCopyWithImpl<_$GrnModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$GrnModelImplToJson(this); + } +} + +abstract class _GrnModel extends GrnModel { + const factory _GrnModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + @JsonKey(name: 'grn_number', readValue: _readPoNumber) + final String? grnNumber, + @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) + final DateTime? grnDate, + final String status, + @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) final int? poId, + @JsonKey(name: 'po_number', readValue: _readPoRefNumber) + final String? poNumber, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) + final int? vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + final String? vendorName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + final int? warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + final String? warehouseName, + @JsonKey(name: 'vendor_invoice_no') final String? vendorInvoiceNo, + @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) + final DateTime? vendorInvoiceDate, + @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) + final double? vendorInvoiceAmount, + @JsonKey(name: 'vehicle_no') final String? vehicleNo, + @JsonKey(name: 'lr_no') final String? lrNo, + @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) + final DateTime? lrDate, + @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) + final int? receivedBy, + @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) + final int? qualityCheckedBy, + final String? remarks, + @JsonKey(name: 'cancellation_reason') final String? cancellationReason, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + final DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + final DateTime? updatedAt, + final List items, + }) = _$GrnModelImpl; + const _GrnModel._() : super._(); + + factory _GrnModel.fromJson(Map json) = + _$GrnModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + @JsonKey(name: 'grn_number', readValue: _readPoNumber) + String? get grnNumber; + @override + @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) + DateTime? get grnDate; + @override + String get status; + @override + @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) + int? get poId; + @override + @JsonKey(name: 'po_number', readValue: _readPoRefNumber) + String? get poNumber; + @override + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) + int? get vendorId; + @override + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + String? get vendorName; + @override + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + int? get warehouseId; + @override + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + String? get warehouseName; + @override + @JsonKey(name: 'vendor_invoice_no') + String? get vendorInvoiceNo; + @override + @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) + DateTime? get vendorInvoiceDate; + @override + @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) + double? get vendorInvoiceAmount; + @override + @JsonKey(name: 'vehicle_no') + String? get vehicleNo; + @override + @JsonKey(name: 'lr_no') + String? get lrNo; + @override + @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) + DateTime? get lrDate; + @override + @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) + int? get receivedBy; + @override + @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) + int? get qualityCheckedBy; + @override + String? get remarks; + @override + @JsonKey(name: 'cancellation_reason') + String? get cancellationReason; + @override + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? get createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? get updatedAt; + @override + List get items; + + /// Create a copy of GrnModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GrnModelImplCopyWith<_$GrnModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +GrnItemModel _$GrnItemModelFromJson(Map json) { + return _GrnItemModel.fromJson(json); +} + +/// @nodoc +mixin _$GrnItemModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'grn_id', fromJson: _idFromJson) + String? get grnId => throw _privateConstructorUsedError; + @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) + int? get poItemId => throw _privateConstructorUsedError; + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) + int? get itemId => throw _privateConstructorUsedError; + @JsonKey(name: 'item_code', readValue: _readItemCode) + String? get itemCode => throw _privateConstructorUsedError; + @JsonKey(name: 'item_name', readValue: _readItemName) + String? get itemName => throw _privateConstructorUsedError; + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) + int? get lineNo => throw _privateConstructorUsedError; + @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) + double? get currentQty => throw _privateConstructorUsedError; + @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) + double? get acceptedQty => throw _privateConstructorUsedError; + @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) + double? get damagedQty => throw _privateConstructorUsedError; + @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) + double? get shortQty => throw _privateConstructorUsedError; + @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) + double? get excessQty => throw _privateConstructorUsedError; + @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) + double? get rejectedQty => throw _privateConstructorUsedError; + @JsonKey(name: 'rejection_reason') + String? get rejectionReason => throw _privateConstructorUsedError; + @JsonKey(fromJson: _doubleFromJsonNullable) + double? get rate => throw _privateConstructorUsedError; + @JsonKey(name: 'batch_no') + String? get batchNo => throw _privateConstructorUsedError; + @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) + DateTime? get mfgDate => throw _privateConstructorUsedError; + @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) + DateTime? get expiryDate => throw _privateConstructorUsedError; + @JsonKey(name: 'storage_location') + String? get storageLocation => throw _privateConstructorUsedError; + @JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable) + int? get assetCategoryId => throw _privateConstructorUsedError; + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) + int? get uomId => throw _privateConstructorUsedError; + @JsonKey(name: 'uom_name', readValue: _readUomName) + String? get uomName => throw _privateConstructorUsedError; + String? get remarks => throw _privateConstructorUsedError; + + /// Serializes this GrnItemModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of GrnItemModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GrnItemModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GrnItemModelCopyWith<$Res> { + factory $GrnItemModelCopyWith( + GrnItemModel value, + $Res Function(GrnItemModel) then, + ) = _$GrnItemModelCopyWithImpl<$Res, GrnItemModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'grn_id', fromJson: _idFromJson) String? grnId, + @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) int? poItemId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) int? itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) String? itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) String? itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) int? lineNo, + @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) + double? currentQty, + @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) + double? acceptedQty, + @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) + double? damagedQty, + @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) + double? shortQty, + @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) + double? excessQty, + @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) + double? rejectedQty, + @JsonKey(name: 'rejection_reason') String? rejectionReason, + @JsonKey(fromJson: _doubleFromJsonNullable) double? rate, + @JsonKey(name: 'batch_no') String? batchNo, + @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) + DateTime? mfgDate, + @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) + DateTime? expiryDate, + @JsonKey(name: 'storage_location') String? storageLocation, + @JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable) + int? assetCategoryId, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName, + String? remarks, + }); +} + +/// @nodoc +class _$GrnItemModelCopyWithImpl<$Res, $Val extends GrnItemModel> + implements $GrnItemModelCopyWith<$Res> { + _$GrnItemModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GrnItemModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? grnId = freezed, + Object? poItemId = freezed, + Object? itemId = freezed, + Object? itemCode = freezed, + Object? itemName = freezed, + Object? lineNo = freezed, + Object? currentQty = freezed, + Object? acceptedQty = freezed, + Object? damagedQty = freezed, + Object? shortQty = freezed, + Object? excessQty = freezed, + Object? rejectedQty = freezed, + Object? rejectionReason = freezed, + Object? rate = freezed, + Object? batchNo = freezed, + Object? mfgDate = freezed, + Object? expiryDate = freezed, + Object? storageLocation = freezed, + Object? assetCategoryId = freezed, + Object? uomId = freezed, + Object? uomName = freezed, + Object? remarks = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + grnId: freezed == grnId + ? _value.grnId + : grnId // ignore: cast_nullable_to_non_nullable + as String?, + poItemId: freezed == poItemId + ? _value.poItemId + : poItemId // ignore: cast_nullable_to_non_nullable + as int?, + itemId: freezed == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as int?, + itemCode: freezed == itemCode + ? _value.itemCode + : itemCode // ignore: cast_nullable_to_non_nullable + as String?, + itemName: freezed == itemName + ? _value.itemName + : itemName // ignore: cast_nullable_to_non_nullable + as String?, + lineNo: freezed == lineNo + ? _value.lineNo + : lineNo // ignore: cast_nullable_to_non_nullable + as int?, + currentQty: freezed == currentQty + ? _value.currentQty + : currentQty // ignore: cast_nullable_to_non_nullable + as double?, + acceptedQty: freezed == acceptedQty + ? _value.acceptedQty + : acceptedQty // ignore: cast_nullable_to_non_nullable + as double?, + damagedQty: freezed == damagedQty + ? _value.damagedQty + : damagedQty // ignore: cast_nullable_to_non_nullable + as double?, + shortQty: freezed == shortQty + ? _value.shortQty + : shortQty // ignore: cast_nullable_to_non_nullable + as double?, + excessQty: freezed == excessQty + ? _value.excessQty + : excessQty // ignore: cast_nullable_to_non_nullable + as double?, + rejectedQty: freezed == rejectedQty + ? _value.rejectedQty + : rejectedQty // ignore: cast_nullable_to_non_nullable + as double?, + rejectionReason: freezed == rejectionReason + ? _value.rejectionReason + : rejectionReason // ignore: cast_nullable_to_non_nullable + as String?, + rate: freezed == rate + ? _value.rate + : rate // ignore: cast_nullable_to_non_nullable + as double?, + batchNo: freezed == batchNo + ? _value.batchNo + : batchNo // ignore: cast_nullable_to_non_nullable + as String?, + mfgDate: freezed == mfgDate + ? _value.mfgDate + : mfgDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + expiryDate: freezed == expiryDate + ? _value.expiryDate + : expiryDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + storageLocation: freezed == storageLocation + ? _value.storageLocation + : storageLocation // ignore: cast_nullable_to_non_nullable + as String?, + assetCategoryId: freezed == assetCategoryId + ? _value.assetCategoryId + : assetCategoryId // ignore: cast_nullable_to_non_nullable + as int?, + uomId: freezed == uomId + ? _value.uomId + : uomId // ignore: cast_nullable_to_non_nullable + as int?, + uomName: freezed == uomName + ? _value.uomName + : uomName // ignore: cast_nullable_to_non_nullable + as String?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$GrnItemModelImplCopyWith<$Res> + implements $GrnItemModelCopyWith<$Res> { + factory _$$GrnItemModelImplCopyWith( + _$GrnItemModelImpl value, + $Res Function(_$GrnItemModelImpl) then, + ) = __$$GrnItemModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'grn_id', fromJson: _idFromJson) String? grnId, + @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) int? poItemId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) int? itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) String? itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) String? itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) int? lineNo, + @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) + double? currentQty, + @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) + double? acceptedQty, + @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) + double? damagedQty, + @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) + double? shortQty, + @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) + double? excessQty, + @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) + double? rejectedQty, + @JsonKey(name: 'rejection_reason') String? rejectionReason, + @JsonKey(fromJson: _doubleFromJsonNullable) double? rate, + @JsonKey(name: 'batch_no') String? batchNo, + @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) + DateTime? mfgDate, + @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) + DateTime? expiryDate, + @JsonKey(name: 'storage_location') String? storageLocation, + @JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable) + int? assetCategoryId, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName, + String? remarks, + }); +} + +/// @nodoc +class __$$GrnItemModelImplCopyWithImpl<$Res> + extends _$GrnItemModelCopyWithImpl<$Res, _$GrnItemModelImpl> + implements _$$GrnItemModelImplCopyWith<$Res> { + __$$GrnItemModelImplCopyWithImpl( + _$GrnItemModelImpl _value, + $Res Function(_$GrnItemModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GrnItemModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? grnId = freezed, + Object? poItemId = freezed, + Object? itemId = freezed, + Object? itemCode = freezed, + Object? itemName = freezed, + Object? lineNo = freezed, + Object? currentQty = freezed, + Object? acceptedQty = freezed, + Object? damagedQty = freezed, + Object? shortQty = freezed, + Object? excessQty = freezed, + Object? rejectedQty = freezed, + Object? rejectionReason = freezed, + Object? rate = freezed, + Object? batchNo = freezed, + Object? mfgDate = freezed, + Object? expiryDate = freezed, + Object? storageLocation = freezed, + Object? assetCategoryId = freezed, + Object? uomId = freezed, + Object? uomName = freezed, + Object? remarks = freezed, + }) { + return _then( + _$GrnItemModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + grnId: freezed == grnId + ? _value.grnId + : grnId // ignore: cast_nullable_to_non_nullable + as String?, + poItemId: freezed == poItemId + ? _value.poItemId + : poItemId // ignore: cast_nullable_to_non_nullable + as int?, + itemId: freezed == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as int?, + itemCode: freezed == itemCode + ? _value.itemCode + : itemCode // ignore: cast_nullable_to_non_nullable + as String?, + itemName: freezed == itemName + ? _value.itemName + : itemName // ignore: cast_nullable_to_non_nullable + as String?, + lineNo: freezed == lineNo + ? _value.lineNo + : lineNo // ignore: cast_nullable_to_non_nullable + as int?, + currentQty: freezed == currentQty + ? _value.currentQty + : currentQty // ignore: cast_nullable_to_non_nullable + as double?, + acceptedQty: freezed == acceptedQty + ? _value.acceptedQty + : acceptedQty // ignore: cast_nullable_to_non_nullable + as double?, + damagedQty: freezed == damagedQty + ? _value.damagedQty + : damagedQty // ignore: cast_nullable_to_non_nullable + as double?, + shortQty: freezed == shortQty + ? _value.shortQty + : shortQty // ignore: cast_nullable_to_non_nullable + as double?, + excessQty: freezed == excessQty + ? _value.excessQty + : excessQty // ignore: cast_nullable_to_non_nullable + as double?, + rejectedQty: freezed == rejectedQty + ? _value.rejectedQty + : rejectedQty // ignore: cast_nullable_to_non_nullable + as double?, + rejectionReason: freezed == rejectionReason + ? _value.rejectionReason + : rejectionReason // ignore: cast_nullable_to_non_nullable + as String?, + rate: freezed == rate + ? _value.rate + : rate // ignore: cast_nullable_to_non_nullable + as double?, + batchNo: freezed == batchNo + ? _value.batchNo + : batchNo // ignore: cast_nullable_to_non_nullable + as String?, + mfgDate: freezed == mfgDate + ? _value.mfgDate + : mfgDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + expiryDate: freezed == expiryDate + ? _value.expiryDate + : expiryDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + storageLocation: freezed == storageLocation + ? _value.storageLocation + : storageLocation // ignore: cast_nullable_to_non_nullable + as String?, + assetCategoryId: freezed == assetCategoryId + ? _value.assetCategoryId + : assetCategoryId // ignore: cast_nullable_to_non_nullable + as int?, + uomId: freezed == uomId + ? _value.uomId + : uomId // ignore: cast_nullable_to_non_nullable + as int?, + uomName: freezed == uomName + ? _value.uomName + : uomName // ignore: cast_nullable_to_non_nullable + as String?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$GrnItemModelImpl implements _GrnItemModel { + const _$GrnItemModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + @JsonKey(name: 'grn_id', fromJson: _idFromJson) this.grnId, + @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) this.poItemId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) this.itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) this.itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) this.itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) this.lineNo, + @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) + this.currentQty, + @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) + this.acceptedQty, + @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) + this.damagedQty, + @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) + this.shortQty, + @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) + this.excessQty, + @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) + this.rejectedQty, + @JsonKey(name: 'rejection_reason') this.rejectionReason, + @JsonKey(fromJson: _doubleFromJsonNullable) this.rate, + @JsonKey(name: 'batch_no') this.batchNo, + @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) this.mfgDate, + @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) + this.expiryDate, + @JsonKey(name: 'storage_location') this.storageLocation, + @JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable) + this.assetCategoryId, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) this.uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) this.uomName, + this.remarks, + }); + + factory _$GrnItemModelImpl.fromJson(Map json) => + _$$GrnItemModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + @JsonKey(name: 'grn_id', fromJson: _idFromJson) + final String? grnId; + @override + @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) + final int? poItemId; + @override + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) + final int? itemId; + @override + @JsonKey(name: 'item_code', readValue: _readItemCode) + final String? itemCode; + @override + @JsonKey(name: 'item_name', readValue: _readItemName) + final String? itemName; + @override + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) + final int? lineNo; + @override + @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) + final double? currentQty; + @override + @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) + final double? acceptedQty; + @override + @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) + final double? damagedQty; + @override + @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) + final double? shortQty; + @override + @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) + final double? excessQty; + @override + @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) + final double? rejectedQty; + @override + @JsonKey(name: 'rejection_reason') + final String? rejectionReason; + @override + @JsonKey(fromJson: _doubleFromJsonNullable) + final double? rate; + @override + @JsonKey(name: 'batch_no') + final String? batchNo; + @override + @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) + final DateTime? mfgDate; + @override + @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) + final DateTime? expiryDate; + @override + @JsonKey(name: 'storage_location') + final String? storageLocation; + @override + @JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable) + final int? assetCategoryId; + @override + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) + final int? uomId; + @override + @JsonKey(name: 'uom_name', readValue: _readUomName) + final String? uomName; + @override + final String? remarks; + + @override + String toString() { + return 'GrnItemModel(id: $id, grnId: $grnId, poItemId: $poItemId, itemId: $itemId, itemCode: $itemCode, itemName: $itemName, lineNo: $lineNo, currentQty: $currentQty, acceptedQty: $acceptedQty, damagedQty: $damagedQty, shortQty: $shortQty, excessQty: $excessQty, rejectedQty: $rejectedQty, rejectionReason: $rejectionReason, rate: $rate, batchNo: $batchNo, mfgDate: $mfgDate, expiryDate: $expiryDate, storageLocation: $storageLocation, assetCategoryId: $assetCategoryId, uomId: $uomId, uomName: $uomName, remarks: $remarks)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GrnItemModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.grnId, grnId) || other.grnId == grnId) && + (identical(other.poItemId, poItemId) || + other.poItemId == poItemId) && + (identical(other.itemId, itemId) || other.itemId == itemId) && + (identical(other.itemCode, itemCode) || + other.itemCode == itemCode) && + (identical(other.itemName, itemName) || + other.itemName == itemName) && + (identical(other.lineNo, lineNo) || other.lineNo == lineNo) && + (identical(other.currentQty, currentQty) || + other.currentQty == currentQty) && + (identical(other.acceptedQty, acceptedQty) || + other.acceptedQty == acceptedQty) && + (identical(other.damagedQty, damagedQty) || + other.damagedQty == damagedQty) && + (identical(other.shortQty, shortQty) || + other.shortQty == shortQty) && + (identical(other.excessQty, excessQty) || + other.excessQty == excessQty) && + (identical(other.rejectedQty, rejectedQty) || + other.rejectedQty == rejectedQty) && + (identical(other.rejectionReason, rejectionReason) || + other.rejectionReason == rejectionReason) && + (identical(other.rate, rate) || other.rate == rate) && + (identical(other.batchNo, batchNo) || other.batchNo == batchNo) && + (identical(other.mfgDate, mfgDate) || other.mfgDate == mfgDate) && + (identical(other.expiryDate, expiryDate) || + other.expiryDate == expiryDate) && + (identical(other.storageLocation, storageLocation) || + other.storageLocation == storageLocation) && + (identical(other.assetCategoryId, assetCategoryId) || + other.assetCategoryId == assetCategoryId) && + (identical(other.uomId, uomId) || other.uomId == uomId) && + (identical(other.uomName, uomName) || other.uomName == uomName) && + (identical(other.remarks, remarks) || other.remarks == remarks)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hashAll([ + runtimeType, + id, + grnId, + poItemId, + itemId, + itemCode, + itemName, + lineNo, + currentQty, + acceptedQty, + damagedQty, + shortQty, + excessQty, + rejectedQty, + rejectionReason, + rate, + batchNo, + mfgDate, + expiryDate, + storageLocation, + assetCategoryId, + uomId, + uomName, + remarks, + ]); + + /// Create a copy of GrnItemModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GrnItemModelImplCopyWith<_$GrnItemModelImpl> get copyWith => + __$$GrnItemModelImplCopyWithImpl<_$GrnItemModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$GrnItemModelImplToJson(this); + } +} + +abstract class _GrnItemModel implements GrnItemModel { + const factory _GrnItemModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + @JsonKey(name: 'grn_id', fromJson: _idFromJson) final String? grnId, + @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) + final int? poItemId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) final int? itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) + final String? itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) + final String? itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) final int? lineNo, + @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) + final double? currentQty, + @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) + final double? acceptedQty, + @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) + final double? damagedQty, + @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) + final double? shortQty, + @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) + final double? excessQty, + @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) + final double? rejectedQty, + @JsonKey(name: 'rejection_reason') final String? rejectionReason, + @JsonKey(fromJson: _doubleFromJsonNullable) final double? rate, + @JsonKey(name: 'batch_no') final String? batchNo, + @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) + final DateTime? mfgDate, + @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) + final DateTime? expiryDate, + @JsonKey(name: 'storage_location') final String? storageLocation, + @JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable) + final int? assetCategoryId, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) final int? uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) final String? uomName, + final String? remarks, + }) = _$GrnItemModelImpl; + + factory _GrnItemModel.fromJson(Map json) = + _$GrnItemModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + @JsonKey(name: 'grn_id', fromJson: _idFromJson) + String? get grnId; + @override + @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) + int? get poItemId; + @override + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) + int? get itemId; + @override + @JsonKey(name: 'item_code', readValue: _readItemCode) + String? get itemCode; + @override + @JsonKey(name: 'item_name', readValue: _readItemName) + String? get itemName; + @override + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) + int? get lineNo; + @override + @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) + double? get currentQty; + @override + @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) + double? get acceptedQty; + @override + @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) + double? get damagedQty; + @override + @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) + double? get shortQty; + @override + @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) + double? get excessQty; + @override + @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) + double? get rejectedQty; + @override + @JsonKey(name: 'rejection_reason') + String? get rejectionReason; + @override + @JsonKey(fromJson: _doubleFromJsonNullable) + double? get rate; + @override + @JsonKey(name: 'batch_no') + String? get batchNo; + @override + @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) + DateTime? get mfgDate; + @override + @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) + DateTime? get expiryDate; + @override + @JsonKey(name: 'storage_location') + String? get storageLocation; + @override + @JsonKey(name: 'asset_category_id', fromJson: _intFromJsonNullable) + int? get assetCategoryId; + @override + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) + int? get uomId; + @override + @JsonKey(name: 'uom_name', readValue: _readUomName) + String? get uomName; + @override + String? get remarks; + + /// Create a copy of GrnItemModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GrnItemModelImplCopyWith<_$GrnItemModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$GrnListQuery { + int get page => throw _privateConstructorUsedError; + int get limit => throw _privateConstructorUsedError; + String? get search => throw _privateConstructorUsedError; + String? get status => throw _privateConstructorUsedError; + int? get poId => throw _privateConstructorUsedError; + int? get vendorId => throw _privateConstructorUsedError; + int? get warehouseId => throw _privateConstructorUsedError; + String? get dateFrom => throw _privateConstructorUsedError; + String? get dateTo => throw _privateConstructorUsedError; + + /// Create a copy of GrnListQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $GrnListQueryCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $GrnListQueryCopyWith<$Res> { + factory $GrnListQueryCopyWith( + GrnListQuery value, + $Res Function(GrnListQuery) then, + ) = _$GrnListQueryCopyWithImpl<$Res, GrnListQuery>; + @useResult + $Res call({ + int page, + int limit, + String? search, + String? status, + int? poId, + int? vendorId, + int? warehouseId, + String? dateFrom, + String? dateTo, + }); +} + +/// @nodoc +class _$GrnListQueryCopyWithImpl<$Res, $Val extends GrnListQuery> + implements $GrnListQueryCopyWith<$Res> { + _$GrnListQueryCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of GrnListQuery + /// 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? poId = freezed, + Object? vendorId = freezed, + Object? warehouseId = freezed, + Object? dateFrom = freezed, + Object? dateTo = 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?, + poId: freezed == poId + ? _value.poId + : poId // ignore: cast_nullable_to_non_nullable + as int?, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as int?, + warehouseId: freezed == warehouseId + ? _value.warehouseId + : warehouseId // ignore: cast_nullable_to_non_nullable + as int?, + dateFrom: freezed == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as String?, + dateTo: freezed == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$GrnListQueryImplCopyWith<$Res> + implements $GrnListQueryCopyWith<$Res> { + factory _$$GrnListQueryImplCopyWith( + _$GrnListQueryImpl value, + $Res Function(_$GrnListQueryImpl) then, + ) = __$$GrnListQueryImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int page, + int limit, + String? search, + String? status, + int? poId, + int? vendorId, + int? warehouseId, + String? dateFrom, + String? dateTo, + }); +} + +/// @nodoc +class __$$GrnListQueryImplCopyWithImpl<$Res> + extends _$GrnListQueryCopyWithImpl<$Res, _$GrnListQueryImpl> + implements _$$GrnListQueryImplCopyWith<$Res> { + __$$GrnListQueryImplCopyWithImpl( + _$GrnListQueryImpl _value, + $Res Function(_$GrnListQueryImpl) _then, + ) : super(_value, _then); + + /// Create a copy of GrnListQuery + /// 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? poId = freezed, + Object? vendorId = freezed, + Object? warehouseId = freezed, + Object? dateFrom = freezed, + Object? dateTo = freezed, + }) { + return _then( + _$GrnListQueryImpl( + 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?, + poId: freezed == poId + ? _value.poId + : poId // ignore: cast_nullable_to_non_nullable + as int?, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as int?, + warehouseId: freezed == warehouseId + ? _value.warehouseId + : warehouseId // ignore: cast_nullable_to_non_nullable + as int?, + dateFrom: freezed == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as String?, + dateTo: freezed == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc + +class _$GrnListQueryImpl implements _GrnListQuery { + const _$GrnListQueryImpl({ + this.page = 1, + this.limit = 20, + this.search, + this.status, + this.poId, + this.vendorId, + this.warehouseId, + this.dateFrom, + this.dateTo, + }); + + @override + @JsonKey() + final int page; + @override + @JsonKey() + final int limit; + @override + final String? search; + @override + final String? status; + @override + final int? poId; + @override + final int? vendorId; + @override + final int? warehouseId; + @override + final String? dateFrom; + @override + final String? dateTo; + + @override + String toString() { + return 'GrnListQuery(page: $page, limit: $limit, search: $search, status: $status, poId: $poId, vendorId: $vendorId, warehouseId: $warehouseId, dateFrom: $dateFrom, dateTo: $dateTo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GrnListQueryImpl && + (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.poId, poId) || other.poId == poId) && + (identical(other.vendorId, vendorId) || + other.vendorId == vendorId) && + (identical(other.warehouseId, warehouseId) || + other.warehouseId == warehouseId) && + (identical(other.dateFrom, dateFrom) || + other.dateFrom == dateFrom) && + (identical(other.dateTo, dateTo) || other.dateTo == dateTo)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + page, + limit, + search, + status, + poId, + vendorId, + warehouseId, + dateFrom, + dateTo, + ); + + /// Create a copy of GrnListQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$GrnListQueryImplCopyWith<_$GrnListQueryImpl> get copyWith => + __$$GrnListQueryImplCopyWithImpl<_$GrnListQueryImpl>(this, _$identity); +} + +abstract class _GrnListQuery implements GrnListQuery { + const factory _GrnListQuery({ + final int page, + final int limit, + final String? search, + final String? status, + final int? poId, + final int? vendorId, + final int? warehouseId, + final String? dateFrom, + final String? dateTo, + }) = _$GrnListQueryImpl; + + @override + int get page; + @override + int get limit; + @override + String? get search; + @override + String? get status; + @override + int? get poId; + @override + int? get vendorId; + @override + int? get warehouseId; + @override + String? get dateFrom; + @override + String? get dateTo; + + /// Create a copy of GrnListQuery + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$GrnListQueryImplCopyWith<_$GrnListQueryImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/grn_model.g.dart b/lib/shared/models/grn_model.g.dart new file mode 100644 index 0000000..7818157 --- /dev/null +++ b/lib/shared/models/grn_model.g.dart @@ -0,0 +1,121 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'grn_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$GrnModelImpl _$$GrnModelImplFromJson(Map json) => + _$GrnModelImpl( + id: _idFromJson(json['id']), + grnNumber: _readPoNumber(json, 'grn_number') as String?, + grnDate: _dateFromJsonNullable(json['grn_date']), + status: json['status'] as String? ?? 'POSTED', + poId: _intFromJsonNullable(json['po_id']), + poNumber: _readPoRefNumber(json, 'po_number') as String?, + vendorId: _intFromJsonNullable(json['vendor_id']), + vendorName: _readVendorName(json, 'vendor_name') as String?, + warehouseId: _intFromJsonNullable(json['warehouse_id']), + warehouseName: _readWarehouseName(json, 'warehouse_name') as String?, + vendorInvoiceNo: json['vendor_invoice_no'] as String?, + vendorInvoiceDate: _dateFromJsonNullable(json['vendor_invoice_date']), + vendorInvoiceAmount: _doubleFromJsonNullable( + json['vendor_invoice_amount'], + ), + vehicleNo: json['vehicle_no'] as String?, + lrNo: json['lr_no'] as String?, + lrDate: _dateFromJsonNullable(json['lr_date']), + receivedBy: _intFromJsonNullable(json['received_by']), + qualityCheckedBy: _intFromJsonNullable(json['quality_checked_by']), + remarks: json['remarks'] as String?, + cancellationReason: json['cancellation_reason'] as String?, + createdAt: _dateFromJsonNullable(json['created_at']), + updatedAt: _dateFromJsonNullable(json['updated_at']), + items: + (json['items'] as List?) + ?.map((e) => GrnItemModel.fromJson(e as Map)) + .toList() ?? + const [], + ); + +Map _$$GrnModelImplToJson(_$GrnModelImpl instance) => + { + 'id': instance.id, + 'grn_number': instance.grnNumber, + 'grn_date': instance.grnDate?.toIso8601String(), + 'status': instance.status, + 'po_id': instance.poId, + 'po_number': instance.poNumber, + 'vendor_id': instance.vendorId, + 'vendor_name': instance.vendorName, + 'warehouse_id': instance.warehouseId, + 'warehouse_name': instance.warehouseName, + 'vendor_invoice_no': instance.vendorInvoiceNo, + 'vendor_invoice_date': instance.vendorInvoiceDate?.toIso8601String(), + 'vendor_invoice_amount': instance.vendorInvoiceAmount, + 'vehicle_no': instance.vehicleNo, + 'lr_no': instance.lrNo, + 'lr_date': instance.lrDate?.toIso8601String(), + 'received_by': instance.receivedBy, + 'quality_checked_by': instance.qualityCheckedBy, + 'remarks': instance.remarks, + 'cancellation_reason': instance.cancellationReason, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + 'items': instance.items, + }; + +_$GrnItemModelImpl _$$GrnItemModelImplFromJson(Map json) => + _$GrnItemModelImpl( + id: _idFromJson(json['id']), + grnId: _idFromJson(json['grn_id']), + poItemId: _intFromJsonNullable(json['po_item_id']), + itemId: _intFromJsonNullable(json['item_id']), + itemCode: _readItemCode(json, 'item_code') as String?, + itemName: _readItemName(json, 'item_name') as String?, + lineNo: _intFromJsonNullable(json['line_no']), + currentQty: _doubleFromJsonNullable(json['current_qty']), + acceptedQty: _doubleFromJsonNullable(json['accepted_qty']), + damagedQty: _doubleFromJsonNullable(json['damaged_qty']), + shortQty: _doubleFromJsonNullable(json['short_qty']), + excessQty: _doubleFromJsonNullable(json['excess_qty']), + rejectedQty: _doubleFromJsonNullable(json['rejected_qty']), + rejectionReason: json['rejection_reason'] as String?, + rate: _doubleFromJsonNullable(json['rate']), + batchNo: json['batch_no'] as String?, + mfgDate: _dateFromJsonNullable(json['mfg_date']), + expiryDate: _dateFromJsonNullable(json['expiry_date']), + storageLocation: json['storage_location'] as String?, + assetCategoryId: _intFromJsonNullable(json['asset_category_id']), + uomId: _intFromJsonNullable(json['uom_id']), + uomName: _readUomName(json, 'uom_name') as String?, + remarks: json['remarks'] as String?, + ); + +Map _$$GrnItemModelImplToJson(_$GrnItemModelImpl instance) => + { + 'id': instance.id, + 'grn_id': instance.grnId, + 'po_item_id': instance.poItemId, + 'item_id': instance.itemId, + 'item_code': instance.itemCode, + 'item_name': instance.itemName, + 'line_no': instance.lineNo, + 'current_qty': instance.currentQty, + 'accepted_qty': instance.acceptedQty, + 'damaged_qty': instance.damagedQty, + 'short_qty': instance.shortQty, + 'excess_qty': instance.excessQty, + 'rejected_qty': instance.rejectedQty, + 'rejection_reason': instance.rejectionReason, + 'rate': instance.rate, + 'batch_no': instance.batchNo, + 'mfg_date': instance.mfgDate?.toIso8601String(), + 'expiry_date': instance.expiryDate?.toIso8601String(), + 'storage_location': instance.storageLocation, + 'asset_category_id': instance.assetCategoryId, + 'uom_id': instance.uomId, + 'uom_name': instance.uomName, + 'remarks': instance.remarks, + }; diff --git a/lib/shared/models/purchase_order_model.dart b/lib/shared/models/purchase_order_model.dart new file mode 100644 index 0000000..6247aa2 --- /dev/null +++ b/lib/shared/models/purchase_order_model.dart @@ -0,0 +1,239 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'purchase_order_model.freezed.dart'; +part 'purchase_order_model.g.dart'; + +String _idFromJson(Object? value) => value?.toString() ?? ''; + +int? _intFromJsonNullable(Object? value) { + if (value == null) return null; + if (value is int) return value; + if (value is num) return value.toInt(); + return int.tryParse(value.toString()); +} + +double? _doubleFromJsonNullable(Object? value) { + if (value == null) return null; + if (value is double) return value; + if (value is num) return value.toDouble(); + return double.tryParse(value.toString()); +} + +DateTime? _dateFromJsonNullable(Object? value) { + if (value == null) return null; + if (value is DateTime) return value; + return DateTime.tryParse(value.toString()); +} + +Object? _readNestedName(Map json, String flatKey, String nestedKey) { + final flat = json[flatKey]; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json[nestedKey]; + if (nested is Map) { + return nested['name'] ?? nested['vendor_name']; + } + return null; +} + +Object? _readVendorName(Map json, String key) => + _readNestedName(json, 'vendor_name', 'vendor'); + +Object? _readPlantName(Map json, String key) => + _readNestedName(json, 'plant_name', 'plant'); + +Object? _readWarehouseName(Map json, String key) => + _readNestedName(json, 'warehouse_name', 'warehouse'); + +Object? _readItemName(Map json, String key) { + final flat = json['item_name']; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json['item']; + if (nested is Map) return nested['name']; + return null; +} + +Object? _readItemCode(Map json, String key) { + final flat = json['item_code']; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json['item']; + if (nested is Map) return nested['code']; + return null; +} + +Object? _readUomName(Map json, String key) { + final flat = json['uom_name']; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json['uom']; + if (nested is Map) return nested['name']; + return null; +} + +Object? _readPoNumber(Map json, String key) { + final poNumber = json['po_number']; + if (poNumber != null && poNumber.toString().trim().isNotEmpty) { + return poNumber.toString(); + } + final poNo = json['po_no']; + if (poNo != null && poNo.toString().trim().isNotEmpty) { + return poNo.toString(); + } + return null; +} + +Object? _readGrandTotal(Map json, String key) => + _doubleFromJsonNullable(json['grand_total'] ?? json['total_amount']); + +Object? _readTaxTotal(Map json, String key) => + _doubleFromJsonNullable(json['tax_total'] ?? json['tax_amount']); + +Object? _readSubTotal(Map json, String key) => + _doubleFromJsonNullable(json['sub_total'] ?? json['taxable_amount']); + +@freezed +class PurchaseOrderModel with _$PurchaseOrderModel { + const PurchaseOrderModel._(); + + const factory PurchaseOrderModel({ + @JsonKey(fromJson: _idFromJson) required String id, + @JsonKey(name: 'po_number', readValue: _readPoNumber) String? poNo, + @JsonKey(name: 'po_date', fromJson: _dateFromJsonNullable) DateTime? poDate, + @JsonKey(name: 'po_type') String? poType, + @Default('DRAFT') String status, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) String? vendorName, + @JsonKey(name: 'plant_id', fromJson: _intFromJsonNullable) int? plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) String? plantName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) int? warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) String? warehouseName, + @JsonKey(name: 'brand_id', fromJson: _intFromJsonNullable) int? brandId, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) int? paymentTermId, + @JsonKey(name: 'delivery_term_id', fromJson: _intFromJsonNullable) int? deliveryTermId, + @JsonKey(name: 'expected_delivery_date', fromJson: _dateFromJsonNullable) + DateTime? expectedDeliveryDate, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? discountAmount, + @JsonKey(name: 'freight_charges', fromJson: _doubleFromJsonNullable) + double? freightCharges, + @JsonKey(name: 'other_charges', fromJson: _doubleFromJsonNullable) + double? otherCharges, + @JsonKey(name: 'sub_total', readValue: _readSubTotal) + double? taxableAmount, + @JsonKey(name: 'tax_total', readValue: _readTaxTotal) + double? taxAmount, + @JsonKey(name: 'grand_total', readValue: _readGrandTotal) + double? totalAmount, + @JsonKey(name: 'terms_and_conditions') String? termsAndConditions, + String? remarks, + @JsonKey(name: 'revision_no', fromJson: _intFromJsonNullable) int? revisionNo, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) DateTime? updatedAt, + @Default([]) List items, + }) = _PurchaseOrderModel; + + factory PurchaseOrderModel.fromJson(Map json) => + _$PurchaseOrderModelFromJson(json); + + bool get canEdit => + status.toUpperCase() == 'DRAFT' || status.toUpperCase() == 'REJECTED'; + + bool get canDelete => canEdit; + + bool get canSubmit => status.toUpperCase() == 'DRAFT'; + + bool get canApprove { + final s = status.toUpperCase(); + return s == 'SUBMITTED' || s == 'PENDING_APPROVAL' || s == 'PENDING'; + } + + bool get canReject => canApprove; + + bool get canAmend => status.toUpperCase() == 'APPROVED'; + + bool get canCancel { + final s = status.toUpperCase(); + return s != 'CANCELLED' && s != 'DRAFT'; + } +} + +@freezed +class PurchaseOrderItemModel with _$PurchaseOrderItemModel { + const factory PurchaseOrderItemModel({ + @JsonKey(fromJson: _idFromJson) required String id, + @JsonKey(name: 'po_id', fromJson: _idFromJson) String? poId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) int? itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) String? itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) String? itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) int? lineNo, + @JsonKey(name: 'ordered_qty', fromJson: _doubleFromJsonNullable) + double? orderedQty, + @JsonKey(name: 'received_qty', fromJson: _doubleFromJsonNullable) + double? receivedQty, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName, + @JsonKey(fromJson: _doubleFromJsonNullable) double? rate, + @JsonKey(name: 'discount_pct', fromJson: _doubleFromJsonNullable) + double? discountPct, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? discountAmount, + @JsonKey(name: 'gst_rate_id', fromJson: _intFromJsonNullable) int? gstRateId, + @JsonKey(name: 'hsn_code_id', fromJson: _intFromJsonNullable) int? hsnCodeId, + @JsonKey(name: 'line_amount', fromJson: _doubleFromJsonNullable) + double? lineAmount, + String? remarks, + }) = _PurchaseOrderItemModel; + + factory PurchaseOrderItemModel.fromJson(Map json) => + _$PurchaseOrderItemModelFromJson(json); +} + +@freezed +class PurchaseOrderListQuery with _$PurchaseOrderListQuery { + const factory PurchaseOrderListQuery({ + @Default(1) int page, + @Default(20) int limit, + String? search, + String? status, + String? poType, + int? vendorId, + int? plantId, + String? dateFrom, + String? dateTo, + }) = _PurchaseOrderListQuery; +} + +const poTypeOptions = [ + ('RAW_MATERIAL', 'Raw Material'), + ('PACKING_MATERIAL', 'Packing Material'), + ('ASSET_CAPITAL', 'Asset / Capital'), + ('SERVICE', 'Service'), + ('GENERAL', 'General'), +]; + +const poStatusOptions = [ + ('DRAFT', 'Draft'), + ('SUBMITTED', 'Submitted'), + ('PENDING_APPROVAL', 'Pending Approval'), + ('APPROVED', 'Approved'), + ('REJECTED', 'Rejected'), + ('CANCELLED', 'Cancelled'), + ('PARTIALLY_RECEIVED', 'Partially Received'), + ('FULLY_RECEIVED', 'Fully Received'), +]; + +String poTypeLabel(String? value) { + if (value == null) return '—'; + return poTypeOptions + .where((e) => e.$1 == value) + .map((e) => e.$2) + .firstOrNull ?? + value; +} + +String poStatusLabel(String? value) { + if (value == null) return '—'; + return poStatusOptions + .where((e) => e.$1 == value) + .map((e) => e.$2) + .firstOrNull ?? + value.replaceAll('_', ' '); +} diff --git a/lib/shared/models/purchase_order_model.freezed.dart b/lib/shared/models/purchase_order_model.freezed.dart new file mode 100644 index 0000000..ef0b6dc --- /dev/null +++ b/lib/shared/models/purchase_order_model.freezed.dart @@ -0,0 +1,1813 @@ +// 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 'purchase_order_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', +); + +PurchaseOrderModel _$PurchaseOrderModelFromJson(Map json) { + return _PurchaseOrderModel.fromJson(json); +} + +/// @nodoc +mixin _$PurchaseOrderModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'po_number', readValue: _readPoNumber) + String? get poNo => throw _privateConstructorUsedError; + @JsonKey(name: 'po_date', fromJson: _dateFromJsonNullable) + DateTime? get poDate => throw _privateConstructorUsedError; + @JsonKey(name: 'po_type') + String? get poType => throw _privateConstructorUsedError; + String get status => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) + int? get vendorId => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + String? get vendorName => throw _privateConstructorUsedError; + @JsonKey(name: 'plant_id', fromJson: _intFromJsonNullable) + int? get plantId => throw _privateConstructorUsedError; + @JsonKey(name: 'plant_name', readValue: _readPlantName) + String? get plantName => throw _privateConstructorUsedError; + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + int? get warehouseId => throw _privateConstructorUsedError; + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + String? get warehouseName => throw _privateConstructorUsedError; + @JsonKey(name: 'brand_id', fromJson: _intFromJsonNullable) + int? get brandId => throw _privateConstructorUsedError; + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? get paymentTermId => throw _privateConstructorUsedError; + @JsonKey(name: 'delivery_term_id', fromJson: _intFromJsonNullable) + int? get deliveryTermId => throw _privateConstructorUsedError; + @JsonKey(name: 'expected_delivery_date', fromJson: _dateFromJsonNullable) + DateTime? get expectedDeliveryDate => throw _privateConstructorUsedError; + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? get discountAmount => throw _privateConstructorUsedError; + @JsonKey(name: 'freight_charges', fromJson: _doubleFromJsonNullable) + double? get freightCharges => throw _privateConstructorUsedError; + @JsonKey(name: 'other_charges', fromJson: _doubleFromJsonNullable) + double? get otherCharges => throw _privateConstructorUsedError; + @JsonKey(name: 'sub_total', readValue: _readSubTotal) + double? get taxableAmount => throw _privateConstructorUsedError; + @JsonKey(name: 'tax_total', readValue: _readTaxTotal) + double? get taxAmount => throw _privateConstructorUsedError; + @JsonKey(name: 'grand_total', readValue: _readGrandTotal) + double? get totalAmount => throw _privateConstructorUsedError; + @JsonKey(name: 'terms_and_conditions') + String? get termsAndConditions => throw _privateConstructorUsedError; + String? get remarks => throw _privateConstructorUsedError; + @JsonKey(name: 'revision_no', fromJson: _intFromJsonNullable) + int? get revisionNo => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? get updatedAt => throw _privateConstructorUsedError; + List get items => throw _privateConstructorUsedError; + + /// Serializes this PurchaseOrderModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PurchaseOrderModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PurchaseOrderModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PurchaseOrderModelCopyWith<$Res> { + factory $PurchaseOrderModelCopyWith( + PurchaseOrderModel value, + $Res Function(PurchaseOrderModel) then, + ) = _$PurchaseOrderModelCopyWithImpl<$Res, PurchaseOrderModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'po_number', readValue: _readPoNumber) String? poNo, + @JsonKey(name: 'po_date', fromJson: _dateFromJsonNullable) DateTime? poDate, + @JsonKey(name: 'po_type') String? poType, + String status, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + String? vendorName, + @JsonKey(name: 'plant_id', fromJson: _intFromJsonNullable) int? plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) String? plantName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + int? warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + String? warehouseName, + @JsonKey(name: 'brand_id', fromJson: _intFromJsonNullable) int? brandId, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? paymentTermId, + @JsonKey(name: 'delivery_term_id', fromJson: _intFromJsonNullable) + int? deliveryTermId, + @JsonKey(name: 'expected_delivery_date', fromJson: _dateFromJsonNullable) + DateTime? expectedDeliveryDate, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? discountAmount, + @JsonKey(name: 'freight_charges', fromJson: _doubleFromJsonNullable) + double? freightCharges, + @JsonKey(name: 'other_charges', fromJson: _doubleFromJsonNullable) + double? otherCharges, + @JsonKey(name: 'sub_total', readValue: _readSubTotal) double? taxableAmount, + @JsonKey(name: 'tax_total', readValue: _readTaxTotal) double? taxAmount, + @JsonKey(name: 'grand_total', readValue: _readGrandTotal) + double? totalAmount, + @JsonKey(name: 'terms_and_conditions') String? termsAndConditions, + String? remarks, + @JsonKey(name: 'revision_no', fromJson: _intFromJsonNullable) + int? revisionNo, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? updatedAt, + List items, + }); +} + +/// @nodoc +class _$PurchaseOrderModelCopyWithImpl<$Res, $Val extends PurchaseOrderModel> + implements $PurchaseOrderModelCopyWith<$Res> { + _$PurchaseOrderModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PurchaseOrderModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? poNo = freezed, + Object? poDate = freezed, + Object? poType = freezed, + Object? status = null, + Object? vendorId = freezed, + Object? vendorName = freezed, + Object? plantId = freezed, + Object? plantName = freezed, + Object? warehouseId = freezed, + Object? warehouseName = freezed, + Object? brandId = freezed, + Object? paymentTermId = freezed, + Object? deliveryTermId = freezed, + Object? expectedDeliveryDate = freezed, + Object? discountAmount = freezed, + Object? freightCharges = freezed, + Object? otherCharges = freezed, + Object? taxableAmount = freezed, + Object? taxAmount = freezed, + Object? totalAmount = freezed, + Object? termsAndConditions = freezed, + Object? remarks = freezed, + Object? revisionNo = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? items = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + poNo: freezed == poNo + ? _value.poNo + : poNo // ignore: cast_nullable_to_non_nullable + as String?, + poDate: freezed == poDate + ? _value.poDate + : poDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + poType: freezed == poType + ? _value.poType + : poType // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as int?, + vendorName: freezed == vendorName + ? _value.vendorName + : vendorName // ignore: cast_nullable_to_non_nullable + as String?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as int?, + plantName: freezed == plantName + ? _value.plantName + : plantName // ignore: cast_nullable_to_non_nullable + as String?, + warehouseId: freezed == warehouseId + ? _value.warehouseId + : warehouseId // ignore: cast_nullable_to_non_nullable + as int?, + warehouseName: freezed == warehouseName + ? _value.warehouseName + : warehouseName // ignore: cast_nullable_to_non_nullable + as String?, + brandId: freezed == brandId + ? _value.brandId + : brandId // ignore: cast_nullable_to_non_nullable + as int?, + paymentTermId: freezed == paymentTermId + ? _value.paymentTermId + : paymentTermId // ignore: cast_nullable_to_non_nullable + as int?, + deliveryTermId: freezed == deliveryTermId + ? _value.deliveryTermId + : deliveryTermId // ignore: cast_nullable_to_non_nullable + as int?, + expectedDeliveryDate: freezed == expectedDeliveryDate + ? _value.expectedDeliveryDate + : expectedDeliveryDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + discountAmount: freezed == discountAmount + ? _value.discountAmount + : discountAmount // ignore: cast_nullable_to_non_nullable + as double?, + freightCharges: freezed == freightCharges + ? _value.freightCharges + : freightCharges // ignore: cast_nullable_to_non_nullable + as double?, + otherCharges: freezed == otherCharges + ? _value.otherCharges + : otherCharges // ignore: cast_nullable_to_non_nullable + as double?, + taxableAmount: freezed == taxableAmount + ? _value.taxableAmount + : taxableAmount // ignore: cast_nullable_to_non_nullable + as double?, + taxAmount: freezed == taxAmount + ? _value.taxAmount + : taxAmount // ignore: cast_nullable_to_non_nullable + as double?, + totalAmount: freezed == totalAmount + ? _value.totalAmount + : totalAmount // ignore: cast_nullable_to_non_nullable + as double?, + termsAndConditions: freezed == termsAndConditions + ? _value.termsAndConditions + : termsAndConditions // ignore: cast_nullable_to_non_nullable + as String?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + revisionNo: freezed == revisionNo + ? _value.revisionNo + : revisionNo // ignore: cast_nullable_to_non_nullable + as int?, + 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?, + items: null == items + ? _value.items + : items // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PurchaseOrderModelImplCopyWith<$Res> + implements $PurchaseOrderModelCopyWith<$Res> { + factory _$$PurchaseOrderModelImplCopyWith( + _$PurchaseOrderModelImpl value, + $Res Function(_$PurchaseOrderModelImpl) then, + ) = __$$PurchaseOrderModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'po_number', readValue: _readPoNumber) String? poNo, + @JsonKey(name: 'po_date', fromJson: _dateFromJsonNullable) DateTime? poDate, + @JsonKey(name: 'po_type') String? poType, + String status, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + String? vendorName, + @JsonKey(name: 'plant_id', fromJson: _intFromJsonNullable) int? plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) String? plantName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + int? warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + String? warehouseName, + @JsonKey(name: 'brand_id', fromJson: _intFromJsonNullable) int? brandId, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? paymentTermId, + @JsonKey(name: 'delivery_term_id', fromJson: _intFromJsonNullable) + int? deliveryTermId, + @JsonKey(name: 'expected_delivery_date', fromJson: _dateFromJsonNullable) + DateTime? expectedDeliveryDate, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? discountAmount, + @JsonKey(name: 'freight_charges', fromJson: _doubleFromJsonNullable) + double? freightCharges, + @JsonKey(name: 'other_charges', fromJson: _doubleFromJsonNullable) + double? otherCharges, + @JsonKey(name: 'sub_total', readValue: _readSubTotal) double? taxableAmount, + @JsonKey(name: 'tax_total', readValue: _readTaxTotal) double? taxAmount, + @JsonKey(name: 'grand_total', readValue: _readGrandTotal) + double? totalAmount, + @JsonKey(name: 'terms_and_conditions') String? termsAndConditions, + String? remarks, + @JsonKey(name: 'revision_no', fromJson: _intFromJsonNullable) + int? revisionNo, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? updatedAt, + List items, + }); +} + +/// @nodoc +class __$$PurchaseOrderModelImplCopyWithImpl<$Res> + extends _$PurchaseOrderModelCopyWithImpl<$Res, _$PurchaseOrderModelImpl> + implements _$$PurchaseOrderModelImplCopyWith<$Res> { + __$$PurchaseOrderModelImplCopyWithImpl( + _$PurchaseOrderModelImpl _value, + $Res Function(_$PurchaseOrderModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PurchaseOrderModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? poNo = freezed, + Object? poDate = freezed, + Object? poType = freezed, + Object? status = null, + Object? vendorId = freezed, + Object? vendorName = freezed, + Object? plantId = freezed, + Object? plantName = freezed, + Object? warehouseId = freezed, + Object? warehouseName = freezed, + Object? brandId = freezed, + Object? paymentTermId = freezed, + Object? deliveryTermId = freezed, + Object? expectedDeliveryDate = freezed, + Object? discountAmount = freezed, + Object? freightCharges = freezed, + Object? otherCharges = freezed, + Object? taxableAmount = freezed, + Object? taxAmount = freezed, + Object? totalAmount = freezed, + Object? termsAndConditions = freezed, + Object? remarks = freezed, + Object? revisionNo = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? items = null, + }) { + return _then( + _$PurchaseOrderModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + poNo: freezed == poNo + ? _value.poNo + : poNo // ignore: cast_nullable_to_non_nullable + as String?, + poDate: freezed == poDate + ? _value.poDate + : poDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + poType: freezed == poType + ? _value.poType + : poType // ignore: cast_nullable_to_non_nullable + as String?, + status: null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as String, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as int?, + vendorName: freezed == vendorName + ? _value.vendorName + : vendorName // ignore: cast_nullable_to_non_nullable + as String?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as int?, + plantName: freezed == plantName + ? _value.plantName + : plantName // ignore: cast_nullable_to_non_nullable + as String?, + warehouseId: freezed == warehouseId + ? _value.warehouseId + : warehouseId // ignore: cast_nullable_to_non_nullable + as int?, + warehouseName: freezed == warehouseName + ? _value.warehouseName + : warehouseName // ignore: cast_nullable_to_non_nullable + as String?, + brandId: freezed == brandId + ? _value.brandId + : brandId // ignore: cast_nullable_to_non_nullable + as int?, + paymentTermId: freezed == paymentTermId + ? _value.paymentTermId + : paymentTermId // ignore: cast_nullable_to_non_nullable + as int?, + deliveryTermId: freezed == deliveryTermId + ? _value.deliveryTermId + : deliveryTermId // ignore: cast_nullable_to_non_nullable + as int?, + expectedDeliveryDate: freezed == expectedDeliveryDate + ? _value.expectedDeliveryDate + : expectedDeliveryDate // ignore: cast_nullable_to_non_nullable + as DateTime?, + discountAmount: freezed == discountAmount + ? _value.discountAmount + : discountAmount // ignore: cast_nullable_to_non_nullable + as double?, + freightCharges: freezed == freightCharges + ? _value.freightCharges + : freightCharges // ignore: cast_nullable_to_non_nullable + as double?, + otherCharges: freezed == otherCharges + ? _value.otherCharges + : otherCharges // ignore: cast_nullable_to_non_nullable + as double?, + taxableAmount: freezed == taxableAmount + ? _value.taxableAmount + : taxableAmount // ignore: cast_nullable_to_non_nullable + as double?, + taxAmount: freezed == taxAmount + ? _value.taxAmount + : taxAmount // ignore: cast_nullable_to_non_nullable + as double?, + totalAmount: freezed == totalAmount + ? _value.totalAmount + : totalAmount // ignore: cast_nullable_to_non_nullable + as double?, + termsAndConditions: freezed == termsAndConditions + ? _value.termsAndConditions + : termsAndConditions // ignore: cast_nullable_to_non_nullable + as String?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + revisionNo: freezed == revisionNo + ? _value.revisionNo + : revisionNo // ignore: cast_nullable_to_non_nullable + as int?, + 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?, + items: null == items + ? _value._items + : items // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PurchaseOrderModelImpl extends _PurchaseOrderModel { + const _$PurchaseOrderModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + @JsonKey(name: 'po_number', readValue: _readPoNumber) this.poNo, + @JsonKey(name: 'po_date', fromJson: _dateFromJsonNullable) this.poDate, + @JsonKey(name: 'po_type') this.poType, + this.status = 'DRAFT', + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) this.vendorName, + @JsonKey(name: 'plant_id', fromJson: _intFromJsonNullable) this.plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) this.plantName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + this.warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + this.warehouseName, + @JsonKey(name: 'brand_id', fromJson: _intFromJsonNullable) this.brandId, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + this.paymentTermId, + @JsonKey(name: 'delivery_term_id', fromJson: _intFromJsonNullable) + this.deliveryTermId, + @JsonKey(name: 'expected_delivery_date', fromJson: _dateFromJsonNullable) + this.expectedDeliveryDate, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + this.discountAmount, + @JsonKey(name: 'freight_charges', fromJson: _doubleFromJsonNullable) + this.freightCharges, + @JsonKey(name: 'other_charges', fromJson: _doubleFromJsonNullable) + this.otherCharges, + @JsonKey(name: 'sub_total', readValue: _readSubTotal) this.taxableAmount, + @JsonKey(name: 'tax_total', readValue: _readTaxTotal) this.taxAmount, + @JsonKey(name: 'grand_total', readValue: _readGrandTotal) this.totalAmount, + @JsonKey(name: 'terms_and_conditions') this.termsAndConditions, + this.remarks, + @JsonKey(name: 'revision_no', fromJson: _intFromJsonNullable) + this.revisionNo, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + this.createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + this.updatedAt, + final List items = const [], + }) : _items = items, + super._(); + + factory _$PurchaseOrderModelImpl.fromJson(Map json) => + _$$PurchaseOrderModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + @JsonKey(name: 'po_number', readValue: _readPoNumber) + final String? poNo; + @override + @JsonKey(name: 'po_date', fromJson: _dateFromJsonNullable) + final DateTime? poDate; + @override + @JsonKey(name: 'po_type') + final String? poType; + @override + @JsonKey() + final String status; + @override + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) + final int? vendorId; + @override + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + final String? vendorName; + @override + @JsonKey(name: 'plant_id', fromJson: _intFromJsonNullable) + final int? plantId; + @override + @JsonKey(name: 'plant_name', readValue: _readPlantName) + final String? plantName; + @override + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + final int? warehouseId; + @override + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + final String? warehouseName; + @override + @JsonKey(name: 'brand_id', fromJson: _intFromJsonNullable) + final int? brandId; + @override + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + final int? paymentTermId; + @override + @JsonKey(name: 'delivery_term_id', fromJson: _intFromJsonNullable) + final int? deliveryTermId; + @override + @JsonKey(name: 'expected_delivery_date', fromJson: _dateFromJsonNullable) + final DateTime? expectedDeliveryDate; + @override + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + final double? discountAmount; + @override + @JsonKey(name: 'freight_charges', fromJson: _doubleFromJsonNullable) + final double? freightCharges; + @override + @JsonKey(name: 'other_charges', fromJson: _doubleFromJsonNullable) + final double? otherCharges; + @override + @JsonKey(name: 'sub_total', readValue: _readSubTotal) + final double? taxableAmount; + @override + @JsonKey(name: 'tax_total', readValue: _readTaxTotal) + final double? taxAmount; + @override + @JsonKey(name: 'grand_total', readValue: _readGrandTotal) + final double? totalAmount; + @override + @JsonKey(name: 'terms_and_conditions') + final String? termsAndConditions; + @override + final String? remarks; + @override + @JsonKey(name: 'revision_no', fromJson: _intFromJsonNullable) + final int? revisionNo; + @override + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + final DateTime? createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + final DateTime? updatedAt; + final List _items; + @override + @JsonKey() + List get items { + if (_items is EqualUnmodifiableListView) return _items; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_items); + } + + @override + String toString() { + return 'PurchaseOrderModel(id: $id, poNo: $poNo, poDate: $poDate, poType: $poType, status: $status, vendorId: $vendorId, vendorName: $vendorName, plantId: $plantId, plantName: $plantName, warehouseId: $warehouseId, warehouseName: $warehouseName, brandId: $brandId, paymentTermId: $paymentTermId, deliveryTermId: $deliveryTermId, expectedDeliveryDate: $expectedDeliveryDate, discountAmount: $discountAmount, freightCharges: $freightCharges, otherCharges: $otherCharges, taxableAmount: $taxableAmount, taxAmount: $taxAmount, totalAmount: $totalAmount, termsAndConditions: $termsAndConditions, remarks: $remarks, revisionNo: $revisionNo, createdAt: $createdAt, updatedAt: $updatedAt, items: $items)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PurchaseOrderModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.poNo, poNo) || other.poNo == poNo) && + (identical(other.poDate, poDate) || other.poDate == poDate) && + (identical(other.poType, poType) || other.poType == poType) && + (identical(other.status, status) || other.status == status) && + (identical(other.vendorId, vendorId) || + other.vendorId == vendorId) && + (identical(other.vendorName, vendorName) || + other.vendorName == vendorName) && + (identical(other.plantId, plantId) || other.plantId == plantId) && + (identical(other.plantName, plantName) || + other.plantName == plantName) && + (identical(other.warehouseId, warehouseId) || + other.warehouseId == warehouseId) && + (identical(other.warehouseName, warehouseName) || + other.warehouseName == warehouseName) && + (identical(other.brandId, brandId) || other.brandId == brandId) && + (identical(other.paymentTermId, paymentTermId) || + other.paymentTermId == paymentTermId) && + (identical(other.deliveryTermId, deliveryTermId) || + other.deliveryTermId == deliveryTermId) && + (identical(other.expectedDeliveryDate, expectedDeliveryDate) || + other.expectedDeliveryDate == expectedDeliveryDate) && + (identical(other.discountAmount, discountAmount) || + other.discountAmount == discountAmount) && + (identical(other.freightCharges, freightCharges) || + other.freightCharges == freightCharges) && + (identical(other.otherCharges, otherCharges) || + other.otherCharges == otherCharges) && + (identical(other.taxableAmount, taxableAmount) || + other.taxableAmount == taxableAmount) && + (identical(other.taxAmount, taxAmount) || + other.taxAmount == taxAmount) && + (identical(other.totalAmount, totalAmount) || + other.totalAmount == totalAmount) && + (identical(other.termsAndConditions, termsAndConditions) || + other.termsAndConditions == termsAndConditions) && + (identical(other.remarks, remarks) || other.remarks == remarks) && + (identical(other.revisionNo, revisionNo) || + other.revisionNo == revisionNo) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + const DeepCollectionEquality().equals(other._items, _items)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hashAll([ + runtimeType, + id, + poNo, + poDate, + poType, + status, + vendorId, + vendorName, + plantId, + plantName, + warehouseId, + warehouseName, + brandId, + paymentTermId, + deliveryTermId, + expectedDeliveryDate, + discountAmount, + freightCharges, + otherCharges, + taxableAmount, + taxAmount, + totalAmount, + termsAndConditions, + remarks, + revisionNo, + createdAt, + updatedAt, + const DeepCollectionEquality().hash(_items), + ]); + + /// Create a copy of PurchaseOrderModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PurchaseOrderModelImplCopyWith<_$PurchaseOrderModelImpl> get copyWith => + __$$PurchaseOrderModelImplCopyWithImpl<_$PurchaseOrderModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$PurchaseOrderModelImplToJson(this); + } +} + +abstract class _PurchaseOrderModel extends PurchaseOrderModel { + const factory _PurchaseOrderModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + @JsonKey(name: 'po_number', readValue: _readPoNumber) final String? poNo, + @JsonKey(name: 'po_date', fromJson: _dateFromJsonNullable) + final DateTime? poDate, + @JsonKey(name: 'po_type') final String? poType, + final String status, + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) + final int? vendorId, + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + final String? vendorName, + @JsonKey(name: 'plant_id', fromJson: _intFromJsonNullable) + final int? plantId, + @JsonKey(name: 'plant_name', readValue: _readPlantName) + final String? plantName, + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + final int? warehouseId, + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + final String? warehouseName, + @JsonKey(name: 'brand_id', fromJson: _intFromJsonNullable) + final int? brandId, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + final int? paymentTermId, + @JsonKey(name: 'delivery_term_id', fromJson: _intFromJsonNullable) + final int? deliveryTermId, + @JsonKey(name: 'expected_delivery_date', fromJson: _dateFromJsonNullable) + final DateTime? expectedDeliveryDate, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + final double? discountAmount, + @JsonKey(name: 'freight_charges', fromJson: _doubleFromJsonNullable) + final double? freightCharges, + @JsonKey(name: 'other_charges', fromJson: _doubleFromJsonNullable) + final double? otherCharges, + @JsonKey(name: 'sub_total', readValue: _readSubTotal) + final double? taxableAmount, + @JsonKey(name: 'tax_total', readValue: _readTaxTotal) + final double? taxAmount, + @JsonKey(name: 'grand_total', readValue: _readGrandTotal) + final double? totalAmount, + @JsonKey(name: 'terms_and_conditions') final String? termsAndConditions, + final String? remarks, + @JsonKey(name: 'revision_no', fromJson: _intFromJsonNullable) + final int? revisionNo, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + final DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + final DateTime? updatedAt, + final List items, + }) = _$PurchaseOrderModelImpl; + const _PurchaseOrderModel._() : super._(); + + factory _PurchaseOrderModel.fromJson(Map json) = + _$PurchaseOrderModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + @JsonKey(name: 'po_number', readValue: _readPoNumber) + String? get poNo; + @override + @JsonKey(name: 'po_date', fromJson: _dateFromJsonNullable) + DateTime? get poDate; + @override + @JsonKey(name: 'po_type') + String? get poType; + @override + String get status; + @override + @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) + int? get vendorId; + @override + @JsonKey(name: 'vendor_name', readValue: _readVendorName) + String? get vendorName; + @override + @JsonKey(name: 'plant_id', fromJson: _intFromJsonNullable) + int? get plantId; + @override + @JsonKey(name: 'plant_name', readValue: _readPlantName) + String? get plantName; + @override + @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) + int? get warehouseId; + @override + @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) + String? get warehouseName; + @override + @JsonKey(name: 'brand_id', fromJson: _intFromJsonNullable) + int? get brandId; + @override + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? get paymentTermId; + @override + @JsonKey(name: 'delivery_term_id', fromJson: _intFromJsonNullable) + int? get deliveryTermId; + @override + @JsonKey(name: 'expected_delivery_date', fromJson: _dateFromJsonNullable) + DateTime? get expectedDeliveryDate; + @override + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? get discountAmount; + @override + @JsonKey(name: 'freight_charges', fromJson: _doubleFromJsonNullable) + double? get freightCharges; + @override + @JsonKey(name: 'other_charges', fromJson: _doubleFromJsonNullable) + double? get otherCharges; + @override + @JsonKey(name: 'sub_total', readValue: _readSubTotal) + double? get taxableAmount; + @override + @JsonKey(name: 'tax_total', readValue: _readTaxTotal) + double? get taxAmount; + @override + @JsonKey(name: 'grand_total', readValue: _readGrandTotal) + double? get totalAmount; + @override + @JsonKey(name: 'terms_and_conditions') + String? get termsAndConditions; + @override + String? get remarks; + @override + @JsonKey(name: 'revision_no', fromJson: _intFromJsonNullable) + int? get revisionNo; + @override + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? get createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? get updatedAt; + @override + List get items; + + /// Create a copy of PurchaseOrderModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PurchaseOrderModelImplCopyWith<_$PurchaseOrderModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +PurchaseOrderItemModel _$PurchaseOrderItemModelFromJson( + Map json, +) { + return _PurchaseOrderItemModel.fromJson(json); +} + +/// @nodoc +mixin _$PurchaseOrderItemModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'po_id', fromJson: _idFromJson) + String? get poId => throw _privateConstructorUsedError; + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) + int? get itemId => throw _privateConstructorUsedError; + @JsonKey(name: 'item_code', readValue: _readItemCode) + String? get itemCode => throw _privateConstructorUsedError; + @JsonKey(name: 'item_name', readValue: _readItemName) + String? get itemName => throw _privateConstructorUsedError; + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) + int? get lineNo => throw _privateConstructorUsedError; + @JsonKey(name: 'ordered_qty', fromJson: _doubleFromJsonNullable) + double? get orderedQty => throw _privateConstructorUsedError; + @JsonKey(name: 'received_qty', fromJson: _doubleFromJsonNullable) + double? get receivedQty => throw _privateConstructorUsedError; + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) + int? get uomId => throw _privateConstructorUsedError; + @JsonKey(name: 'uom_name', readValue: _readUomName) + String? get uomName => throw _privateConstructorUsedError; + @JsonKey(fromJson: _doubleFromJsonNullable) + double? get rate => throw _privateConstructorUsedError; + @JsonKey(name: 'discount_pct', fromJson: _doubleFromJsonNullable) + double? get discountPct => throw _privateConstructorUsedError; + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? get discountAmount => throw _privateConstructorUsedError; + @JsonKey(name: 'gst_rate_id', fromJson: _intFromJsonNullable) + int? get gstRateId => throw _privateConstructorUsedError; + @JsonKey(name: 'hsn_code_id', fromJson: _intFromJsonNullable) + int? get hsnCodeId => throw _privateConstructorUsedError; + @JsonKey(name: 'line_amount', fromJson: _doubleFromJsonNullable) + double? get lineAmount => throw _privateConstructorUsedError; + String? get remarks => throw _privateConstructorUsedError; + + /// Serializes this PurchaseOrderItemModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PurchaseOrderItemModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PurchaseOrderItemModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PurchaseOrderItemModelCopyWith<$Res> { + factory $PurchaseOrderItemModelCopyWith( + PurchaseOrderItemModel value, + $Res Function(PurchaseOrderItemModel) then, + ) = _$PurchaseOrderItemModelCopyWithImpl<$Res, PurchaseOrderItemModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'po_id', fromJson: _idFromJson) String? poId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) int? itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) String? itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) String? itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) int? lineNo, + @JsonKey(name: 'ordered_qty', fromJson: _doubleFromJsonNullable) + double? orderedQty, + @JsonKey(name: 'received_qty', fromJson: _doubleFromJsonNullable) + double? receivedQty, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName, + @JsonKey(fromJson: _doubleFromJsonNullable) double? rate, + @JsonKey(name: 'discount_pct', fromJson: _doubleFromJsonNullable) + double? discountPct, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? discountAmount, + @JsonKey(name: 'gst_rate_id', fromJson: _intFromJsonNullable) + int? gstRateId, + @JsonKey(name: 'hsn_code_id', fromJson: _intFromJsonNullable) + int? hsnCodeId, + @JsonKey(name: 'line_amount', fromJson: _doubleFromJsonNullable) + double? lineAmount, + String? remarks, + }); +} + +/// @nodoc +class _$PurchaseOrderItemModelCopyWithImpl< + $Res, + $Val extends PurchaseOrderItemModel +> + implements $PurchaseOrderItemModelCopyWith<$Res> { + _$PurchaseOrderItemModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PurchaseOrderItemModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? poId = freezed, + Object? itemId = freezed, + Object? itemCode = freezed, + Object? itemName = freezed, + Object? lineNo = freezed, + Object? orderedQty = freezed, + Object? receivedQty = freezed, + Object? uomId = freezed, + Object? uomName = freezed, + Object? rate = freezed, + Object? discountPct = freezed, + Object? discountAmount = freezed, + Object? gstRateId = freezed, + Object? hsnCodeId = freezed, + Object? lineAmount = freezed, + Object? remarks = freezed, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + poId: freezed == poId + ? _value.poId + : poId // ignore: cast_nullable_to_non_nullable + as String?, + itemId: freezed == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as int?, + itemCode: freezed == itemCode + ? _value.itemCode + : itemCode // ignore: cast_nullable_to_non_nullable + as String?, + itemName: freezed == itemName + ? _value.itemName + : itemName // ignore: cast_nullable_to_non_nullable + as String?, + lineNo: freezed == lineNo + ? _value.lineNo + : lineNo // ignore: cast_nullable_to_non_nullable + as int?, + orderedQty: freezed == orderedQty + ? _value.orderedQty + : orderedQty // ignore: cast_nullable_to_non_nullable + as double?, + receivedQty: freezed == receivedQty + ? _value.receivedQty + : receivedQty // ignore: cast_nullable_to_non_nullable + as double?, + uomId: freezed == uomId + ? _value.uomId + : uomId // ignore: cast_nullable_to_non_nullable + as int?, + uomName: freezed == uomName + ? _value.uomName + : uomName // ignore: cast_nullable_to_non_nullable + as String?, + rate: freezed == rate + ? _value.rate + : rate // ignore: cast_nullable_to_non_nullable + as double?, + discountPct: freezed == discountPct + ? _value.discountPct + : discountPct // ignore: cast_nullable_to_non_nullable + as double?, + discountAmount: freezed == discountAmount + ? _value.discountAmount + : discountAmount // ignore: cast_nullable_to_non_nullable + as double?, + gstRateId: freezed == gstRateId + ? _value.gstRateId + : gstRateId // ignore: cast_nullable_to_non_nullable + as int?, + hsnCodeId: freezed == hsnCodeId + ? _value.hsnCodeId + : hsnCodeId // ignore: cast_nullable_to_non_nullable + as int?, + lineAmount: freezed == lineAmount + ? _value.lineAmount + : lineAmount // ignore: cast_nullable_to_non_nullable + as double?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PurchaseOrderItemModelImplCopyWith<$Res> + implements $PurchaseOrderItemModelCopyWith<$Res> { + factory _$$PurchaseOrderItemModelImplCopyWith( + _$PurchaseOrderItemModelImpl value, + $Res Function(_$PurchaseOrderItemModelImpl) then, + ) = __$$PurchaseOrderItemModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'po_id', fromJson: _idFromJson) String? poId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) int? itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) String? itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) String? itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) int? lineNo, + @JsonKey(name: 'ordered_qty', fromJson: _doubleFromJsonNullable) + double? orderedQty, + @JsonKey(name: 'received_qty', fromJson: _doubleFromJsonNullable) + double? receivedQty, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName, + @JsonKey(fromJson: _doubleFromJsonNullable) double? rate, + @JsonKey(name: 'discount_pct', fromJson: _doubleFromJsonNullable) + double? discountPct, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? discountAmount, + @JsonKey(name: 'gst_rate_id', fromJson: _intFromJsonNullable) + int? gstRateId, + @JsonKey(name: 'hsn_code_id', fromJson: _intFromJsonNullable) + int? hsnCodeId, + @JsonKey(name: 'line_amount', fromJson: _doubleFromJsonNullable) + double? lineAmount, + String? remarks, + }); +} + +/// @nodoc +class __$$PurchaseOrderItemModelImplCopyWithImpl<$Res> + extends + _$PurchaseOrderItemModelCopyWithImpl<$Res, _$PurchaseOrderItemModelImpl> + implements _$$PurchaseOrderItemModelImplCopyWith<$Res> { + __$$PurchaseOrderItemModelImplCopyWithImpl( + _$PurchaseOrderItemModelImpl _value, + $Res Function(_$PurchaseOrderItemModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PurchaseOrderItemModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? poId = freezed, + Object? itemId = freezed, + Object? itemCode = freezed, + Object? itemName = freezed, + Object? lineNo = freezed, + Object? orderedQty = freezed, + Object? receivedQty = freezed, + Object? uomId = freezed, + Object? uomName = freezed, + Object? rate = freezed, + Object? discountPct = freezed, + Object? discountAmount = freezed, + Object? gstRateId = freezed, + Object? hsnCodeId = freezed, + Object? lineAmount = freezed, + Object? remarks = freezed, + }) { + return _then( + _$PurchaseOrderItemModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + poId: freezed == poId + ? _value.poId + : poId // ignore: cast_nullable_to_non_nullable + as String?, + itemId: freezed == itemId + ? _value.itemId + : itemId // ignore: cast_nullable_to_non_nullable + as int?, + itemCode: freezed == itemCode + ? _value.itemCode + : itemCode // ignore: cast_nullable_to_non_nullable + as String?, + itemName: freezed == itemName + ? _value.itemName + : itemName // ignore: cast_nullable_to_non_nullable + as String?, + lineNo: freezed == lineNo + ? _value.lineNo + : lineNo // ignore: cast_nullable_to_non_nullable + as int?, + orderedQty: freezed == orderedQty + ? _value.orderedQty + : orderedQty // ignore: cast_nullable_to_non_nullable + as double?, + receivedQty: freezed == receivedQty + ? _value.receivedQty + : receivedQty // ignore: cast_nullable_to_non_nullable + as double?, + uomId: freezed == uomId + ? _value.uomId + : uomId // ignore: cast_nullable_to_non_nullable + as int?, + uomName: freezed == uomName + ? _value.uomName + : uomName // ignore: cast_nullable_to_non_nullable + as String?, + rate: freezed == rate + ? _value.rate + : rate // ignore: cast_nullable_to_non_nullable + as double?, + discountPct: freezed == discountPct + ? _value.discountPct + : discountPct // ignore: cast_nullable_to_non_nullable + as double?, + discountAmount: freezed == discountAmount + ? _value.discountAmount + : discountAmount // ignore: cast_nullable_to_non_nullable + as double?, + gstRateId: freezed == gstRateId + ? _value.gstRateId + : gstRateId // ignore: cast_nullable_to_non_nullable + as int?, + hsnCodeId: freezed == hsnCodeId + ? _value.hsnCodeId + : hsnCodeId // ignore: cast_nullable_to_non_nullable + as int?, + lineAmount: freezed == lineAmount + ? _value.lineAmount + : lineAmount // ignore: cast_nullable_to_non_nullable + as double?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PurchaseOrderItemModelImpl implements _PurchaseOrderItemModel { + const _$PurchaseOrderItemModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + @JsonKey(name: 'po_id', fromJson: _idFromJson) this.poId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) this.itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) this.itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) this.itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) this.lineNo, + @JsonKey(name: 'ordered_qty', fromJson: _doubleFromJsonNullable) + this.orderedQty, + @JsonKey(name: 'received_qty', fromJson: _doubleFromJsonNullable) + this.receivedQty, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) this.uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) this.uomName, + @JsonKey(fromJson: _doubleFromJsonNullable) this.rate, + @JsonKey(name: 'discount_pct', fromJson: _doubleFromJsonNullable) + this.discountPct, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + this.discountAmount, + @JsonKey(name: 'gst_rate_id', fromJson: _intFromJsonNullable) + this.gstRateId, + @JsonKey(name: 'hsn_code_id', fromJson: _intFromJsonNullable) + this.hsnCodeId, + @JsonKey(name: 'line_amount', fromJson: _doubleFromJsonNullable) + this.lineAmount, + this.remarks, + }); + + factory _$PurchaseOrderItemModelImpl.fromJson(Map json) => + _$$PurchaseOrderItemModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + @JsonKey(name: 'po_id', fromJson: _idFromJson) + final String? poId; + @override + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) + final int? itemId; + @override + @JsonKey(name: 'item_code', readValue: _readItemCode) + final String? itemCode; + @override + @JsonKey(name: 'item_name', readValue: _readItemName) + final String? itemName; + @override + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) + final int? lineNo; + @override + @JsonKey(name: 'ordered_qty', fromJson: _doubleFromJsonNullable) + final double? orderedQty; + @override + @JsonKey(name: 'received_qty', fromJson: _doubleFromJsonNullable) + final double? receivedQty; + @override + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) + final int? uomId; + @override + @JsonKey(name: 'uom_name', readValue: _readUomName) + final String? uomName; + @override + @JsonKey(fromJson: _doubleFromJsonNullable) + final double? rate; + @override + @JsonKey(name: 'discount_pct', fromJson: _doubleFromJsonNullable) + final double? discountPct; + @override + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + final double? discountAmount; + @override + @JsonKey(name: 'gst_rate_id', fromJson: _intFromJsonNullable) + final int? gstRateId; + @override + @JsonKey(name: 'hsn_code_id', fromJson: _intFromJsonNullable) + final int? hsnCodeId; + @override + @JsonKey(name: 'line_amount', fromJson: _doubleFromJsonNullable) + final double? lineAmount; + @override + final String? remarks; + + @override + String toString() { + return 'PurchaseOrderItemModel(id: $id, poId: $poId, itemId: $itemId, itemCode: $itemCode, itemName: $itemName, lineNo: $lineNo, orderedQty: $orderedQty, receivedQty: $receivedQty, uomId: $uomId, uomName: $uomName, rate: $rate, discountPct: $discountPct, discountAmount: $discountAmount, gstRateId: $gstRateId, hsnCodeId: $hsnCodeId, lineAmount: $lineAmount, remarks: $remarks)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PurchaseOrderItemModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.poId, poId) || other.poId == poId) && + (identical(other.itemId, itemId) || other.itemId == itemId) && + (identical(other.itemCode, itemCode) || + other.itemCode == itemCode) && + (identical(other.itemName, itemName) || + other.itemName == itemName) && + (identical(other.lineNo, lineNo) || other.lineNo == lineNo) && + (identical(other.orderedQty, orderedQty) || + other.orderedQty == orderedQty) && + (identical(other.receivedQty, receivedQty) || + other.receivedQty == receivedQty) && + (identical(other.uomId, uomId) || other.uomId == uomId) && + (identical(other.uomName, uomName) || other.uomName == uomName) && + (identical(other.rate, rate) || other.rate == rate) && + (identical(other.discountPct, discountPct) || + other.discountPct == discountPct) && + (identical(other.discountAmount, discountAmount) || + other.discountAmount == discountAmount) && + (identical(other.gstRateId, gstRateId) || + other.gstRateId == gstRateId) && + (identical(other.hsnCodeId, hsnCodeId) || + other.hsnCodeId == hsnCodeId) && + (identical(other.lineAmount, lineAmount) || + other.lineAmount == lineAmount) && + (identical(other.remarks, remarks) || other.remarks == remarks)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + poId, + itemId, + itemCode, + itemName, + lineNo, + orderedQty, + receivedQty, + uomId, + uomName, + rate, + discountPct, + discountAmount, + gstRateId, + hsnCodeId, + lineAmount, + remarks, + ); + + /// Create a copy of PurchaseOrderItemModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PurchaseOrderItemModelImplCopyWith<_$PurchaseOrderItemModelImpl> + get copyWith => + __$$PurchaseOrderItemModelImplCopyWithImpl<_$PurchaseOrderItemModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$PurchaseOrderItemModelImplToJson(this); + } +} + +abstract class _PurchaseOrderItemModel implements PurchaseOrderItemModel { + const factory _PurchaseOrderItemModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + @JsonKey(name: 'po_id', fromJson: _idFromJson) final String? poId, + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) final int? itemId, + @JsonKey(name: 'item_code', readValue: _readItemCode) + final String? itemCode, + @JsonKey(name: 'item_name', readValue: _readItemName) + final String? itemName, + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) final int? lineNo, + @JsonKey(name: 'ordered_qty', fromJson: _doubleFromJsonNullable) + final double? orderedQty, + @JsonKey(name: 'received_qty', fromJson: _doubleFromJsonNullable) + final double? receivedQty, + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) final int? uomId, + @JsonKey(name: 'uom_name', readValue: _readUomName) final String? uomName, + @JsonKey(fromJson: _doubleFromJsonNullable) final double? rate, + @JsonKey(name: 'discount_pct', fromJson: _doubleFromJsonNullable) + final double? discountPct, + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + final double? discountAmount, + @JsonKey(name: 'gst_rate_id', fromJson: _intFromJsonNullable) + final int? gstRateId, + @JsonKey(name: 'hsn_code_id', fromJson: _intFromJsonNullable) + final int? hsnCodeId, + @JsonKey(name: 'line_amount', fromJson: _doubleFromJsonNullable) + final double? lineAmount, + final String? remarks, + }) = _$PurchaseOrderItemModelImpl; + + factory _PurchaseOrderItemModel.fromJson(Map json) = + _$PurchaseOrderItemModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + @JsonKey(name: 'po_id', fromJson: _idFromJson) + String? get poId; + @override + @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) + int? get itemId; + @override + @JsonKey(name: 'item_code', readValue: _readItemCode) + String? get itemCode; + @override + @JsonKey(name: 'item_name', readValue: _readItemName) + String? get itemName; + @override + @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) + int? get lineNo; + @override + @JsonKey(name: 'ordered_qty', fromJson: _doubleFromJsonNullable) + double? get orderedQty; + @override + @JsonKey(name: 'received_qty', fromJson: _doubleFromJsonNullable) + double? get receivedQty; + @override + @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) + int? get uomId; + @override + @JsonKey(name: 'uom_name', readValue: _readUomName) + String? get uomName; + @override + @JsonKey(fromJson: _doubleFromJsonNullable) + double? get rate; + @override + @JsonKey(name: 'discount_pct', fromJson: _doubleFromJsonNullable) + double? get discountPct; + @override + @JsonKey(name: 'discount_amount', fromJson: _doubleFromJsonNullable) + double? get discountAmount; + @override + @JsonKey(name: 'gst_rate_id', fromJson: _intFromJsonNullable) + int? get gstRateId; + @override + @JsonKey(name: 'hsn_code_id', fromJson: _intFromJsonNullable) + int? get hsnCodeId; + @override + @JsonKey(name: 'line_amount', fromJson: _doubleFromJsonNullable) + double? get lineAmount; + @override + String? get remarks; + + /// Create a copy of PurchaseOrderItemModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PurchaseOrderItemModelImplCopyWith<_$PurchaseOrderItemModelImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$PurchaseOrderListQuery { + int get page => throw _privateConstructorUsedError; + int get limit => throw _privateConstructorUsedError; + String? get search => throw _privateConstructorUsedError; + String? get status => throw _privateConstructorUsedError; + String? get poType => throw _privateConstructorUsedError; + int? get vendorId => throw _privateConstructorUsedError; + int? get plantId => throw _privateConstructorUsedError; + String? get dateFrom => throw _privateConstructorUsedError; + String? get dateTo => throw _privateConstructorUsedError; + + /// Create a copy of PurchaseOrderListQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PurchaseOrderListQueryCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PurchaseOrderListQueryCopyWith<$Res> { + factory $PurchaseOrderListQueryCopyWith( + PurchaseOrderListQuery value, + $Res Function(PurchaseOrderListQuery) then, + ) = _$PurchaseOrderListQueryCopyWithImpl<$Res, PurchaseOrderListQuery>; + @useResult + $Res call({ + int page, + int limit, + String? search, + String? status, + String? poType, + int? vendorId, + int? plantId, + String? dateFrom, + String? dateTo, + }); +} + +/// @nodoc +class _$PurchaseOrderListQueryCopyWithImpl< + $Res, + $Val extends PurchaseOrderListQuery +> + implements $PurchaseOrderListQueryCopyWith<$Res> { + _$PurchaseOrderListQueryCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PurchaseOrderListQuery + /// 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? poType = freezed, + Object? vendorId = freezed, + Object? plantId = freezed, + Object? dateFrom = freezed, + Object? dateTo = 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?, + poType: freezed == poType + ? _value.poType + : poType // ignore: cast_nullable_to_non_nullable + as String?, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as int?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as int?, + dateFrom: freezed == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as String?, + dateTo: freezed == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as String?, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PurchaseOrderListQueryImplCopyWith<$Res> + implements $PurchaseOrderListQueryCopyWith<$Res> { + factory _$$PurchaseOrderListQueryImplCopyWith( + _$PurchaseOrderListQueryImpl value, + $Res Function(_$PurchaseOrderListQueryImpl) then, + ) = __$$PurchaseOrderListQueryImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int page, + int limit, + String? search, + String? status, + String? poType, + int? vendorId, + int? plantId, + String? dateFrom, + String? dateTo, + }); +} + +/// @nodoc +class __$$PurchaseOrderListQueryImplCopyWithImpl<$Res> + extends + _$PurchaseOrderListQueryCopyWithImpl<$Res, _$PurchaseOrderListQueryImpl> + implements _$$PurchaseOrderListQueryImplCopyWith<$Res> { + __$$PurchaseOrderListQueryImplCopyWithImpl( + _$PurchaseOrderListQueryImpl _value, + $Res Function(_$PurchaseOrderListQueryImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PurchaseOrderListQuery + /// 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? poType = freezed, + Object? vendorId = freezed, + Object? plantId = freezed, + Object? dateFrom = freezed, + Object? dateTo = freezed, + }) { + return _then( + _$PurchaseOrderListQueryImpl( + 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?, + poType: freezed == poType + ? _value.poType + : poType // ignore: cast_nullable_to_non_nullable + as String?, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as int?, + plantId: freezed == plantId + ? _value.plantId + : plantId // ignore: cast_nullable_to_non_nullable + as int?, + dateFrom: freezed == dateFrom + ? _value.dateFrom + : dateFrom // ignore: cast_nullable_to_non_nullable + as String?, + dateTo: freezed == dateTo + ? _value.dateTo + : dateTo // ignore: cast_nullable_to_non_nullable + as String?, + ), + ); + } +} + +/// @nodoc + +class _$PurchaseOrderListQueryImpl implements _PurchaseOrderListQuery { + const _$PurchaseOrderListQueryImpl({ + this.page = 1, + this.limit = 20, + this.search, + this.status, + this.poType, + this.vendorId, + this.plantId, + this.dateFrom, + this.dateTo, + }); + + @override + @JsonKey() + final int page; + @override + @JsonKey() + final int limit; + @override + final String? search; + @override + final String? status; + @override + final String? poType; + @override + final int? vendorId; + @override + final int? plantId; + @override + final String? dateFrom; + @override + final String? dateTo; + + @override + String toString() { + return 'PurchaseOrderListQuery(page: $page, limit: $limit, search: $search, status: $status, poType: $poType, vendorId: $vendorId, plantId: $plantId, dateFrom: $dateFrom, dateTo: $dateTo)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PurchaseOrderListQueryImpl && + (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.poType, poType) || other.poType == poType) && + (identical(other.vendorId, vendorId) || + other.vendorId == vendorId) && + (identical(other.plantId, plantId) || other.plantId == plantId) && + (identical(other.dateFrom, dateFrom) || + other.dateFrom == dateFrom) && + (identical(other.dateTo, dateTo) || other.dateTo == dateTo)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + page, + limit, + search, + status, + poType, + vendorId, + plantId, + dateFrom, + dateTo, + ); + + /// Create a copy of PurchaseOrderListQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PurchaseOrderListQueryImplCopyWith<_$PurchaseOrderListQueryImpl> + get copyWith => + __$$PurchaseOrderListQueryImplCopyWithImpl<_$PurchaseOrderListQueryImpl>( + this, + _$identity, + ); +} + +abstract class _PurchaseOrderListQuery implements PurchaseOrderListQuery { + const factory _PurchaseOrderListQuery({ + final int page, + final int limit, + final String? search, + final String? status, + final String? poType, + final int? vendorId, + final int? plantId, + final String? dateFrom, + final String? dateTo, + }) = _$PurchaseOrderListQueryImpl; + + @override + int get page; + @override + int get limit; + @override + String? get search; + @override + String? get status; + @override + String? get poType; + @override + int? get vendorId; + @override + int? get plantId; + @override + String? get dateFrom; + @override + String? get dateTo; + + /// Create a copy of PurchaseOrderListQuery + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PurchaseOrderListQueryImplCopyWith<_$PurchaseOrderListQueryImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/purchase_order_model.g.dart b/lib/shared/models/purchase_order_model.g.dart new file mode 100644 index 0000000..8e6a63f --- /dev/null +++ b/lib/shared/models/purchase_order_model.g.dart @@ -0,0 +1,121 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'purchase_order_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$PurchaseOrderModelImpl _$$PurchaseOrderModelImplFromJson( + Map json, +) => _$PurchaseOrderModelImpl( + id: _idFromJson(json['id']), + poNo: _readPoNumber(json, 'po_number') as String?, + poDate: _dateFromJsonNullable(json['po_date']), + poType: json['po_type'] as String?, + status: json['status'] as String? ?? 'DRAFT', + vendorId: _intFromJsonNullable(json['vendor_id']), + vendorName: _readVendorName(json, 'vendor_name') as String?, + plantId: _intFromJsonNullable(json['plant_id']), + plantName: _readPlantName(json, 'plant_name') as String?, + warehouseId: _intFromJsonNullable(json['warehouse_id']), + warehouseName: _readWarehouseName(json, 'warehouse_name') as String?, + brandId: _intFromJsonNullable(json['brand_id']), + paymentTermId: _intFromJsonNullable(json['payment_term_id']), + deliveryTermId: _intFromJsonNullable(json['delivery_term_id']), + expectedDeliveryDate: _dateFromJsonNullable(json['expected_delivery_date']), + discountAmount: _doubleFromJsonNullable(json['discount_amount']), + freightCharges: _doubleFromJsonNullable(json['freight_charges']), + otherCharges: _doubleFromJsonNullable(json['other_charges']), + taxableAmount: (_readSubTotal(json, 'sub_total') as num?)?.toDouble(), + taxAmount: (_readTaxTotal(json, 'tax_total') as num?)?.toDouble(), + totalAmount: (_readGrandTotal(json, 'grand_total') as num?)?.toDouble(), + termsAndConditions: json['terms_and_conditions'] as String?, + remarks: json['remarks'] as String?, + revisionNo: _intFromJsonNullable(json['revision_no']), + createdAt: _dateFromJsonNullable(json['created_at']), + updatedAt: _dateFromJsonNullable(json['updated_at']), + items: + (json['items'] as List?) + ?.map( + (e) => PurchaseOrderItemModel.fromJson(e as Map), + ) + .toList() ?? + const [], +); + +Map _$$PurchaseOrderModelImplToJson( + _$PurchaseOrderModelImpl instance, +) => { + 'id': instance.id, + 'po_number': instance.poNo, + 'po_date': instance.poDate?.toIso8601String(), + 'po_type': instance.poType, + 'status': instance.status, + 'vendor_id': instance.vendorId, + 'vendor_name': instance.vendorName, + 'plant_id': instance.plantId, + 'plant_name': instance.plantName, + 'warehouse_id': instance.warehouseId, + 'warehouse_name': instance.warehouseName, + 'brand_id': instance.brandId, + 'payment_term_id': instance.paymentTermId, + 'delivery_term_id': instance.deliveryTermId, + 'expected_delivery_date': instance.expectedDeliveryDate?.toIso8601String(), + 'discount_amount': instance.discountAmount, + 'freight_charges': instance.freightCharges, + 'other_charges': instance.otherCharges, + 'sub_total': instance.taxableAmount, + 'tax_total': instance.taxAmount, + 'grand_total': instance.totalAmount, + 'terms_and_conditions': instance.termsAndConditions, + 'remarks': instance.remarks, + 'revision_no': instance.revisionNo, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + 'items': instance.items, +}; + +_$PurchaseOrderItemModelImpl _$$PurchaseOrderItemModelImplFromJson( + Map json, +) => _$PurchaseOrderItemModelImpl( + id: _idFromJson(json['id']), + poId: _idFromJson(json['po_id']), + itemId: _intFromJsonNullable(json['item_id']), + itemCode: _readItemCode(json, 'item_code') as String?, + itemName: _readItemName(json, 'item_name') as String?, + lineNo: _intFromJsonNullable(json['line_no']), + orderedQty: _doubleFromJsonNullable(json['ordered_qty']), + receivedQty: _doubleFromJsonNullable(json['received_qty']), + uomId: _intFromJsonNullable(json['uom_id']), + uomName: _readUomName(json, 'uom_name') as String?, + rate: _doubleFromJsonNullable(json['rate']), + discountPct: _doubleFromJsonNullable(json['discount_pct']), + discountAmount: _doubleFromJsonNullable(json['discount_amount']), + gstRateId: _intFromJsonNullable(json['gst_rate_id']), + hsnCodeId: _intFromJsonNullable(json['hsn_code_id']), + lineAmount: _doubleFromJsonNullable(json['line_amount']), + remarks: json['remarks'] as String?, +); + +Map _$$PurchaseOrderItemModelImplToJson( + _$PurchaseOrderItemModelImpl instance, +) => { + 'id': instance.id, + 'po_id': instance.poId, + 'item_id': instance.itemId, + 'item_code': instance.itemCode, + 'item_name': instance.itemName, + 'line_no': instance.lineNo, + 'ordered_qty': instance.orderedQty, + 'received_qty': instance.receivedQty, + 'uom_id': instance.uomId, + 'uom_name': instance.uomName, + 'rate': instance.rate, + 'discount_pct': instance.discountPct, + 'discount_amount': instance.discountAmount, + 'gst_rate_id': instance.gstRateId, + 'hsn_code_id': instance.hsnCodeId, + 'line_amount': instance.lineAmount, + 'remarks': instance.remarks, +}; diff --git a/lib/shared/models/vendor_model.dart b/lib/shared/models/vendor_model.dart new file mode 100644 index 0000000..7146aad --- /dev/null +++ b/lib/shared/models/vendor_model.dart @@ -0,0 +1,178 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'vendor_model.freezed.dart'; +part 'vendor_model.g.dart'; + +String _idFromJson(Object? value) => value?.toString() ?? ''; + +int? _intFromJsonNullable(Object? value) { + if (value == null) return null; + if (value is int) return value; + if (value is num) return value.toInt(); + return int.tryParse(value.toString()); +} + +DateTime? _dateFromJsonNullable(Object? value) { + if (value == null) return null; + if (value is DateTime) return value; + return DateTime.tryParse(value.toString()); +} + +Object? _readPaymentTermName(Map json, String key) { + final flat = json['payment_term_name']; + if (flat is String && flat.isNotEmpty) return flat; + final nested = json['payment_term']; + if (nested is Map) return nested['name']; + return null; +} + +Object? _readPaymentTermId(Map json, String key) { + final flat = json['payment_term_id']; + if (flat != null) return flat; + final nested = json['payment_term']; + if (nested is Map) return nested['id']; + return null; +} + +@freezed +class VendorModel with _$VendorModel { + const factory VendorModel({ + @JsonKey(fromJson: _idFromJson) required String id, + @JsonKey(name: 'vendor_code') String? vendorCode, + @JsonKey(name: 'vendor_name') required String vendorName, + @JsonKey(name: 'vendor_type') String? vendorType, + String? gstin, + String? pan, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? paymentTermId, + @JsonKey(name: 'payment_term_name', readValue: _readPaymentTermName) + String? paymentTermName, + @JsonKey(name: 'credit_period_days', fromJson: _intFromJsonNullable) + int? creditPeriodDays, + String? remarks, + String? status, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? updatedAt, + @Default([]) List addresses, + @Default([]) List contacts, + @JsonKey(name: 'bank_details') @Default([]) List bankDetails, + }) = _VendorModel; + + factory VendorModel.fromJson(Map json) => + _$VendorModelFromJson(json); +} + +@freezed +class VendorAddressModel with _$VendorAddressModel { + const factory VendorAddressModel({ + @JsonKey(fromJson: _idFromJson) required String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'address_type') String? addressType, + @JsonKey(name: 'address_line1') String? addressLine1, + @JsonKey(name: 'address_line2') String? addressLine2, + String? city, + String? state, + String? pincode, + String? country, + String? gstin, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + }) = _VendorAddressModel; + + factory VendorAddressModel.fromJson(Map json) => + _$VendorAddressModelFromJson(json); +} + +@freezed +class VendorContactModel with _$VendorContactModel { + const factory VendorContactModel({ + @JsonKey(fromJson: _idFromJson) required String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'contact_name') required String contactName, + String? designation, + String? phone, + String? email, + @JsonKey(name: 'is_primary') @Default(false) bool isPrimary, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + }) = _VendorContactModel; + + factory VendorContactModel.fromJson(Map json) => + _$VendorContactModelFromJson(json); +} + +@freezed +class VendorBankDetailModel with _$VendorBankDetailModel { + const factory VendorBankDetailModel({ + @JsonKey(fromJson: _idFromJson) required String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'bank_name') String? bankName, + String? branch, + @JsonKey(name: 'account_number') String? accountNumber, + String? ifsc, + @JsonKey(name: 'account_holder_name') String? accountHolderName, + @JsonKey(name: 'account_type') String? accountType, + @JsonKey(name: 'is_primary') @Default(false) bool isPrimary, + @JsonKey(name: 'is_active') @Default(true) bool isActive, + }) = _VendorBankDetailModel; + + factory VendorBankDetailModel.fromJson(Map json) => + _$VendorBankDetailModelFromJson(json); +} + +@freezed +class VendorListQuery with _$VendorListQuery { + const factory VendorListQuery({ + @Default(1) int page, + @Default(20) int limit, + String? search, + String? status, + String? vendorType, + bool? isActive, + }) = _VendorListQuery; +} + +const vendorTypeOptions = [ + ('RAW_MATERIAL', 'Raw Material'), + ('PACKING_MATERIAL', 'Packing Material'), + ('ASSET_CAPITAL', 'Asset / Capital'), + ('SERVICE', 'Service'), + ('GENERAL', 'General'), +]; + +const vendorStatusOptions = [ + ('active', 'Active'), + ('inactive', 'Inactive'), + ('blacklisted', 'Blacklisted'), +]; + +const addressTypeOptions = [ + ('REGISTERED', 'Registered'), + ('BILLING', 'Billing'), + ('DISPATCH', 'Dispatch'), +]; + +const accountTypeOptions = [ + ('CURRENT', 'Current'), + ('SAVINGS', 'Savings'), + ('OVERDRAFT', 'Overdraft'), +]; + +String vendorTypeLabel(String? value) { + if (value == null) return '—'; + return vendorTypeOptions + .where((e) => e.$1 == value) + .map((e) => e.$2) + .firstOrNull ?? + value; +} + +String vendorStatusLabel(String? value) { + if (value == null) return '—'; + return vendorStatusOptions + .where((e) => e.$1 == value) + .map((e) => e.$2) + .firstOrNull ?? + value; +} diff --git a/lib/shared/models/vendor_model.freezed.dart b/lib/shared/models/vendor_model.freezed.dart new file mode 100644 index 0000000..2425585 --- /dev/null +++ b/lib/shared/models/vendor_model.freezed.dart @@ -0,0 +1,2003 @@ +// 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 'vendor_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', +); + +VendorModel _$VendorModelFromJson(Map json) { + return _VendorModel.fromJson(json); +} + +/// @nodoc +mixin _$VendorModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_code') + String? get vendorCode => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_name') + String get vendorName => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_type') + String? get vendorType => throw _privateConstructorUsedError; + String? get gstin => throw _privateConstructorUsedError; + String? get pan => throw _privateConstructorUsedError; + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? get paymentTermId => throw _privateConstructorUsedError; + @JsonKey(name: 'payment_term_name', readValue: _readPaymentTermName) + String? get paymentTermName => throw _privateConstructorUsedError; + @JsonKey(name: 'credit_period_days', fromJson: _intFromJsonNullable) + int? get creditPeriodDays => throw _privateConstructorUsedError; + String? get remarks => throw _privateConstructorUsedError; + String? get status => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? get updatedAt => throw _privateConstructorUsedError; + List get addresses => throw _privateConstructorUsedError; + List get contacts => throw _privateConstructorUsedError; + @JsonKey(name: 'bank_details') + List get bankDetails => + throw _privateConstructorUsedError; + + /// Serializes this VendorModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of VendorModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $VendorModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VendorModelCopyWith<$Res> { + factory $VendorModelCopyWith( + VendorModel value, + $Res Function(VendorModel) then, + ) = _$VendorModelCopyWithImpl<$Res, VendorModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'vendor_code') String? vendorCode, + @JsonKey(name: 'vendor_name') String vendorName, + @JsonKey(name: 'vendor_type') String? vendorType, + String? gstin, + String? pan, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? paymentTermId, + @JsonKey(name: 'payment_term_name', readValue: _readPaymentTermName) + String? paymentTermName, + @JsonKey(name: 'credit_period_days', fromJson: _intFromJsonNullable) + int? creditPeriodDays, + String? remarks, + String? status, + @JsonKey(name: 'is_active') bool isActive, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? updatedAt, + List addresses, + List contacts, + @JsonKey(name: 'bank_details') List bankDetails, + }); +} + +/// @nodoc +class _$VendorModelCopyWithImpl<$Res, $Val extends VendorModel> + implements $VendorModelCopyWith<$Res> { + _$VendorModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of VendorModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? vendorCode = freezed, + Object? vendorName = null, + Object? vendorType = freezed, + Object? gstin = freezed, + Object? pan = freezed, + Object? paymentTermId = freezed, + Object? paymentTermName = freezed, + Object? creditPeriodDays = freezed, + Object? remarks = freezed, + Object? status = freezed, + Object? isActive = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? addresses = null, + Object? contacts = null, + Object? bankDetails = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + vendorCode: freezed == vendorCode + ? _value.vendorCode + : vendorCode // ignore: cast_nullable_to_non_nullable + as String?, + vendorName: null == vendorName + ? _value.vendorName + : vendorName // ignore: cast_nullable_to_non_nullable + as String, + vendorType: freezed == vendorType + ? _value.vendorType + : vendorType // ignore: cast_nullable_to_non_nullable + as String?, + gstin: freezed == gstin + ? _value.gstin + : gstin // ignore: cast_nullable_to_non_nullable + as String?, + pan: freezed == pan + ? _value.pan + : pan // ignore: cast_nullable_to_non_nullable + as String?, + paymentTermId: freezed == paymentTermId + ? _value.paymentTermId + : paymentTermId // ignore: cast_nullable_to_non_nullable + as int?, + paymentTermName: freezed == paymentTermName + ? _value.paymentTermName + : paymentTermName // ignore: cast_nullable_to_non_nullable + as String?, + creditPeriodDays: freezed == creditPeriodDays + ? _value.creditPeriodDays + : creditPeriodDays // ignore: cast_nullable_to_non_nullable + as int?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == 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, + 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?, + addresses: null == addresses + ? _value.addresses + : addresses // ignore: cast_nullable_to_non_nullable + as List, + contacts: null == contacts + ? _value.contacts + : contacts // ignore: cast_nullable_to_non_nullable + as List, + bankDetails: null == bankDetails + ? _value.bankDetails + : bankDetails // ignore: cast_nullable_to_non_nullable + as List, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$VendorModelImplCopyWith<$Res> + implements $VendorModelCopyWith<$Res> { + factory _$$VendorModelImplCopyWith( + _$VendorModelImpl value, + $Res Function(_$VendorModelImpl) then, + ) = __$$VendorModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'vendor_code') String? vendorCode, + @JsonKey(name: 'vendor_name') String vendorName, + @JsonKey(name: 'vendor_type') String? vendorType, + String? gstin, + String? pan, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? paymentTermId, + @JsonKey(name: 'payment_term_name', readValue: _readPaymentTermName) + String? paymentTermName, + @JsonKey(name: 'credit_period_days', fromJson: _intFromJsonNullable) + int? creditPeriodDays, + String? remarks, + String? status, + @JsonKey(name: 'is_active') bool isActive, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? updatedAt, + List addresses, + List contacts, + @JsonKey(name: 'bank_details') List bankDetails, + }); +} + +/// @nodoc +class __$$VendorModelImplCopyWithImpl<$Res> + extends _$VendorModelCopyWithImpl<$Res, _$VendorModelImpl> + implements _$$VendorModelImplCopyWith<$Res> { + __$$VendorModelImplCopyWithImpl( + _$VendorModelImpl _value, + $Res Function(_$VendorModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of VendorModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? vendorCode = freezed, + Object? vendorName = null, + Object? vendorType = freezed, + Object? gstin = freezed, + Object? pan = freezed, + Object? paymentTermId = freezed, + Object? paymentTermName = freezed, + Object? creditPeriodDays = freezed, + Object? remarks = freezed, + Object? status = freezed, + Object? isActive = null, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? addresses = null, + Object? contacts = null, + Object? bankDetails = null, + }) { + return _then( + _$VendorModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + vendorCode: freezed == vendorCode + ? _value.vendorCode + : vendorCode // ignore: cast_nullable_to_non_nullable + as String?, + vendorName: null == vendorName + ? _value.vendorName + : vendorName // ignore: cast_nullable_to_non_nullable + as String, + vendorType: freezed == vendorType + ? _value.vendorType + : vendorType // ignore: cast_nullable_to_non_nullable + as String?, + gstin: freezed == gstin + ? _value.gstin + : gstin // ignore: cast_nullable_to_non_nullable + as String?, + pan: freezed == pan + ? _value.pan + : pan // ignore: cast_nullable_to_non_nullable + as String?, + paymentTermId: freezed == paymentTermId + ? _value.paymentTermId + : paymentTermId // ignore: cast_nullable_to_non_nullable + as int?, + paymentTermName: freezed == paymentTermName + ? _value.paymentTermName + : paymentTermName // ignore: cast_nullable_to_non_nullable + as String?, + creditPeriodDays: freezed == creditPeriodDays + ? _value.creditPeriodDays + : creditPeriodDays // ignore: cast_nullable_to_non_nullable + as int?, + remarks: freezed == remarks + ? _value.remarks + : remarks // ignore: cast_nullable_to_non_nullable + as String?, + status: freezed == 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, + 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?, + addresses: null == addresses + ? _value._addresses + : addresses // ignore: cast_nullable_to_non_nullable + as List, + contacts: null == contacts + ? _value._contacts + : contacts // ignore: cast_nullable_to_non_nullable + as List, + bankDetails: null == bankDetails + ? _value._bankDetails + : bankDetails // ignore: cast_nullable_to_non_nullable + as List, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$VendorModelImpl implements _VendorModel { + const _$VendorModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + @JsonKey(name: 'vendor_code') this.vendorCode, + @JsonKey(name: 'vendor_name') required this.vendorName, + @JsonKey(name: 'vendor_type') this.vendorType, + this.gstin, + this.pan, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + this.paymentTermId, + @JsonKey(name: 'payment_term_name', readValue: _readPaymentTermName) + this.paymentTermName, + @JsonKey(name: 'credit_period_days', fromJson: _intFromJsonNullable) + this.creditPeriodDays, + this.remarks, + this.status, + @JsonKey(name: 'is_active') this.isActive = true, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + this.createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + this.updatedAt, + final List addresses = const [], + final List contacts = const [], + @JsonKey(name: 'bank_details') + final List bankDetails = const [], + }) : _addresses = addresses, + _contacts = contacts, + _bankDetails = bankDetails; + + factory _$VendorModelImpl.fromJson(Map json) => + _$$VendorModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + @JsonKey(name: 'vendor_code') + final String? vendorCode; + @override + @JsonKey(name: 'vendor_name') + final String vendorName; + @override + @JsonKey(name: 'vendor_type') + final String? vendorType; + @override + final String? gstin; + @override + final String? pan; + @override + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + final int? paymentTermId; + @override + @JsonKey(name: 'payment_term_name', readValue: _readPaymentTermName) + final String? paymentTermName; + @override + @JsonKey(name: 'credit_period_days', fromJson: _intFromJsonNullable) + final int? creditPeriodDays; + @override + final String? remarks; + @override + final String? status; + @override + @JsonKey(name: 'is_active') + final bool isActive; + @override + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + final DateTime? createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + final DateTime? updatedAt; + final List _addresses; + @override + @JsonKey() + List get addresses { + if (_addresses is EqualUnmodifiableListView) return _addresses; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_addresses); + } + + final List _contacts; + @override + @JsonKey() + List get contacts { + if (_contacts is EqualUnmodifiableListView) return _contacts; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_contacts); + } + + final List _bankDetails; + @override + @JsonKey(name: 'bank_details') + List get bankDetails { + if (_bankDetails is EqualUnmodifiableListView) return _bankDetails; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_bankDetails); + } + + @override + String toString() { + return 'VendorModel(id: $id, vendorCode: $vendorCode, vendorName: $vendorName, vendorType: $vendorType, gstin: $gstin, pan: $pan, paymentTermId: $paymentTermId, paymentTermName: $paymentTermName, creditPeriodDays: $creditPeriodDays, remarks: $remarks, status: $status, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, addresses: $addresses, contacts: $contacts, bankDetails: $bankDetails)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VendorModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.vendorCode, vendorCode) || + other.vendorCode == vendorCode) && + (identical(other.vendorName, vendorName) || + other.vendorName == vendorName) && + (identical(other.vendorType, vendorType) || + other.vendorType == vendorType) && + (identical(other.gstin, gstin) || other.gstin == gstin) && + (identical(other.pan, pan) || other.pan == pan) && + (identical(other.paymentTermId, paymentTermId) || + other.paymentTermId == paymentTermId) && + (identical(other.paymentTermName, paymentTermName) || + other.paymentTermName == paymentTermName) && + (identical(other.creditPeriodDays, creditPeriodDays) || + other.creditPeriodDays == creditPeriodDays) && + (identical(other.remarks, remarks) || other.remarks == remarks) && + (identical(other.status, status) || other.status == status) && + (identical(other.isActive, isActive) || + other.isActive == isActive) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + const DeepCollectionEquality().equals( + other._addresses, + _addresses, + ) && + const DeepCollectionEquality().equals(other._contacts, _contacts) && + const DeepCollectionEquality().equals( + other._bankDetails, + _bankDetails, + )); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + vendorCode, + vendorName, + vendorType, + gstin, + pan, + paymentTermId, + paymentTermName, + creditPeriodDays, + remarks, + status, + isActive, + createdAt, + updatedAt, + const DeepCollectionEquality().hash(_addresses), + const DeepCollectionEquality().hash(_contacts), + const DeepCollectionEquality().hash(_bankDetails), + ); + + /// Create a copy of VendorModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$VendorModelImplCopyWith<_$VendorModelImpl> get copyWith => + __$$VendorModelImplCopyWithImpl<_$VendorModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$VendorModelImplToJson(this); + } +} + +abstract class _VendorModel implements VendorModel { + const factory _VendorModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + @JsonKey(name: 'vendor_code') final String? vendorCode, + @JsonKey(name: 'vendor_name') required final String vendorName, + @JsonKey(name: 'vendor_type') final String? vendorType, + final String? gstin, + final String? pan, + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + final int? paymentTermId, + @JsonKey(name: 'payment_term_name', readValue: _readPaymentTermName) + final String? paymentTermName, + @JsonKey(name: 'credit_period_days', fromJson: _intFromJsonNullable) + final int? creditPeriodDays, + final String? remarks, + final String? status, + @JsonKey(name: 'is_active') final bool isActive, + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + final DateTime? createdAt, + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + final DateTime? updatedAt, + final List addresses, + final List contacts, + @JsonKey(name: 'bank_details') + final List bankDetails, + }) = _$VendorModelImpl; + + factory _VendorModel.fromJson(Map json) = + _$VendorModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + @JsonKey(name: 'vendor_code') + String? get vendorCode; + @override + @JsonKey(name: 'vendor_name') + String get vendorName; + @override + @JsonKey(name: 'vendor_type') + String? get vendorType; + @override + String? get gstin; + @override + String? get pan; + @override + @JsonKey(name: 'payment_term_id', fromJson: _intFromJsonNullable) + int? get paymentTermId; + @override + @JsonKey(name: 'payment_term_name', readValue: _readPaymentTermName) + String? get paymentTermName; + @override + @JsonKey(name: 'credit_period_days', fromJson: _intFromJsonNullable) + int? get creditPeriodDays; + @override + String? get remarks; + @override + String? get status; + @override + @JsonKey(name: 'is_active') + bool get isActive; + @override + @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) + DateTime? get createdAt; + @override + @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) + DateTime? get updatedAt; + @override + List get addresses; + @override + List get contacts; + @override + @JsonKey(name: 'bank_details') + List get bankDetails; + + /// Create a copy of VendorModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$VendorModelImplCopyWith<_$VendorModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +VendorAddressModel _$VendorAddressModelFromJson(Map json) { + return _VendorAddressModel.fromJson(json); +} + +/// @nodoc +mixin _$VendorAddressModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + String? get vendorId => throw _privateConstructorUsedError; + @JsonKey(name: 'address_type') + String? get addressType => throw _privateConstructorUsedError; + @JsonKey(name: 'address_line1') + String? get addressLine1 => throw _privateConstructorUsedError; + @JsonKey(name: 'address_line2') + String? get addressLine2 => throw _privateConstructorUsedError; + String? get city => throw _privateConstructorUsedError; + String? get state => throw _privateConstructorUsedError; + String? get pincode => throw _privateConstructorUsedError; + String? get country => throw _privateConstructorUsedError; + String? get gstin => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + + /// Serializes this VendorAddressModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of VendorAddressModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $VendorAddressModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VendorAddressModelCopyWith<$Res> { + factory $VendorAddressModelCopyWith( + VendorAddressModel value, + $Res Function(VendorAddressModel) then, + ) = _$VendorAddressModelCopyWithImpl<$Res, VendorAddressModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'address_type') String? addressType, + @JsonKey(name: 'address_line1') String? addressLine1, + @JsonKey(name: 'address_line2') String? addressLine2, + String? city, + String? state, + String? pincode, + String? country, + String? gstin, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class _$VendorAddressModelCopyWithImpl<$Res, $Val extends VendorAddressModel> + implements $VendorAddressModelCopyWith<$Res> { + _$VendorAddressModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of VendorAddressModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? vendorId = freezed, + Object? addressType = freezed, + Object? addressLine1 = freezed, + Object? addressLine2 = freezed, + Object? city = freezed, + Object? state = freezed, + Object? pincode = freezed, + Object? country = freezed, + Object? gstin = freezed, + Object? isActive = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as String?, + addressType: freezed == addressType + ? _value.addressType + : addressType // ignore: cast_nullable_to_non_nullable + as String?, + addressLine1: freezed == addressLine1 + ? _value.addressLine1 + : addressLine1 // ignore: cast_nullable_to_non_nullable + as String?, + addressLine2: freezed == addressLine2 + ? _value.addressLine2 + : addressLine2 // ignore: cast_nullable_to_non_nullable + as String?, + city: freezed == city + ? _value.city + : city // ignore: cast_nullable_to_non_nullable + as String?, + state: freezed == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as String?, + pincode: freezed == pincode + ? _value.pincode + : pincode // ignore: cast_nullable_to_non_nullable + as String?, + country: freezed == country + ? _value.country + : country // ignore: cast_nullable_to_non_nullable + as String?, + gstin: freezed == gstin + ? _value.gstin + : gstin // 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 _$$VendorAddressModelImplCopyWith<$Res> + implements $VendorAddressModelCopyWith<$Res> { + factory _$$VendorAddressModelImplCopyWith( + _$VendorAddressModelImpl value, + $Res Function(_$VendorAddressModelImpl) then, + ) = __$$VendorAddressModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'address_type') String? addressType, + @JsonKey(name: 'address_line1') String? addressLine1, + @JsonKey(name: 'address_line2') String? addressLine2, + String? city, + String? state, + String? pincode, + String? country, + String? gstin, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class __$$VendorAddressModelImplCopyWithImpl<$Res> + extends _$VendorAddressModelCopyWithImpl<$Res, _$VendorAddressModelImpl> + implements _$$VendorAddressModelImplCopyWith<$Res> { + __$$VendorAddressModelImplCopyWithImpl( + _$VendorAddressModelImpl _value, + $Res Function(_$VendorAddressModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of VendorAddressModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? vendorId = freezed, + Object? addressType = freezed, + Object? addressLine1 = freezed, + Object? addressLine2 = freezed, + Object? city = freezed, + Object? state = freezed, + Object? pincode = freezed, + Object? country = freezed, + Object? gstin = freezed, + Object? isActive = null, + }) { + return _then( + _$VendorAddressModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as String?, + addressType: freezed == addressType + ? _value.addressType + : addressType // ignore: cast_nullable_to_non_nullable + as String?, + addressLine1: freezed == addressLine1 + ? _value.addressLine1 + : addressLine1 // ignore: cast_nullable_to_non_nullable + as String?, + addressLine2: freezed == addressLine2 + ? _value.addressLine2 + : addressLine2 // ignore: cast_nullable_to_non_nullable + as String?, + city: freezed == city + ? _value.city + : city // ignore: cast_nullable_to_non_nullable + as String?, + state: freezed == state + ? _value.state + : state // ignore: cast_nullable_to_non_nullable + as String?, + pincode: freezed == pincode + ? _value.pincode + : pincode // ignore: cast_nullable_to_non_nullable + as String?, + country: freezed == country + ? _value.country + : country // ignore: cast_nullable_to_non_nullable + as String?, + gstin: freezed == gstin + ? _value.gstin + : gstin // 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 _$VendorAddressModelImpl implements _VendorAddressModel { + const _$VendorAddressModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) this.vendorId, + @JsonKey(name: 'address_type') this.addressType, + @JsonKey(name: 'address_line1') this.addressLine1, + @JsonKey(name: 'address_line2') this.addressLine2, + this.city, + this.state, + this.pincode, + this.country, + this.gstin, + @JsonKey(name: 'is_active') this.isActive = true, + }); + + factory _$VendorAddressModelImpl.fromJson(Map json) => + _$$VendorAddressModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + final String? vendorId; + @override + @JsonKey(name: 'address_type') + final String? addressType; + @override + @JsonKey(name: 'address_line1') + final String? addressLine1; + @override + @JsonKey(name: 'address_line2') + final String? addressLine2; + @override + final String? city; + @override + final String? state; + @override + final String? pincode; + @override + final String? country; + @override + final String? gstin; + @override + @JsonKey(name: 'is_active') + final bool isActive; + + @override + String toString() { + return 'VendorAddressModel(id: $id, vendorId: $vendorId, addressType: $addressType, addressLine1: $addressLine1, addressLine2: $addressLine2, city: $city, state: $state, pincode: $pincode, country: $country, gstin: $gstin, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VendorAddressModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.vendorId, vendorId) || + other.vendorId == vendorId) && + (identical(other.addressType, addressType) || + other.addressType == addressType) && + (identical(other.addressLine1, addressLine1) || + other.addressLine1 == addressLine1) && + (identical(other.addressLine2, addressLine2) || + other.addressLine2 == addressLine2) && + (identical(other.city, city) || other.city == city) && + (identical(other.state, state) || other.state == state) && + (identical(other.pincode, pincode) || other.pincode == pincode) && + (identical(other.country, country) || other.country == country) && + (identical(other.gstin, gstin) || other.gstin == gstin) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + vendorId, + addressType, + addressLine1, + addressLine2, + city, + state, + pincode, + country, + gstin, + isActive, + ); + + /// Create a copy of VendorAddressModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$VendorAddressModelImplCopyWith<_$VendorAddressModelImpl> get copyWith => + __$$VendorAddressModelImplCopyWithImpl<_$VendorAddressModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$VendorAddressModelImplToJson(this); + } +} + +abstract class _VendorAddressModel implements VendorAddressModel { + const factory _VendorAddressModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) final String? vendorId, + @JsonKey(name: 'address_type') final String? addressType, + @JsonKey(name: 'address_line1') final String? addressLine1, + @JsonKey(name: 'address_line2') final String? addressLine2, + final String? city, + final String? state, + final String? pincode, + final String? country, + final String? gstin, + @JsonKey(name: 'is_active') final bool isActive, + }) = _$VendorAddressModelImpl; + + factory _VendorAddressModel.fromJson(Map json) = + _$VendorAddressModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + String? get vendorId; + @override + @JsonKey(name: 'address_type') + String? get addressType; + @override + @JsonKey(name: 'address_line1') + String? get addressLine1; + @override + @JsonKey(name: 'address_line2') + String? get addressLine2; + @override + String? get city; + @override + String? get state; + @override + String? get pincode; + @override + String? get country; + @override + String? get gstin; + @override + @JsonKey(name: 'is_active') + bool get isActive; + + /// Create a copy of VendorAddressModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$VendorAddressModelImplCopyWith<_$VendorAddressModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +VendorContactModel _$VendorContactModelFromJson(Map json) { + return _VendorContactModel.fromJson(json); +} + +/// @nodoc +mixin _$VendorContactModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + String? get vendorId => throw _privateConstructorUsedError; + @JsonKey(name: 'contact_name') + String get contactName => throw _privateConstructorUsedError; + String? get designation => throw _privateConstructorUsedError; + String? get phone => throw _privateConstructorUsedError; + String? get email => throw _privateConstructorUsedError; + @JsonKey(name: 'is_primary') + bool get isPrimary => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + + /// Serializes this VendorContactModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of VendorContactModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $VendorContactModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VendorContactModelCopyWith<$Res> { + factory $VendorContactModelCopyWith( + VendorContactModel value, + $Res Function(VendorContactModel) then, + ) = _$VendorContactModelCopyWithImpl<$Res, VendorContactModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'contact_name') String contactName, + String? designation, + String? phone, + String? email, + @JsonKey(name: 'is_primary') bool isPrimary, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class _$VendorContactModelCopyWithImpl<$Res, $Val extends VendorContactModel> + implements $VendorContactModelCopyWith<$Res> { + _$VendorContactModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of VendorContactModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? vendorId = freezed, + Object? contactName = null, + Object? designation = freezed, + Object? phone = freezed, + Object? email = freezed, + Object? isPrimary = null, + Object? isActive = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as String?, + contactName: null == contactName + ? _value.contactName + : contactName // ignore: cast_nullable_to_non_nullable + as String, + designation: freezed == designation + ? _value.designation + : designation // ignore: cast_nullable_to_non_nullable + as String?, + phone: freezed == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + isPrimary: null == isPrimary + ? _value.isPrimary + : isPrimary // ignore: cast_nullable_to_non_nullable + as bool, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$VendorContactModelImplCopyWith<$Res> + implements $VendorContactModelCopyWith<$Res> { + factory _$$VendorContactModelImplCopyWith( + _$VendorContactModelImpl value, + $Res Function(_$VendorContactModelImpl) then, + ) = __$$VendorContactModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'contact_name') String contactName, + String? designation, + String? phone, + String? email, + @JsonKey(name: 'is_primary') bool isPrimary, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class __$$VendorContactModelImplCopyWithImpl<$Res> + extends _$VendorContactModelCopyWithImpl<$Res, _$VendorContactModelImpl> + implements _$$VendorContactModelImplCopyWith<$Res> { + __$$VendorContactModelImplCopyWithImpl( + _$VendorContactModelImpl _value, + $Res Function(_$VendorContactModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of VendorContactModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? vendorId = freezed, + Object? contactName = null, + Object? designation = freezed, + Object? phone = freezed, + Object? email = freezed, + Object? isPrimary = null, + Object? isActive = null, + }) { + return _then( + _$VendorContactModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as String?, + contactName: null == contactName + ? _value.contactName + : contactName // ignore: cast_nullable_to_non_nullable + as String, + designation: freezed == designation + ? _value.designation + : designation // ignore: cast_nullable_to_non_nullable + as String?, + phone: freezed == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String?, + email: freezed == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String?, + isPrimary: null == isPrimary + ? _value.isPrimary + : isPrimary // ignore: cast_nullable_to_non_nullable + as bool, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$VendorContactModelImpl implements _VendorContactModel { + const _$VendorContactModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) this.vendorId, + @JsonKey(name: 'contact_name') required this.contactName, + this.designation, + this.phone, + this.email, + @JsonKey(name: 'is_primary') this.isPrimary = false, + @JsonKey(name: 'is_active') this.isActive = true, + }); + + factory _$VendorContactModelImpl.fromJson(Map json) => + _$$VendorContactModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + final String? vendorId; + @override + @JsonKey(name: 'contact_name') + final String contactName; + @override + final String? designation; + @override + final String? phone; + @override + final String? email; + @override + @JsonKey(name: 'is_primary') + final bool isPrimary; + @override + @JsonKey(name: 'is_active') + final bool isActive; + + @override + String toString() { + return 'VendorContactModel(id: $id, vendorId: $vendorId, contactName: $contactName, designation: $designation, phone: $phone, email: $email, isPrimary: $isPrimary, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VendorContactModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.vendorId, vendorId) || + other.vendorId == vendorId) && + (identical(other.contactName, contactName) || + other.contactName == contactName) && + (identical(other.designation, designation) || + other.designation == designation) && + (identical(other.phone, phone) || other.phone == phone) && + (identical(other.email, email) || other.email == email) && + (identical(other.isPrimary, isPrimary) || + other.isPrimary == isPrimary) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + vendorId, + contactName, + designation, + phone, + email, + isPrimary, + isActive, + ); + + /// Create a copy of VendorContactModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$VendorContactModelImplCopyWith<_$VendorContactModelImpl> get copyWith => + __$$VendorContactModelImplCopyWithImpl<_$VendorContactModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$VendorContactModelImplToJson(this); + } +} + +abstract class _VendorContactModel implements VendorContactModel { + const factory _VendorContactModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) final String? vendorId, + @JsonKey(name: 'contact_name') required final String contactName, + final String? designation, + final String? phone, + final String? email, + @JsonKey(name: 'is_primary') final bool isPrimary, + @JsonKey(name: 'is_active') final bool isActive, + }) = _$VendorContactModelImpl; + + factory _VendorContactModel.fromJson(Map json) = + _$VendorContactModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + String? get vendorId; + @override + @JsonKey(name: 'contact_name') + String get contactName; + @override + String? get designation; + @override + String? get phone; + @override + String? get email; + @override + @JsonKey(name: 'is_primary') + bool get isPrimary; + @override + @JsonKey(name: 'is_active') + bool get isActive; + + /// Create a copy of VendorContactModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$VendorContactModelImplCopyWith<_$VendorContactModelImpl> get copyWith => + throw _privateConstructorUsedError; +} + +VendorBankDetailModel _$VendorBankDetailModelFromJson( + Map json, +) { + return _VendorBankDetailModel.fromJson(json); +} + +/// @nodoc +mixin _$VendorBankDetailModel { + @JsonKey(fromJson: _idFromJson) + String get id => throw _privateConstructorUsedError; + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + String? get vendorId => throw _privateConstructorUsedError; + @JsonKey(name: 'bank_name') + String? get bankName => throw _privateConstructorUsedError; + String? get branch => throw _privateConstructorUsedError; + @JsonKey(name: 'account_number') + String? get accountNumber => throw _privateConstructorUsedError; + String? get ifsc => throw _privateConstructorUsedError; + @JsonKey(name: 'account_holder_name') + String? get accountHolderName => throw _privateConstructorUsedError; + @JsonKey(name: 'account_type') + String? get accountType => throw _privateConstructorUsedError; + @JsonKey(name: 'is_primary') + bool get isPrimary => throw _privateConstructorUsedError; + @JsonKey(name: 'is_active') + bool get isActive => throw _privateConstructorUsedError; + + /// Serializes this VendorBankDetailModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of VendorBankDetailModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $VendorBankDetailModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VendorBankDetailModelCopyWith<$Res> { + factory $VendorBankDetailModelCopyWith( + VendorBankDetailModel value, + $Res Function(VendorBankDetailModel) then, + ) = _$VendorBankDetailModelCopyWithImpl<$Res, VendorBankDetailModel>; + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'bank_name') String? bankName, + String? branch, + @JsonKey(name: 'account_number') String? accountNumber, + String? ifsc, + @JsonKey(name: 'account_holder_name') String? accountHolderName, + @JsonKey(name: 'account_type') String? accountType, + @JsonKey(name: 'is_primary') bool isPrimary, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class _$VendorBankDetailModelCopyWithImpl< + $Res, + $Val extends VendorBankDetailModel +> + implements $VendorBankDetailModelCopyWith<$Res> { + _$VendorBankDetailModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of VendorBankDetailModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? vendorId = freezed, + Object? bankName = freezed, + Object? branch = freezed, + Object? accountNumber = freezed, + Object? ifsc = freezed, + Object? accountHolderName = freezed, + Object? accountType = freezed, + Object? isPrimary = null, + Object? isActive = null, + }) { + return _then( + _value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as String?, + bankName: freezed == bankName + ? _value.bankName + : bankName // ignore: cast_nullable_to_non_nullable + as String?, + branch: freezed == branch + ? _value.branch + : branch // ignore: cast_nullable_to_non_nullable + as String?, + accountNumber: freezed == accountNumber + ? _value.accountNumber + : accountNumber // ignore: cast_nullable_to_non_nullable + as String?, + ifsc: freezed == ifsc + ? _value.ifsc + : ifsc // ignore: cast_nullable_to_non_nullable + as String?, + accountHolderName: freezed == accountHolderName + ? _value.accountHolderName + : accountHolderName // ignore: cast_nullable_to_non_nullable + as String?, + accountType: freezed == accountType + ? _value.accountType + : accountType // ignore: cast_nullable_to_non_nullable + as String?, + isPrimary: null == isPrimary + ? _value.isPrimary + : isPrimary // ignore: cast_nullable_to_non_nullable + as bool, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$VendorBankDetailModelImplCopyWith<$Res> + implements $VendorBankDetailModelCopyWith<$Res> { + factory _$$VendorBankDetailModelImplCopyWith( + _$VendorBankDetailModelImpl value, + $Res Function(_$VendorBankDetailModelImpl) then, + ) = __$$VendorBankDetailModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + @JsonKey(fromJson: _idFromJson) String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) String? vendorId, + @JsonKey(name: 'bank_name') String? bankName, + String? branch, + @JsonKey(name: 'account_number') String? accountNumber, + String? ifsc, + @JsonKey(name: 'account_holder_name') String? accountHolderName, + @JsonKey(name: 'account_type') String? accountType, + @JsonKey(name: 'is_primary') bool isPrimary, + @JsonKey(name: 'is_active') bool isActive, + }); +} + +/// @nodoc +class __$$VendorBankDetailModelImplCopyWithImpl<$Res> + extends + _$VendorBankDetailModelCopyWithImpl<$Res, _$VendorBankDetailModelImpl> + implements _$$VendorBankDetailModelImplCopyWith<$Res> { + __$$VendorBankDetailModelImplCopyWithImpl( + _$VendorBankDetailModelImpl _value, + $Res Function(_$VendorBankDetailModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of VendorBankDetailModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? vendorId = freezed, + Object? bankName = freezed, + Object? branch = freezed, + Object? accountNumber = freezed, + Object? ifsc = freezed, + Object? accountHolderName = freezed, + Object? accountType = freezed, + Object? isPrimary = null, + Object? isActive = null, + }) { + return _then( + _$VendorBankDetailModelImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + vendorId: freezed == vendorId + ? _value.vendorId + : vendorId // ignore: cast_nullable_to_non_nullable + as String?, + bankName: freezed == bankName + ? _value.bankName + : bankName // ignore: cast_nullable_to_non_nullable + as String?, + branch: freezed == branch + ? _value.branch + : branch // ignore: cast_nullable_to_non_nullable + as String?, + accountNumber: freezed == accountNumber + ? _value.accountNumber + : accountNumber // ignore: cast_nullable_to_non_nullable + as String?, + ifsc: freezed == ifsc + ? _value.ifsc + : ifsc // ignore: cast_nullable_to_non_nullable + as String?, + accountHolderName: freezed == accountHolderName + ? _value.accountHolderName + : accountHolderName // ignore: cast_nullable_to_non_nullable + as String?, + accountType: freezed == accountType + ? _value.accountType + : accountType // ignore: cast_nullable_to_non_nullable + as String?, + isPrimary: null == isPrimary + ? _value.isPrimary + : isPrimary // ignore: cast_nullable_to_non_nullable + as bool, + isActive: null == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$VendorBankDetailModelImpl implements _VendorBankDetailModel { + const _$VendorBankDetailModelImpl({ + @JsonKey(fromJson: _idFromJson) required this.id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) this.vendorId, + @JsonKey(name: 'bank_name') this.bankName, + this.branch, + @JsonKey(name: 'account_number') this.accountNumber, + this.ifsc, + @JsonKey(name: 'account_holder_name') this.accountHolderName, + @JsonKey(name: 'account_type') this.accountType, + @JsonKey(name: 'is_primary') this.isPrimary = false, + @JsonKey(name: 'is_active') this.isActive = true, + }); + + factory _$VendorBankDetailModelImpl.fromJson(Map json) => + _$$VendorBankDetailModelImplFromJson(json); + + @override + @JsonKey(fromJson: _idFromJson) + final String id; + @override + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + final String? vendorId; + @override + @JsonKey(name: 'bank_name') + final String? bankName; + @override + final String? branch; + @override + @JsonKey(name: 'account_number') + final String? accountNumber; + @override + final String? ifsc; + @override + @JsonKey(name: 'account_holder_name') + final String? accountHolderName; + @override + @JsonKey(name: 'account_type') + final String? accountType; + @override + @JsonKey(name: 'is_primary') + final bool isPrimary; + @override + @JsonKey(name: 'is_active') + final bool isActive; + + @override + String toString() { + return 'VendorBankDetailModel(id: $id, vendorId: $vendorId, bankName: $bankName, branch: $branch, accountNumber: $accountNumber, ifsc: $ifsc, accountHolderName: $accountHolderName, accountType: $accountType, isPrimary: $isPrimary, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VendorBankDetailModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.vendorId, vendorId) || + other.vendorId == vendorId) && + (identical(other.bankName, bankName) || + other.bankName == bankName) && + (identical(other.branch, branch) || other.branch == branch) && + (identical(other.accountNumber, accountNumber) || + other.accountNumber == accountNumber) && + (identical(other.ifsc, ifsc) || other.ifsc == ifsc) && + (identical(other.accountHolderName, accountHolderName) || + other.accountHolderName == accountHolderName) && + (identical(other.accountType, accountType) || + other.accountType == accountType) && + (identical(other.isPrimary, isPrimary) || + other.isPrimary == isPrimary) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + vendorId, + bankName, + branch, + accountNumber, + ifsc, + accountHolderName, + accountType, + isPrimary, + isActive, + ); + + /// Create a copy of VendorBankDetailModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$VendorBankDetailModelImplCopyWith<_$VendorBankDetailModelImpl> + get copyWith => + __$$VendorBankDetailModelImplCopyWithImpl<_$VendorBankDetailModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$VendorBankDetailModelImplToJson(this); + } +} + +abstract class _VendorBankDetailModel implements VendorBankDetailModel { + const factory _VendorBankDetailModel({ + @JsonKey(fromJson: _idFromJson) required final String id, + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) final String? vendorId, + @JsonKey(name: 'bank_name') final String? bankName, + final String? branch, + @JsonKey(name: 'account_number') final String? accountNumber, + final String? ifsc, + @JsonKey(name: 'account_holder_name') final String? accountHolderName, + @JsonKey(name: 'account_type') final String? accountType, + @JsonKey(name: 'is_primary') final bool isPrimary, + @JsonKey(name: 'is_active') final bool isActive, + }) = _$VendorBankDetailModelImpl; + + factory _VendorBankDetailModel.fromJson(Map json) = + _$VendorBankDetailModelImpl.fromJson; + + @override + @JsonKey(fromJson: _idFromJson) + String get id; + @override + @JsonKey(name: 'vendor_id', fromJson: _idFromJson) + String? get vendorId; + @override + @JsonKey(name: 'bank_name') + String? get bankName; + @override + String? get branch; + @override + @JsonKey(name: 'account_number') + String? get accountNumber; + @override + String? get ifsc; + @override + @JsonKey(name: 'account_holder_name') + String? get accountHolderName; + @override + @JsonKey(name: 'account_type') + String? get accountType; + @override + @JsonKey(name: 'is_primary') + bool get isPrimary; + @override + @JsonKey(name: 'is_active') + bool get isActive; + + /// Create a copy of VendorBankDetailModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$VendorBankDetailModelImplCopyWith<_$VendorBankDetailModelImpl> + get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$VendorListQuery { + int get page => throw _privateConstructorUsedError; + int get limit => throw _privateConstructorUsedError; + String? get search => throw _privateConstructorUsedError; + String? get status => throw _privateConstructorUsedError; + String? get vendorType => throw _privateConstructorUsedError; + bool? get isActive => throw _privateConstructorUsedError; + + /// Create a copy of VendorListQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $VendorListQueryCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VendorListQueryCopyWith<$Res> { + factory $VendorListQueryCopyWith( + VendorListQuery value, + $Res Function(VendorListQuery) then, + ) = _$VendorListQueryCopyWithImpl<$Res, VendorListQuery>; + @useResult + $Res call({ + int page, + int limit, + String? search, + String? status, + String? vendorType, + bool? isActive, + }); +} + +/// @nodoc +class _$VendorListQueryCopyWithImpl<$Res, $Val extends VendorListQuery> + implements $VendorListQueryCopyWith<$Res> { + _$VendorListQueryCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of VendorListQuery + /// 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? vendorType = freezed, + 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?, + vendorType: freezed == vendorType + ? _value.vendorType + : vendorType // 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 _$$VendorListQueryImplCopyWith<$Res> + implements $VendorListQueryCopyWith<$Res> { + factory _$$VendorListQueryImplCopyWith( + _$VendorListQueryImpl value, + $Res Function(_$VendorListQueryImpl) then, + ) = __$$VendorListQueryImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int page, + int limit, + String? search, + String? status, + String? vendorType, + bool? isActive, + }); +} + +/// @nodoc +class __$$VendorListQueryImplCopyWithImpl<$Res> + extends _$VendorListQueryCopyWithImpl<$Res, _$VendorListQueryImpl> + implements _$$VendorListQueryImplCopyWith<$Res> { + __$$VendorListQueryImplCopyWithImpl( + _$VendorListQueryImpl _value, + $Res Function(_$VendorListQueryImpl) _then, + ) : super(_value, _then); + + /// Create a copy of VendorListQuery + /// 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? vendorType = freezed, + Object? isActive = freezed, + }) { + return _then( + _$VendorListQueryImpl( + 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?, + vendorType: freezed == vendorType + ? _value.vendorType + : vendorType // ignore: cast_nullable_to_non_nullable + as String?, + isActive: freezed == isActive + ? _value.isActive + : isActive // ignore: cast_nullable_to_non_nullable + as bool?, + ), + ); + } +} + +/// @nodoc + +class _$VendorListQueryImpl implements _VendorListQuery { + const _$VendorListQueryImpl({ + this.page = 1, + this.limit = 20, + this.search, + this.status, + this.vendorType, + this.isActive, + }); + + @override + @JsonKey() + final int page; + @override + @JsonKey() + final int limit; + @override + final String? search; + @override + final String? status; + @override + final String? vendorType; + @override + final bool? isActive; + + @override + String toString() { + return 'VendorListQuery(page: $page, limit: $limit, search: $search, status: $status, vendorType: $vendorType, isActive: $isActive)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VendorListQueryImpl && + (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.vendorType, vendorType) || + other.vendorType == vendorType) && + (identical(other.isActive, isActive) || + other.isActive == isActive)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + page, + limit, + search, + status, + vendorType, + isActive, + ); + + /// Create a copy of VendorListQuery + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$VendorListQueryImplCopyWith<_$VendorListQueryImpl> get copyWith => + __$$VendorListQueryImplCopyWithImpl<_$VendorListQueryImpl>( + this, + _$identity, + ); +} + +abstract class _VendorListQuery implements VendorListQuery { + const factory _VendorListQuery({ + final int page, + final int limit, + final String? search, + final String? status, + final String? vendorType, + final bool? isActive, + }) = _$VendorListQueryImpl; + + @override + int get page; + @override + int get limit; + @override + String? get search; + @override + String? get status; + @override + String? get vendorType; + @override + bool? get isActive; + + /// Create a copy of VendorListQuery + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$VendorListQueryImplCopyWith<_$VendorListQueryImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/shared/models/vendor_model.g.dart b/lib/shared/models/vendor_model.g.dart new file mode 100644 index 0000000..704ec55 --- /dev/null +++ b/lib/shared/models/vendor_model.g.dart @@ -0,0 +1,152 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'vendor_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$VendorModelImpl _$$VendorModelImplFromJson( + Map json, +) => _$VendorModelImpl( + id: _idFromJson(json['id']), + vendorCode: json['vendor_code'] as String?, + vendorName: json['vendor_name'] as String, + vendorType: json['vendor_type'] as String?, + gstin: json['gstin'] as String?, + pan: json['pan'] as String?, + paymentTermId: _intFromJsonNullable(json['payment_term_id']), + paymentTermName: _readPaymentTermName(json, 'payment_term_name') as String?, + creditPeriodDays: _intFromJsonNullable(json['credit_period_days']), + remarks: json['remarks'] as String?, + status: json['status'] as String?, + isActive: json['is_active'] as bool? ?? true, + createdAt: _dateFromJsonNullable(json['created_at']), + updatedAt: _dateFromJsonNullable(json['updated_at']), + addresses: + (json['addresses'] as List?) + ?.map((e) => VendorAddressModel.fromJson(e as Map)) + .toList() ?? + const [], + contacts: + (json['contacts'] as List?) + ?.map((e) => VendorContactModel.fromJson(e as Map)) + .toList() ?? + const [], + bankDetails: + (json['bank_details'] as List?) + ?.map( + (e) => VendorBankDetailModel.fromJson(e as Map), + ) + .toList() ?? + const [], +); + +Map _$$VendorModelImplToJson(_$VendorModelImpl instance) => + { + 'id': instance.id, + 'vendor_code': instance.vendorCode, + 'vendor_name': instance.vendorName, + 'vendor_type': instance.vendorType, + 'gstin': instance.gstin, + 'pan': instance.pan, + 'payment_term_id': instance.paymentTermId, + 'payment_term_name': instance.paymentTermName, + 'credit_period_days': instance.creditPeriodDays, + 'remarks': instance.remarks, + 'status': instance.status, + 'is_active': instance.isActive, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + 'addresses': instance.addresses, + 'contacts': instance.contacts, + 'bank_details': instance.bankDetails, + }; + +_$VendorAddressModelImpl _$$VendorAddressModelImplFromJson( + Map json, +) => _$VendorAddressModelImpl( + id: _idFromJson(json['id']), + vendorId: _idFromJson(json['vendor_id']), + addressType: json['address_type'] as String?, + addressLine1: json['address_line1'] as String?, + addressLine2: json['address_line2'] as String?, + city: json['city'] as String?, + state: json['state'] as String?, + pincode: json['pincode'] as String?, + country: json['country'] as String?, + gstin: json['gstin'] as String?, + isActive: json['is_active'] as bool? ?? true, +); + +Map _$$VendorAddressModelImplToJson( + _$VendorAddressModelImpl instance, +) => { + 'id': instance.id, + 'vendor_id': instance.vendorId, + 'address_type': instance.addressType, + 'address_line1': instance.addressLine1, + 'address_line2': instance.addressLine2, + 'city': instance.city, + 'state': instance.state, + 'pincode': instance.pincode, + 'country': instance.country, + 'gstin': instance.gstin, + 'is_active': instance.isActive, +}; + +_$VendorContactModelImpl _$$VendorContactModelImplFromJson( + Map json, +) => _$VendorContactModelImpl( + id: _idFromJson(json['id']), + vendorId: _idFromJson(json['vendor_id']), + contactName: json['contact_name'] as String, + designation: json['designation'] as String?, + phone: json['phone'] as String?, + email: json['email'] as String?, + isPrimary: json['is_primary'] as bool? ?? false, + isActive: json['is_active'] as bool? ?? true, +); + +Map _$$VendorContactModelImplToJson( + _$VendorContactModelImpl instance, +) => { + 'id': instance.id, + 'vendor_id': instance.vendorId, + 'contact_name': instance.contactName, + 'designation': instance.designation, + 'phone': instance.phone, + 'email': instance.email, + 'is_primary': instance.isPrimary, + 'is_active': instance.isActive, +}; + +_$VendorBankDetailModelImpl _$$VendorBankDetailModelImplFromJson( + Map json, +) => _$VendorBankDetailModelImpl( + id: _idFromJson(json['id']), + vendorId: _idFromJson(json['vendor_id']), + bankName: json['bank_name'] as String?, + branch: json['branch'] as String?, + accountNumber: json['account_number'] as String?, + ifsc: json['ifsc'] as String?, + accountHolderName: json['account_holder_name'] as String?, + accountType: json['account_type'] as String?, + isPrimary: json['is_primary'] as bool? ?? false, + isActive: json['is_active'] as bool? ?? true, +); + +Map _$$VendorBankDetailModelImplToJson( + _$VendorBankDetailModelImpl instance, +) => { + 'id': instance.id, + 'vendor_id': instance.vendorId, + 'bank_name': instance.bankName, + 'branch': instance.branch, + 'account_number': instance.accountNumber, + 'ifsc': instance.ifsc, + 'account_holder_name': instance.accountHolderName, + 'account_type': instance.accountType, + 'is_primary': instance.isPrimary, + 'is_active': instance.isActive, +}; diff --git a/lib/shared/routes/app_router.dart b/lib/shared/routes/app_router.dart index 029ece4..c708595 100644 --- a/lib/shared/routes/app_router.dart +++ b/lib/shared/routes/app_router.dart @@ -29,6 +29,14 @@ import '../../modules/rbac/presentation/screens/users_role_management_screen.dar 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/grn/presentation/screens/grn_detail_screen.dart'; +import '../../modules/grn/presentation/screens/grn_form_screen.dart'; +import '../../modules/grn/presentation/screens/grn_list_screen.dart'; +import '../../modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart'; +import '../../modules/purchase_orders/presentation/screens/purchase_order_form_screen.dart'; +import '../../modules/purchase_orders/presentation/screens/purchase_order_list_screen.dart'; +import '../../modules/vendors/presentation/screens/vendor_detail_screen.dart'; +import '../../modules/vendors/presentation/screens/vendor_list_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'; @@ -64,12 +72,12 @@ final routerProvider = Provider((ref) { return null; } - // Screen gallery and ?preview=true routes work without login API. + // Screen gallery and ?preview=true routes work without login API (dev only). if (DevConfig.screenPreviewEnabled && location == RouteConstants.screenGallery) { return null; } - if (isPreview) return null; + if (isPreview && DevConfig.screenPreviewEnabled) return null; if (!isAuthenticated && !isAuthRoute) { return RouteConstants.login; @@ -200,6 +208,65 @@ final routerProvider = Provider((ref) { pageBuilder: (context, state) => shellPage(state, const UserProfileScreen()), ), + GoRoute( + path: RouteConstants.purchaseOrders, + pageBuilder: (context, state) => + shellPage(state, const PurchaseOrderListScreen()), + routes: [ + GoRoute( + path: 'add', + builder: (context, state) => const PurchaseOrderFormScreen(), + ), + GoRoute( + path: ':id/edit', + builder: (context, state) => PurchaseOrderFormScreen( + purchaseOrderId: state.pathParameters['id']!, + ), + ), + GoRoute( + path: ':id', + builder: (context, state) => PurchaseOrderDetailScreen( + purchaseOrderId: state.pathParameters['id']!, + ), + ), + ], + ), + GoRoute( + path: RouteConstants.grn, + pageBuilder: (context, state) => + shellPage(state, const GrnListScreen()), + routes: [ + GoRoute( + path: 'add', + builder: (context, state) => const GrnFormScreen(), + ), + GoRoute( + path: ':id/edit', + builder: (context, state) => GrnFormScreen( + grnId: state.pathParameters['id']!, + ), + ), + GoRoute( + path: ':id', + builder: (context, state) => GrnDetailScreen( + grnId: state.pathParameters['id']!, + ), + ), + ], + ), + GoRoute( + path: RouteConstants.vendors, + pageBuilder: (context, state) => + shellPage(state, const VendorListScreen()), + routes: [ + GoRoute( + path: ':id', + builder: (context, state) => VendorDetailScreen( + vendorId: state.pathParameters['id']!, + ), + ), + ], + ), GoRoute( path: RouteConstants.assets, pageBuilder: (context, state) => diff --git a/lib/shared/routes/menu_config.dart b/lib/shared/routes/menu_config.dart index 7fd0bd3..fff9c1d 100644 --- a/lib/shared/routes/menu_config.dart +++ b/lib/shared/routes/menu_config.dart @@ -74,6 +74,24 @@ const List appMenuItems = [ ), ], ), + MenuItem( + label: 'Vendors', + icon: Icons.store_outlined, + route: RouteConstants.vendors, + module: 'vendors', + ), + MenuItem( + label: 'Purchase Orders', + icon: Icons.receipt_long_outlined, + route: RouteConstants.purchaseOrders, + module: 'purchase_orders', + ), + MenuItem( + label: 'GRN', + icon: Icons.inventory_2_outlined, + route: RouteConstants.grn, + module: 'grn', + ), MenuItem( label: 'Master Data', icon: Icons.dataset_outlined, diff --git a/lib/shared/widgets/app_data_table.dart b/lib/shared/widgets/app_data_table.dart index eaeb801..75e7db9 100644 --- a/lib/shared/widgets/app_data_table.dart +++ b/lib/shared/widgets/app_data_table.dart @@ -27,6 +27,8 @@ class AppDataTable extends StatelessWidget { this.sortAscending = true, this.onSort, this.emptyMessage = 'No records found', + this.wrapInCard = true, + this.shrinkWrap = false, }); final List> columns; @@ -35,11 +37,14 @@ class AppDataTable extends StatelessWidget { final bool sortAscending; final void Function(String column, bool ascending)? onSort; final String emptyMessage; + final bool wrapInCard; + /// Set true when the table is placed inside another scrollable. + final bool shrinkWrap; @override Widget build(BuildContext context) { if (rows.isEmpty) { - return Center( + final empty = Center( child: Padding( padding: const EdgeInsets.all(32), child: Text( @@ -50,40 +55,151 @@ class AppDataTable extends StatelessWidget { ), ), ); + if (!wrapInCard) return empty; + return AppCard(clipBehavior: Clip.antiAlias, child: empty); } + final table = LayoutBuilder( + builder: (context, constraints) { + return ListView( + padding: EdgeInsets.zero, + shrinkWrap: shrinkWrap, + physics: shrinkWrap + ? const NeverScrollableScrollPhysics() + : null, + children: [ + _TableHeaderRow( + columns: columns, + sortColumn: sortColumn, + sortAscending: sortAscending, + onSort: onSort, + ), + ...rows.map( + (row) => _TableDataRow( + columns: columns, + row: row, + ), + ), + ], + ); + }, + ); + + if (!wrapInCard) return table; + 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(), - ), - ), + child: table, + ); + } +} + +class _TableHeaderRow extends StatelessWidget { + const _TableHeaderRow({ + required this.columns, + required this.sortColumn, + required this.sortAscending, + required this.onSort, + }); + + final List> columns; + final String? sortColumn; + final bool sortAscending; + final void Function(String column, bool ascending)? onSort; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + width: double.infinity, + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.4), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + children: columns.map((col) { + final isSorted = col.sortKey != null && col.sortKey == sortColumn; + final label = Text( + col.label.toUpperCase(), + style: theme.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: theme.colorScheme.onSurfaceVariant, + ), + ); + + Widget header = label; + if (col.sortKey != null && onSort != null) { + header = InkWell( + onTap: () => onSort!( + col.sortKey!, + isSorted ? !sortAscending : true, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + label, + if (isSorted) ...[ + const SizedBox(width: 4), + Icon( + sortAscending + ? Icons.arrow_upward + : Icons.arrow_downward, + size: 14, + color: theme.colorScheme.onSurfaceVariant, + ), + ], + ], + ), + ); + } + + return Expanded( + flex: col.flex, + child: Align( + alignment: col.alignment, + child: header, + ), + ); + }).toList(), + ), + ); + } +} + +class _TableDataRow extends StatelessWidget { + const _TableDataRow({ + required this.columns, + required this.row, + }); + + final List> columns; + final T row; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Container( + width: double.infinity, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: theme.colorScheme.outline.withValues(alpha: 0.08), + ), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: columns.map((col) { + return Expanded( + flex: col.flex, + child: Align( + alignment: col.alignment, + child: col.cellBuilder(context, row), + ), + ); + }).toList(), ), ); } diff --git a/lib/shared/widgets/app_dropdown.dart b/lib/shared/widgets/app_dropdown.dart index 68c1fdb..70a8aa7 100644 --- a/lib/shared/widgets/app_dropdown.dart +++ b/lib/shared/widgets/app_dropdown.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'app_searchable_dropdown.dart'; + class AppDropdownOption { const AppDropdownOption({required this.value, required this.label}); @@ -7,6 +9,7 @@ class AppDropdownOption { final String label; } +/// Searchable dropdown — all app dropdowns use [AppSearchableDropdown] under the hood. class AppDropdown extends StatelessWidget { const AppDropdown({ super.key, @@ -16,7 +19,9 @@ class AppDropdown extends StatelessWidget { required this.onChanged, this.validator, this.hint, + this.searchHint, this.enabled = true, + this.isDense = false, }); final String label; @@ -25,23 +30,33 @@ class AppDropdown extends StatelessWidget { final ValueChanged onChanged; final String? Function(T?)? validator; final String? hint; + final String? searchHint; final bool enabled; + final bool isDense; @override Widget build(BuildContext context) { - return DropdownButtonFormField( + return AppSearchableDropdown( + label: label, 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, + options: options, + onChanged: onChanged, validator: validator, + hint: hint, + searchHint: searchHint ?? 'Search ${label.toLowerCase()}...', + enabled: enabled, + isDense: isDense, ); } } + +List> stringDropdownOptions(List values) { + return values + .map( + (value) => AppDropdownOption( + value: value, + label: value.replaceAll('_', ' '), + ), + ) + .toList(); +} diff --git a/lib/shared/widgets/app_searchable_dropdown.dart b/lib/shared/widgets/app_searchable_dropdown.dart index 5b8e3a0..383640f 100644 --- a/lib/shared/widgets/app_searchable_dropdown.dart +++ b/lib/shared/widgets/app_searchable_dropdown.dart @@ -55,7 +55,11 @@ class _AppSearchableDropdownState extends State> { if (_overlayEntry == null) return; _overlayEntry!.remove(); _overlayEntry = null; - if (mounted) setState(() {}); + if (mounted) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + }); + } } void _openPicker(FormFieldState field) { diff --git a/lib/shared/widgets/app_side_panel.dart b/lib/shared/widgets/app_side_panel.dart index 4de1592..ed79b23 100644 --- a/lib/shared/widgets/app_side_panel.dart +++ b/lib/shared/widgets/app_side_panel.dart @@ -192,3 +192,52 @@ class SidePanelFormRow extends StatelessWidget { ); } } + +/// Responsive row with up to three equal-width form fields. +class FormRowThree extends StatelessWidget { + const FormRowThree({ + super.key, + required this.children, + this.spacing = 12, + this.stackBelowWidth = 768, + }); + + final List children; + final double spacing; + final double stackBelowWidth; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + if (constraints.maxWidth < stackBelowWidth) { + return Padding( + padding: EdgeInsets.only(bottom: spacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + for (var i = 0; i < children.length; i++) ...[ + if (i > 0) SizedBox(height: spacing), + children[i], + ], + ], + ), + ); + } + + return Padding( + padding: EdgeInsets.only(bottom: spacing), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (var i = 0; i < children.length; i++) ...[ + if (i > 0) SizedBox(width: spacing), + Expanded(child: children[i]), + ], + ], + ), + ); + }, + ); + } +} diff --git a/lib/shared/widgets/app_table_action_icon.dart b/lib/shared/widgets/app_table_action_icon.dart new file mode 100644 index 0000000..ecdda88 --- /dev/null +++ b/lib/shared/widgets/app_table_action_icon.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +/// Compact outline icon for data-table action columns (matches Users & Roles table). +class AppTableActionIcon extends StatelessWidget { + const AppTableActionIcon({ + super.key, + required this.tooltip, + required this.icon, + required this.onPressed, + this.color, + }); + + final String tooltip; + final IconData icon; + final VoidCallback onPressed; + final Color? color; + + @override + Widget build(BuildContext context) { + final iconColor = color ?? Theme.of(context).colorScheme.onSurfaceVariant; + + return Tooltip( + message: tooltip, + child: InkWell( + onTap: onPressed, + borderRadius: BorderRadius.circular(6), + child: Padding( + padding: const EdgeInsets.all(6), + child: Icon(icon, size: 18, color: iconColor), + ), + ), + ); + } +} + +/// Right-aligned row of [AppTableActionIcon] widgets for table cells. +class AppTableActions extends StatelessWidget { + const AppTableActions({super.key, required this.children}); + + final List children; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: children, + ); + } +} diff --git a/lib/shared/widgets/app_table_shell.dart b/lib/shared/widgets/app_table_shell.dart new file mode 100644 index 0000000..04dc345 --- /dev/null +++ b/lib/shared/widgets/app_table_shell.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; + +import 'app_card.dart'; + +/// Card shell for list pages: toolbar, divider, full-width table body, footer. +class AppTableShell extends StatelessWidget { + const AppTableShell({ + super.key, + required this.toolbar, + required this.child, + this.footer, + }); + + final Widget toolbar; + final Widget child; + final Widget? footer; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return AppCard( + enableHover: false, + clipBehavior: Clip.none, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: theme.colorScheme.outline.withValues(alpha: 0.12), + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: toolbar, + ), + const Divider(height: 1), + Expanded(child: child), + if (footer != null) ...[ + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(16), + child: footer!, + ), + ], + ], + ), + ); + } +} diff --git a/lib/shared/widgets/app_text_field.dart b/lib/shared/widgets/app_text_field.dart index 1ea0a04..045af5f 100644 --- a/lib/shared/widgets/app_text_field.dart +++ b/lib/shared/widgets/app_text_field.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; class AppTextField extends StatelessWidget { const AppTextField({ @@ -13,6 +14,8 @@ class AppTextField extends StatelessWidget { this.validator, this.onChanged, this.maxLines = 1, + this.maxLength, + this.inputFormatters, this.enabled = true, this.autofillHints, }); @@ -27,6 +30,8 @@ class AppTextField extends StatelessWidget { final String? Function(String?)? validator; final void Function(String)? onChanged; final int maxLines; + final int? maxLength; + final List? inputFormatters; final bool enabled; final Iterable? autofillHints; @@ -39,6 +44,8 @@ class AppTextField extends StatelessWidget { validator: validator, onChanged: onChanged, maxLines: maxLines, + maxLength: maxLength, + inputFormatters: inputFormatters, enabled: enabled, autofillHints: autofillHints, decoration: InputDecoration( diff --git a/pubspec.lock b/pubspec.lock index cdab5da..3acee31 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -404,7 +404,7 @@ packages: source: sdk version: "0.0.0" flutter_web_plugins: - dependency: transitive + dependency: "direct main" description: flutter source: sdk version: "0.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 85c12fb..1963d66 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_web_plugins: + sdk: flutter cupertino_icons: ^1.0.8 diff --git a/test/core/validators_test.dart b/test/core/validators_test.dart new file mode 100644 index 0000000..41f4afc --- /dev/null +++ b/test/core/validators_test.dart @@ -0,0 +1,80 @@ +import 'package:bharat_erp/core/utils/validators.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('email', () { + test('accepts valid addresses', () { + expect(Validators.email('user@example.com'), isNull); + expect(Validators.email('user.name+tag@company.co.in'), isNull); + }); + + test('rejects invalid addresses', () { + expect(Validators.email('not-an-email'), isNotNull); + expect(Validators.email('user@'), isNotNull); + }); + + test('optional allows empty', () { + expect(Validators.optionalEmail(''), isNull); + expect(Validators.optionalEmail('bad'), isNotNull); + }); + }); + + group('accountNumber', () { + test('accepts 9–18 digits', () { + expect(Validators.accountNumber('123456789'), isNull); + expect(Validators.accountNumber('123456789012345678'), isNull); + }); + + test('rejects invalid lengths and non-digits', () { + expect(Validators.accountNumber('12345'), isNotNull); + expect(Validators.accountNumber('1234567890123456789'), isNotNull); + expect(Validators.accountNumber('12345ABC'), isNotNull); + }); + }); + + group('ifsc', () { + test('accepts valid IFSC', () { + expect(Validators.ifsc('SBIN0001234'), isNull); + expect(Validators.ifsc('sbin0001234'), isNull); + }); + + test('rejects invalid IFSC', () { + expect(Validators.ifsc('SBIN001234'), isNotNull); + expect(Validators.ifsc('SBIN00012345'), isNotNull); + expect(Validators.ifsc('SB1N0001234'), isNotNull); + }); + }); + + group('pincode', () { + test('accepts valid 6-digit pincode', () { + expect(Validators.pincode('560001'), isNull); + }); + + test('rejects invalid pincode', () { + expect(Validators.pincode('056001'), isNotNull); + expect(Validators.pincode('56001'), isNotNull); + expect(Validators.pincode('5600011'), isNotNull); + }); + + test('optional allows empty', () { + expect(Validators.optionalPincode(''), isNull); + }); + }); + + group('forFieldKey', () { + test('routes email and pincode keys', () { + expect( + Validators.forFieldKey('email', 'a@b.com', required: true), + isNull, + ); + expect( + Validators.forFieldKey('pincode', '560001', required: false), + isNull, + ); + expect( + Validators.forFieldKey('ifsc', 'HDFC0001234', required: true), + isNull, + ); + }); + }); +} diff --git a/test/security/security_test.dart b/test/security/security_test.dart new file mode 100644 index 0000000..baf5168 --- /dev/null +++ b/test/security/security_test.dart @@ -0,0 +1,118 @@ +import 'package:bharat_erp/core/constants/enums.dart'; +import 'package:bharat_erp/core/utils/permission_utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Permission security', () { + test('wildcard grants all module actions', () { + expect( + hasPermission( + userPermissions: const ['*'], + module: 'assets', + action: PermissionAction.delete, + ), + isTrue, + ); + }); + + test('denies action when permission missing', () { + expect( + hasPermission( + userPermissions: const ['ASSET:read'], + module: 'assets', + action: PermissionAction.delete, + ), + isFalse, + ); + }); + + test('grants read via view alias', () { + expect( + hasPermission( + userPermissions: const ['asset:view'], + module: 'assets', + action: PermissionAction.read, + ), + isTrue, + ); + }); + + test('grants update via edit alias', () { + expect( + hasPermission( + userPermissions: const ['ASSET:edit'], + module: 'assets', + action: PermissionAction.update, + ), + isTrue, + ); + }); + + test('module aliases map master_data to MASTERS', () { + expect(normalizePermissionModule('master_data'), 'MASTERS'); + expect(normalizePermissionModule('assets'), 'ASSET'); + }); + + test('super admin can see all menu modules', () { + expect( + canSeeMenuModule( + permissions: const [], + module: 'assets', + role: UserRole.superAdmin, + ), + isTrue, + ); + }); + + test('employee without permissions cannot see assets menu', () { + expect( + canSeeMenuModule( + permissions: const [], + module: 'assets', + role: UserRole.employee, + ), + isFalse, + ); + }); + + test('users menu visible with roles read permission', () { + expect( + canSeeMenuModule( + permissions: const ['ROLES:read'], + module: 'users', + role: UserRole.employee, + ), + isTrue, + ); + }); + }); + + group('Route preview guard logic', () { + bool shouldAllowUnauthenticatedPreview({ + required bool isPreview, + required bool screenPreviewEnabled, + }) { + return isPreview && screenPreviewEnabled; + } + + test('preview query blocked when screen preview disabled (production)', () { + expect( + shouldAllowUnauthenticatedPreview( + isPreview: true, + screenPreviewEnabled: false, + ), + isFalse, + ); + }); + + test('preview query allowed only in dev preview mode', () { + expect( + shouldAllowUnauthenticatedPreview( + isPreview: true, + screenPreviewEnabled: true, + ), + isTrue, + ); + }); + }); +} diff --git a/web/.htaccess b/web/.htaccess new file mode 100644 index 0000000..8f0842b --- /dev/null +++ b/web/.htaccess @@ -0,0 +1,9 @@ +# SPA fallback for Flutter web path URL strategy (dev: /erp/). + + RewriteEngine On + RewriteBase /erp/ + RewriteRule ^index\.html$ - [L] + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule . /erp/index.html [L] + diff --git a/web/index.html b/web/index.html index f82bb7c..f9f01f0 100644 --- a/web/index.html +++ b/web/index.html @@ -16,6 +16,23 @@ --> + +