diff --git a/lib/modules/assets/presentation/screens/asset_detail_screen.dart b/lib/modules/assets/presentation/screens/asset_detail_screen.dart index 799ee92..2c18849 100644 --- a/lib/modules/assets/presentation/screens/asset_detail_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_detail_screen.dart @@ -6,6 +6,7 @@ import 'package:intl/intl.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/asset_model.dart'; import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/user_management_models.dart'; @@ -23,6 +24,7 @@ import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_segmented_tab_bar.dart'; import '../providers/assets_provider.dart'; import '../providers/asset_form_lookups_provider.dart'; +import '../utils/maintenance_due_display.dart'; import 'asset_form_screen.dart'; import '../widgets/asset_maintenance_panel.dart'; import '../widgets/asset_side_panels.dart'; @@ -236,6 +238,13 @@ class _OverviewTab extends ConsumerWidget { asset.maintenanceInchargeUserId, users, ); + final daysUntilDueDisplay = MaintenanceDueDisplay.fromDaysUntilDue( + asset.maintenance?.daysUntilDue, + ); + final retainedPct = _retainedPercentage( + currentValue: asset.resolvedCurrentValue, + purchaseCost: asset.purchaseCost, + ); return SingleChildScrollView( child: Center( @@ -267,95 +276,176 @@ class _OverviewTab extends ConsumerWidget { ), ], ), - const SizedBox(height: 12), - _AssetInfoGrid( - items: [ - _AssetInfo('Asset Name', asset.assetName), - _AssetInfo('Asset Code', asset.assetCode ?? '—'), - _AssetInfo('Category', asset.assetCategoryName ?? '—'), - _AssetInfo( - 'Subcategory', - asset.assetSubcategoryName ?? '—', - ), - _AssetInfo('Location', asset.locationName ?? '—'), - _AssetInfo( - 'Commencement Date', - asset.commencementDate != null - ? dateFormat.format(asset.commencementDate!) - : '—', - ), - _AssetInfo( - 'Maintenance Incharge', - maintenanceInchargeLabel, - ), - _AssetInfo( - 'Maintenance Frequency', - asset.maintenanceFrequencyInDays != null - ? '${asset.maintenanceFrequencyInDays} days' - : '—', - ), - if (asset.maintenance != null) ...[ - _AssetInfo( - 'Maintenance Due', - asset.maintenance!.isDue ? 'Yes' : 'No', + const SizedBox(height: 16), + + // 1) Top Summary + _AssetOverviewSection( + title: 'Summary', + child: _AssetInfoGrid( + items: [ + _AssetInfo.widget( + 'Status', + AppStatusChip(status: asset.status ?? 'IN_USE'), ), _AssetInfo( - 'Next Due Date', - asset.maintenance!.nextDueDate != null - ? dateFormat - .format(asset.maintenance!.nextDueDate!) + 'Maintenance Due', + asset.maintenance == null + ? '—' + : (asset.maintenance!.isDue ? 'Yes' : 'No'), + ), + _AssetInfo( + 'Current Value', + asset.resolvedCurrentValue != null + ? CurrencyFormatter.format( + asset.resolvedCurrentValue, + ) + : '—', + ), + _AssetInfo.widget( + 'Active', + _ActiveDotIndicator(isActive: asset.isActive), + ), + ], + ), + ), + + // 2) Identity + _AssetOverviewSection( + title: 'Identity', + child: _AssetInfoGrid( + items: [ + _AssetInfo('Asset Name', asset.assetName), + _AssetInfo('Asset Code', asset.assetCode ?? '—'), + _AssetInfo('Location', asset.locationName ?? '—'), + _AssetInfo( + 'Asset Category', + asset.assetCategoryName ?? '—', + ), + _AssetInfo( + 'Subcategory', + asset.assetSubcategoryName ?? '—', + ), + _AssetInfo( + 'Manufacturer', + asset.manufacturer ?? '—', + ), + _AssetInfo( + 'Brand / Model', + asset.brandModel ?? '—', + ), + _AssetInfo( + 'Serial Number', + asset.serialNumber ?? '—', + ), + ], + ), + ), + + // 3) Maintenance + _AssetOverviewSection( + title: 'Maintenance', + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _AssetInfoGrid( + items: [ + _AssetInfo( + 'Maintenance Incharge', + maintenanceInchargeLabel, + ), + _AssetInfo( + 'Maintenance Frequency', + asset.maintenanceFrequencyInDays != null + ? '${asset.maintenanceFrequencyInDays} days' + : '—', + ), + _AssetInfo( + 'Next Due Date', + asset.maintenance?.nextDueDate != null + ? dateFormat.format( + asset.maintenance!.nextDueDate!, + ) + : '—', + ), + if (daysUntilDueDisplay == null) + const _AssetInfo('Days Until Due', '—') + else + _AssetInfo.widget( + 'Days Until Due', + Text( + daysUntilDueDisplay.label, + style: TextStyle( + color: daysUntilDueDisplay.color, + fontWeight: FontWeight.w600, + ), + ), + ), + _AssetInfo( + 'Condition', + assetConditionLabel(asset.condition), + ), + ], + ), + if (asset.maintenance != null) ...[ + const SizedBox(height: 12), + _MaintenanceDueCallout( + isDue: asset.maintenance!.isDue, + daysUntilDue: asset.maintenance!.daysUntilDue, + display: daysUntilDueDisplay, + ), + ], + ], + ), + ), + + // 4) Purchase & Warranty + _AssetOverviewSection( + title: 'Purchase & Warranty', + child: _AssetInfoGrid( + items: [ + _AssetInfo( + 'Purchase Date', + asset.purchaseDate != null + ? dateFormat.format(asset.purchaseDate!) : '—', ), _AssetInfo( - 'Days Until Due', - asset.maintenance!.daysUntilDue?.toString() ?? '—', + 'Purchase Cost', + asset.purchaseCost != null + ? CurrencyFormatter.format(asset.purchaseCost) + : '—', + ), + _AssetInfo( + 'Warranty Expiry', + asset.warrantyExpiryDate != null + ? dateFormat.format(asset.warrantyExpiryDate!) + : '—', ), ], - _AssetInfo('Serial Number', asset.serialNumber ?? '—'), - _AssetInfo('Brand / Model', asset.brandModel ?? '—'), - _AssetInfo('Manufacturer', asset.manufacturer ?? '—'), - _AssetInfo( - 'Purchase Date', - asset.purchaseDate != null - ? dateFormat.format(asset.purchaseDate!) - : '—', - ), - _AssetInfo( - 'Warranty Expiry', - asset.warrantyExpiryDate != null - ? dateFormat.format(asset.warrantyExpiryDate!) - : '—', - ), - _AssetInfo( - 'Purchase Cost', - asset.purchaseCost != null - ? '₹${asset.purchaseCost}' - : '—', - ), - _AssetInfo( - 'Useful Life', - asset.usefulLifeYears != null - ? '${asset.usefulLifeYears} years' - : '—', - ), - _AssetInfo( - 'Depreciation', - asset.depreciationMethod != null - ? '${asset.depreciationMethod}' - '${asset.depreciationRate != null ? ' (${asset.depreciationRate}%)' : ''}' - : '—', - ), - _AssetInfo( - 'Condition', - assetConditionLabel(asset.condition), - ), - _AssetInfo.widget( - 'Status', - AppStatusChip(status: asset.status ?? 'IN_USE'), - ), - _AssetInfo('Active', asset.isActive ? 'Yes' : 'No'), - ], + ), ), + + // 5) Valuation & Depreciation + _AssetOverviewSection( + title: 'Valuation & Depreciation', + showDivider: false, + child: _ValuationDepreciationCard( + currentValue: asset.resolvedCurrentValue, + purchaseCost: asset.purchaseCost, + retainedPct: retainedPct, + commencementDate: asset.commencementDate, + usefulLifeYears: asset.usefulLifeYears, + depreciationMethod: asset.depreciationMethod, + depreciationRate: asset.depreciationRate, + annualDepreciation: + asset.depreciation?.annualDepreciation, + depreciatedAmount: asset.depreciatedAmount, + salvageValue: asset.salvageValue, + salvagePercentage: asset.salvagePercentage, + dateFormat: dateFormat, + ), + ), + if (asset.maintenanceFrequencyInDays != null || asset.maintenanceChecklistJson?.isNotEmpty == true || asset.maintenance != null) ...[ @@ -434,6 +524,388 @@ class _OverviewTab extends ConsumerWidget { } } +double? _retainedPercentage({ + required double? currentValue, + required double? purchaseCost, +}) { + if (currentValue == null || purchaseCost == null || purchaseCost <= 0) { + return null; + } + return (currentValue / purchaseCost * 100).clamp(0, 100); +} + +class _AssetOverviewSection extends StatelessWidget { + const _AssetOverviewSection({ + required this.title, + required this.child, + this.showDivider = true, + }); + + final String title; + final Widget child; + final bool showDivider; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + title.toUpperCase(), + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.w700, + letterSpacing: 0.4, + ), + ), + const SizedBox(height: 12), + child, + if (showDivider) + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Divider(height: 1), + ) + else + const SizedBox(height: 8), + ], + ); + } +} + +class _ActiveDotIndicator extends StatelessWidget { + const _ActiveDotIndicator({required this.isActive}); + + final bool isActive; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final color = + isActive ? const Color(0xFF16A34A) : theme.colorScheme.onSurfaceVariant; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: color, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + isActive ? 'Active' : 'Inactive', + style: theme.textTheme.bodyLarge?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ], + ); + } +} + +class _MaintenanceDueCallout extends StatelessWidget { + const _MaintenanceDueCallout({ + required this.isDue, + required this.daysUntilDue, + required this.display, + }); + + final bool isDue; + final int? daysUntilDue; + final MaintenanceDueDisplay? display; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final color = display?.color ?? theme.colorScheme.onSurfaceVariant; + final suffix = () { + if (daysUntilDue == null) return null; + if (daysUntilDue! > 0) { + return 'next service in $daysUntilDue day${daysUntilDue == 1 ? '' : 's'}'; + } + if (daysUntilDue == 0) return 'due today'; + final overdueBy = daysUntilDue!.abs(); + return 'overdue by $overdueBy day${overdueBy == 1 ? '' : 's'}'; + }(); + + final text = suffix == null + ? 'Maintenance Due: ${isDue ? 'Yes' : 'No'}' + : 'Maintenance Due: ${isDue ? 'Yes' : 'No'} — $suffix'; + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.08), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withValues(alpha: 0.28)), + ), + child: Text( + text, + style: theme.textTheme.bodyMedium?.copyWith( + color: color, + fontWeight: FontWeight.w600, + ), + ), + ); + } +} + +class _ValuationDepreciationCard extends StatelessWidget { + const _ValuationDepreciationCard({ + required this.currentValue, + required this.purchaseCost, + required this.retainedPct, + required this.commencementDate, + required this.usefulLifeYears, + required this.depreciationMethod, + required this.depreciationRate, + required this.annualDepreciation, + required this.depreciatedAmount, + required this.salvageValue, + required this.salvagePercentage, + required this.dateFormat, + }); + + final double? currentValue; + final double? purchaseCost; + final double? retainedPct; + final DateTime? commencementDate; + final int? usefulLifeYears; + final String? depreciationMethod; + final double? depreciationRate; + final double? annualDepreciation; + final double? depreciatedAmount; + final double? salvageValue; + final double? salvagePercentage; + final DateFormat dateFormat; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final scheme = theme.colorScheme; + final onCard = scheme.onPrimary; + final muted = onCard.withValues(alpha: 0.72); + final progress = retainedPct == null ? 0.0 : retainedPct! / 100; + + final subtitleParts = []; + if (purchaseCost != null) { + subtitleParts.add( + 'of ${CurrencyFormatter.format(purchaseCost)} purchase cost', + ); + } + if (commencementDate != null) { + subtitleParts.add('commenced ${dateFormat.format(commencementDate!)}'); + } + + final methodLabel = depreciationMethod == null + ? '—' + : '$depreciationMethod' + '${depreciationRate != null ? ' ($depreciationRate%)' : ''}'; + + final detailItems = <(String, String)>[ + ( + 'Commencement Date', + commencementDate != null ? dateFormat.format(commencementDate!) : '—', + ), + ( + 'Useful Life', + usefulLifeYears != null ? '$usefulLifeYears years' : '—', + ), + ('Depreciation Method', methodLabel), + ( + 'Annual Depreciation', + annualDepreciation != null + ? CurrencyFormatter.format(annualDepreciation) + : '—', + ), + ( + 'Depreciated Amount', + depreciatedAmount != null + ? CurrencyFormatter.format(depreciatedAmount) + : '—', + ), + ( + 'Salvage Value', + salvageValue != null ? CurrencyFormatter.format(salvageValue) : '—', + ), + ( + 'Salvage Percentage', + salvagePercentage != null ? '$salvagePercentage%' : '—', + ), + ]; + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + scheme.primary, + Color.lerp(scheme.primary, scheme.secondary, 0.55)!, + scheme.secondary, + ], + stops: const [0, 0.55, 1], + ), + boxShadow: [ + BoxShadow( + color: scheme.primary.withValues(alpha: 0.28), + blurRadius: 18, + offset: const Offset(0, 8), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 88, + height: 88, + child: Stack( + alignment: Alignment.center, + children: [ + SizedBox( + width: 88, + height: 88, + child: CircularProgressIndicator( + value: progress, + strokeWidth: 7, + backgroundColor: onCard.withValues(alpha: 0.18), + color: Color.lerp(scheme.secondary, Colors.white, 0.45)!, + strokeCap: StrokeCap.round, + ), + ), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + retainedPct == null + ? '—' + : '${retainedPct!.toStringAsFixed(2)}%', + style: theme.textTheme.titleMedium?.copyWith( + color: onCard, + fontWeight: FontWeight.w800, + ), + ), + Text( + 'RETAINED', + style: theme.textTheme.labelSmall?.copyWith( + color: muted, + fontWeight: FontWeight.w600, + letterSpacing: 0.6, + fontSize: 9, + ), + ), + ], + ), + ], + ), + ), + const SizedBox(width: 18), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'CURRENT VALUE', + style: theme.textTheme.labelSmall?.copyWith( + color: muted, + fontWeight: FontWeight.w600, + letterSpacing: 0.8, + ), + ), + const SizedBox(height: 4), + Text( + currentValue != null + ? CurrencyFormatter.format(currentValue) + : '—', + style: theme.textTheme.headlineSmall?.copyWith( + color: onCard, + fontWeight: FontWeight.w800, + ), + ), + if (subtitleParts.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + subtitleParts.join(' · '), + style: theme.textTheme.bodySmall?.copyWith( + color: muted, + ), + ), + ], + ], + ), + ), + ], + ), + const SizedBox(height: 18), + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: onCard.withValues(alpha: 0.10), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: onCard.withValues(alpha: 0.12)), + ), + child: LayoutBuilder( + builder: (context, constraints) { + final cols = constraints.maxWidth < 560 + ? 2 + : constraints.maxWidth < 820 + ? 3 + : 4; + const spacing = 16.0; + final colWidth = (constraints.maxWidth - spacing * (cols - 1)) / + cols; + return Wrap( + spacing: spacing, + runSpacing: 14, + children: [ + for (final item in detailItems) + SizedBox( + width: colWidth, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.$1, + style: theme.textTheme.labelSmall?.copyWith( + color: muted, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + item.$2, + style: theme.textTheme.bodyMedium?.copyWith( + color: onCard, + fontWeight: FontWeight.w700, + ), + ), + ], + ), + ), + ], + ); + }, + ), + ), + ], + ), + ); + } +} + class _AssetInfoGrid extends StatelessWidget { const _AssetInfoGrid({required this.items}); diff --git a/lib/modules/assets/presentation/screens/asset_form_screen.dart b/lib/modules/assets/presentation/screens/asset_form_screen.dart index 3263d40..675826e 100644 --- a/lib/modules/assets/presentation/screens/asset_form_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_form_screen.dart @@ -82,6 +82,7 @@ class _AssetFormScreenState extends ConsumerState { final _usefulLifeController = TextEditingController(); final _depreciationRateController = TextEditingController(); final _salvageValueController = TextEditingController(); + final _salvagePercentageController = TextEditingController(); final _remarksController = TextEditingController(); final _frequencyController = TextEditingController(); int? _categoryId; @@ -97,6 +98,8 @@ class _AssetFormScreenState extends ConsumerState { String? _status; String? _condition; String? _depreciationMethod; + /// When true, salvage is entered as % of purchase cost; otherwise as ₹ amount. + bool _salvageAsPercentage = false; DateTime? _warrantyExpiry; DateTime? _purchaseDate; DateTime? _commencementDate; @@ -123,6 +126,7 @@ class _AssetFormScreenState extends ConsumerState { super.initState(); _costController.addListener(_onDepreciationFieldChanged); _salvageValueController.addListener(_onDepreciationFieldChanged); + _salvagePercentageController.addListener(_onDepreciationFieldChanged); _usefulLifeController.addListener(_onDepreciationFieldChanged); _depreciationRateController.addListener(_onDepreciationFieldChanged); } @@ -142,6 +146,7 @@ class _AssetFormScreenState extends ConsumerState { _scrollController.dispose(); _costController.removeListener(_onDepreciationFieldChanged); _salvageValueController.removeListener(_onDepreciationFieldChanged); + _salvagePercentageController.removeListener(_onDepreciationFieldChanged); _usefulLifeController.removeListener(_onDepreciationFieldChanged); _depreciationRateController.removeListener(_onDepreciationFieldChanged); _nameController.dispose(); @@ -157,6 +162,7 @@ class _AssetFormScreenState extends ConsumerState { _usefulLifeController.dispose(); _depreciationRateController.dispose(); _salvageValueController.dispose(); + _salvagePercentageController.dispose(); _remarksController.dispose(); _frequencyController.dispose(); super.dispose(); @@ -166,6 +172,71 @@ class _AssetFormScreenState extends ConsumerState { _triggerPreviewRecalculation(); } + /// Backend renamed OTHER → CUSTOM; accept both while options refresh. + String? get _normalizedDepreciationMethod { + final method = _depreciationMethod?.trim().toUpperCase(); + if (method == null || method.isEmpty) return null; + if (method == 'OTHER') return 'CUSTOM'; + return method; + } + + bool get _requiresCustomRate { + final method = _normalizedDepreciationMethod; + return method == 'CUSTOM'; + } + + void _applySalvageFromAsset(AssetModel asset) { + _salvageValueController.text = + CurrencyFormatter.formatEditable(asset.salvageValue); + _salvagePercentageController.text = + asset.salvagePercentage != null + ? CurrencyFormatter.formatEditable(asset.salvagePercentage) + : ''; + // Prefer % mode when percentage is present (API syncs both). + _salvageAsPercentage = asset.salvagePercentage != null; + } + + void _setSalvageInputMode(bool asPercentage) { + if (_salvageAsPercentage == asPercentage) return; + final purchaseCost = CurrencyFormatter.tryParse(_costController.text); + setState(() { + if (asPercentage) { + final amount = CurrencyFormatter.tryParse(_salvageValueController.text); + if (purchaseCost != null && + purchaseCost > 0 && + amount != null && + _salvagePercentageController.text.trim().isEmpty) { + final pct = (amount / purchaseCost) * 100; + _salvagePercentageController.text = + CurrencyFormatter.formatEditable(pct); + } + } else { + final pct = + CurrencyFormatter.tryParse(_salvagePercentageController.text); + if (purchaseCost != null && + pct != null && + _salvageValueController.text.trim().isEmpty) { + final amount = purchaseCost * pct / 100; + _salvageValueController.text = + CurrencyFormatter.formatEditable(amount); + } + } + _salvageAsPercentage = asPercentage; + }); + _triggerPreviewRecalculation(); + } + + void _putSalvageIntoPayload(Map payload) { + // Prefer sending only one salvage field; % wins when both modes apply. + if (_salvageAsPercentage) { + final pct = + CurrencyFormatter.tryParse(_salvagePercentageController.text); + if (pct != null) payload['salvage_percentage'] = pct; + return; + } + _putOptionalDouble(payload, 'salvage_value', _salvageValueController.text); + } + String _assetSignature(AssetModel asset) => '${asset.id}:${asset.assetCategoryId}:${asset.assetSubcategoryId}:' '${asset.locationId}:${asset.status}:${asset.assetName}'; @@ -226,7 +297,10 @@ class _AssetFormScreenState extends ConsumerState { _grnItemId = _nullablePositiveId(asset.grnItemId); _status = asset.status; _condition = asset.condition; - _depreciationMethod = asset.depreciationMethod; + _depreciationMethod = asset.depreciationMethod?.trim().toUpperCase() == + 'OTHER' + ? 'CUSTOM' + : asset.depreciationMethod; _warrantyExpiry = asset.warrantyExpiryDate; _purchaseDate = asset.purchaseDate; _commencementDate = asset.commencementDate; @@ -246,8 +320,7 @@ class _AssetFormScreenState extends ConsumerState { CurrencyFormatter.formatEditable(asset.disposalValue); _usefulLifeController.text = asset.usefulLifeYears?.toString() ?? ''; _depreciationRateController.text = asset.depreciationRate?.toString() ?? ''; - _salvageValueController.text = - CurrencyFormatter.formatEditable(asset.salvageValue); + _applySalvageFromAsset(asset); _remarksController.text = asset.remarks ?? ''; _frequencyController.text = asset.maintenanceFrequencyInDays?.toString() ?? ''; @@ -268,13 +341,13 @@ class _AssetFormScreenState extends ConsumerState { } Map? _buildDepreciationPreviewPayload() { - final method = _depreciationMethod?.trim().toUpperCase(); + final method = _normalizedDepreciationMethod; if (method == null || method.isEmpty) { return null; } final rate = CurrencyFormatter.tryParse(_depreciationRateController.text); - if (method == 'OTHER' && rate == null) { + if (_requiresCustomRate && rate == null) { return null; } @@ -286,9 +359,7 @@ class _AssetFormScreenState extends ConsumerState { final purchaseCost = CurrencyFormatter.tryParse(_costController.text); if (purchaseCost != null) payload['purchase_cost'] = purchaseCost; - final salvageValue = - CurrencyFormatter.tryParse(_salvageValueController.text); - if (salvageValue != null) payload['salvage_value'] = salvageValue; + _putSalvageIntoPayload(payload); final usefulLife = int.tryParse(_usefulLifeController.text.trim()); if (usefulLife != null) payload['useful_life_years'] = usefulLife; @@ -312,8 +383,8 @@ class _AssetFormScreenState extends ConsumerState { setState(() { _isDepreciationPreviewLoading = false; _depreciationPreview = null; - _depreciationPreviewError = _depreciationMethod?.trim().toUpperCase() == 'OTHER' - ? 'Depreciation rate is required for OTHER' + _depreciationPreviewError = _requiresCustomRate + ? 'Depreciation rate is required for CUSTOM' : null; }); return; @@ -393,7 +464,7 @@ class _AssetFormScreenState extends ConsumerState { } _putOptionalDouble(payload, 'purchase_cost', _costController.text); - _putOptionalDouble(payload, 'salvage_value', _salvageValueController.text); + _putSalvageIntoPayload(payload); _putOptionalDouble(payload, 'disposal_value', _disposalValueController.text); final usefulLife = int.tryParse(_usefulLifeController.text.trim()); @@ -417,8 +488,9 @@ class _AssetFormScreenState extends ConsumerState { payload['maintenance_checklist_json'] = checklistPayload; } - if (_depreciationMethod != null) { - payload['depreciation_method'] = _depreciationMethod; + final method = _normalizedDepreciationMethod; + if (method != null) { + payload['depreciation_method'] = method; } final depreciationRate = @@ -516,23 +588,30 @@ class _AssetFormScreenState extends ConsumerState { } final purchaseCost = CurrencyFormatter.tryParse(_costController.text); - final salvageValue = - CurrencyFormatter.tryParse(_salvageValueController.text); - if (purchaseCost != null && - salvageValue != null && - salvageValue > purchaseCost) { - return 'Salvage value cannot exceed purchase cost'; + if (_salvageAsPercentage) { + final pct = + CurrencyFormatter.tryParse(_salvagePercentageController.text); + if (pct != null && (pct < 0 || pct > 100)) { + return 'Salvage percentage must be between 0 and 100'; + } + } else { + final salvageValue = + CurrencyFormatter.tryParse(_salvageValueController.text); + if (purchaseCost != null && + salvageValue != null && + salvageValue > purchaseCost) { + return 'Salvage value cannot exceed purchase cost'; + } } - final hasDepreciationMethod = - _depreciationMethod != null && _depreciationMethod!.trim().isNotEmpty; + final hasDepreciationMethod = _normalizedDepreciationMethod != null; if (hasDepreciationMethod) { if (_usefulLifeController.text.trim().isEmpty) { return 'Useful life is required when depreciation method is set'; } - final method = _depreciationMethod!.trim().toUpperCase(); - if (method == 'OTHER' && _depreciationRateController.text.trim().isEmpty) { - return 'Depreciation rate is required when depreciation method is OTHER'; + if (_requiresCustomRate && + _depreciationRateController.text.trim().isEmpty) { + return 'Depreciation rate is required when depreciation method is CUSTOM'; } } @@ -887,8 +966,10 @@ class _AssetFormScreenState extends ConsumerState { AppDropdown( isDense: true, label: 'Depreciation Method', - value: _depreciationMethod, - options: assetOptionDropdowns(depreciationMethods), + value: _normalizedDepreciationMethod, + options: _depreciationMethodOptions( + depreciationMethods, + ), enabled: depreciationMethods.isNotEmpty, hint: depreciationMethods.isEmpty ? 'Loading depreciation methods...' @@ -910,20 +991,7 @@ class _AssetFormScreenState extends ConsumerState { fieldName: 'Depreciation Rate', ), ), - AppTextField( - isDense: true, - controller: _salvageValueController, - label: 'Salvage Value', - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - inputFormatters: CurrencyFormatter.amountInput, - validator: (v) => - Validators.optionalNonNegativeDouble( - v, - fieldName: 'Salvage Value', - ), - ), + _buildSalvageField(), ], ), _DepreciationPreviewCard( @@ -1283,6 +1351,89 @@ class _AssetFormScreenState extends ConsumerState { ); } + List> _depreciationMethodOptions( + List methods, + ) { + final mapped = >[]; + final seen = {}; + for (final option in assetOptionDropdowns(methods)) { + final raw = option.value.trim().toUpperCase(); + final value = raw == 'OTHER' ? 'CUSTOM' : option.value; + final key = value.trim().toUpperCase(); + if (key.isEmpty || seen.contains(key)) continue; + seen.add(key); + mapped.add( + AppDropdownOption( + value: value, + label: key == 'CUSTOM' ? 'CUSTOM' : option.label, + ), + ); + } + return mapped; + } + + Widget _buildSalvageField() { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + 'Salvage', + style: theme.textTheme.labelLarge?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 6), + SegmentedButton( + segments: const [ + ButtonSegment( + value: false, + label: Text('Amount'), + icon: Icon(Icons.currency_rupee, size: 16), + ), + ButtonSegment( + value: true, + label: Text('%'), + icon: Icon(Icons.percent, size: 16), + ), + ], + selected: {_salvageAsPercentage}, + onSelectionChanged: (selected) { + _setSalvageInputMode(selected.first); + }, + style: ButtonStyle( + visualDensity: VisualDensity.compact, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + const SizedBox(height: 8), + if (_salvageAsPercentage) + AppTextField( + isDense: true, + controller: _salvagePercentageController, + label: 'Salvage Percentage (%)', + keyboardType: const TextInputType.numberWithOptions(decimal: true), + validator: (v) => Validators.optionalPercentage( + v, + fieldName: 'Salvage Percentage', + ), + ) + else + AppTextField( + isDense: true, + controller: _salvageValueController, + label: 'Salvage Value (₹)', + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: CurrencyFormatter.amountInput, + validator: (v) => Validators.optionalNonNegativeDouble( + v, + fieldName: 'Salvage Value', + ), + ), + ], + ); + } + Widget _optionalLookupDropdown({ required String label, required int? value, @@ -1686,6 +1837,15 @@ class _DepreciationPreviewCard extends StatelessWidget { label: 'Current Book Value', value: money(preview!.bookValue), ), + _PreviewItem( + label: 'Salvage Value', + value: money(preview!.salvageValue), + ), + if (preview!.salvagePercentage != null) + _PreviewItem( + label: 'Salvage %', + value: '${preview!.salvagePercentage!.toStringAsFixed(2)}%', + ), _PreviewItem( label: 'Years Elapsed', value: preview!.yearsElapsed.toString(), diff --git a/lib/modules/assets/presentation/screens/asset_list_screen.dart b/lib/modules/assets/presentation/screens/asset_list_screen.dart index fb459e5..c525802 100644 --- a/lib/modules/assets/presentation/screens/asset_list_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_list_screen.dart @@ -427,6 +427,28 @@ class _AssetDataTable extends StatelessWidget { searchText: (asset) => asset.locationName ?? '', cellBuilder: (_, asset) => Text(asset.locationName ?? '—'), ), + AppDataColumn( + label: 'Current Value', + flex: 1, + searchText: (asset) => + CurrencyFormatter.searchable(asset.resolvedCurrentValue), + cellBuilder: (_, asset) => Text( + asset.resolvedCurrentValue != null + ? CurrencyFormatter.format(asset.resolvedCurrentValue) + : '—', + ), + ), + AppDataColumn( + label: 'Depreciated Amount', + flex: 1, + searchText: (asset) => + CurrencyFormatter.searchable(asset.depreciatedAmount), + cellBuilder: (_, asset) => Text( + asset.depreciatedAmount != null + ? CurrencyFormatter.format(asset.depreciatedAmount) + : '—', + ), + ), AppDataColumn( label: 'Warranty Validity', flex: 1, diff --git a/lib/modules/assets/presentation/screens/asset_maintenance_screen.dart b/lib/modules/assets/presentation/screens/asset_maintenance_screen.dart index 9dc8def..b946205 100644 --- a/lib/modules/assets/presentation/screens/asset_maintenance_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_maintenance_screen.dart @@ -18,6 +18,7 @@ import '../../../../shared/widgets/app_toast.dart'; import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../providers/assets_provider.dart'; +import '../utils/maintenance_due_display.dart'; import '../widgets/asset_maintenance_panel.dart'; class AssetMaintenanceScreen extends ConsumerStatefulWidget { @@ -351,16 +352,10 @@ class _DueBadge extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final color = isDue - ? const Color(0xFFDC2626) - : const Color(0xFF16A34A); - final label = isDue - ? (daysUntilDue != null && daysUntilDue! < 0 - ? 'Overdue' - : 'Due') - : (daysUntilDue != null - ? 'In $daysUntilDue day${daysUntilDue == 1 ? '' : 's'}' - : 'On track'); + final due = MaintenanceDueDisplay.fromDaysUntilDue(daysUntilDue); + final color = due?.color ?? + (isDue ? const Color(0xFFDC2626) : const Color(0xFF16A34A)); + final label = due?.label ?? (isDue ? 'Due' : 'On track'); return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), diff --git a/lib/modules/assets/presentation/utils/maintenance_due_display.dart b/lib/modules/assets/presentation/utils/maintenance_due_display.dart new file mode 100644 index 0000000..413b8a0 --- /dev/null +++ b/lib/modules/assets/presentation/utils/maintenance_due_display.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; + +/// Display copy + color for maintenance `days_until_due` from the API. +class MaintenanceDueDisplay { + const MaintenanceDueDisplay({ + required this.label, + required this.color, + }); + + final String label; + final Color color; + + /// Green/neutral upcoming, amber due today, red overdue. + static MaintenanceDueDisplay? fromDaysUntilDue(int? daysUntilDue) { + if (daysUntilDue == null) return null; + + if (daysUntilDue > 0) { + return MaintenanceDueDisplay( + label: + 'Due in $daysUntilDue day${daysUntilDue == 1 ? '' : 's'}', + color: const Color(0xFF16A34A), + ); + } + + if (daysUntilDue == 0) { + return const MaintenanceDueDisplay( + label: 'Due today', + color: Color(0xFFD97706), + ); + } + + final overdueBy = daysUntilDue.abs(); + return MaintenanceDueDisplay( + label: 'Overdue by $overdueBy day${overdueBy == 1 ? '' : 's'}', + color: const Color(0xFFDC2626), + ); + } +} diff --git a/lib/modules/assets/presentation/widgets/asset_side_panels.dart b/lib/modules/assets/presentation/widgets/asset_side_panels.dart index 473bb16..183a526 100644 --- a/lib/modules/assets/presentation/widgets/asset_side_panels.dart +++ b/lib/modules/assets/presentation/widgets/asset_side_panels.dart @@ -5,6 +5,8 @@ import '../../../../core/errors/failure.dart'; import '../../../../core/utils/formatters.dart'; import '../../../../core/utils/validators.dart'; import '../../../../shared/models/asset_model.dart'; +import '../../../../shared/models/user_management_models.dart' + show FilterOptionModel; import '../../../../shared/widgets/api_feedback.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_card.dart'; @@ -543,7 +545,7 @@ class _LogServiceVisitPanelState extends ConsumerState { _serviceCostController.text = CurrencyFormatter.formatEditable(visit.serviceCost); } - _isUnderAmc = visit.isUnderAmc; + _isUnderAmc = visit.isUnderAmc || visit.amcContractId != null; _assetConditionAfter = visit.assetConditionAfter; _remarksController.text = visit.remarks ?? ''; } @@ -570,9 +572,11 @@ class _LogServiceVisitPanelState extends ConsumerState { final serviceCost = CurrencyFormatter.tryParse(_serviceCostController.text); return { - if (_visitType != null && _visitType!.trim().isNotEmpty) 'visit_type': _visitType, + if (_visitType != null && _visitType!.trim().isNotEmpty) + 'visit_type': _visitType, 'visit_date': DateFormatter.toApiDate(_visitDate!), - if (_amcContractId != null) 'amc_contract_id': _amcContractId, + if (_isUnderAmc && _amcContractId != null) + 'amc_contract_id': _amcContractId, if (_complaintNoController.text.trim().isNotEmpty) 'complaint_no': _complaintNoController.text.trim(), if (_complaintDate != null) @@ -594,7 +598,8 @@ class _LogServiceVisitPanelState extends ConsumerState { if (downtimeHours != null) 'downtime_hours': downtimeHours, if (serviceCost != null) 'service_cost': serviceCost, 'is_under_amc': _isUnderAmc, - if (_assetConditionAfter != null && _assetConditionAfter!.trim().isNotEmpty) + if (_assetConditionAfter != null && + _assetConditionAfter!.trim().isNotEmpty) 'asset_condition_after': _assetConditionAfter, if (_remarksController.text.trim().isNotEmpty) 'remarks': _remarksController.text.trim(), @@ -627,9 +632,60 @@ class _LogServiceVisitPanelState extends ConsumerState { } } + void _setUnderAmc(bool value) { + setState(() { + _isUnderAmc = value; + if (!value) { + _amcContractId = null; + } + }); + } + + void _onAmcContractChanged( + int? contractId, + List amcContracts, + ) { + setState(() { + _amcContractId = contractId; + if (contractId == null) return; + final contract = amcContracts + .where((c) => int.tryParse(c.id) == contractId) + .firstOrNull; + if (contract?.vendorId != null) { + _vendorId = contract!.vendorId; + } + }); + } + + List> _vendorOptions({ + required List vendors, + required List amcContracts, + }) { + final all = vendors + .map( + (vendor) => AppDropdownOption( + value: int.tryParse(vendor.id) ?? 0, + label: vendor.name, + ), + ) + .where((option) => option.value != 0) + .toList(); + + if (!_isUnderAmc || _amcContractId == null) return all; + + final contract = amcContracts + .where((c) => int.tryParse(c.id) == _amcContractId) + .firstOrNull; + final contractVendorId = contract?.vendorId; + if (contractVendorId == null) return all; + + return all.where((option) => option.value == contractVendorId).toList(); + } + Widget _buildForm(List amcContracts) { final lookupsAsync = ref.watch(assetFormLookupsProvider); - final options = lookupsAsync.valueOrNull?.options ?? const AssetDropdownOptionsModel(); + final options = + lookupsAsync.valueOrNull?.options ?? const AssetDropdownOptionsModel(); if (lookupsAsync.hasValue) { final nextVisitType = resolveAssetOptionValue( @@ -666,6 +722,7 @@ class _LogServiceVisitPanelState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + // 1) Visit classification SidePanelFormRow( left: AppSearchableDropdown( label: 'Visit Type *', @@ -698,24 +755,35 @@ class _LogServiceVisitPanelState extends ConsumerState { onPicked: (date) => setState(() => _visitDate = date), ), ), - const SizedBox(height: 12), - AppSearchableDropdown( - label: 'AMC Contract', - value: _amcContractId, - searchHint: 'Search AMC contract...', - options: amcContracts - .map((contract) { - final id = int.tryParse(contract.id); - if (id == null) return null; - final label = contract.contractNo?.trim().isNotEmpty == true - ? contract.contractNo! - : 'AMC #${contract.id}'; - return AppDropdownOption(value: id, label: label); - }) - .whereType>() - .toList(), - onChanged: (v) => setState(() => _amcContractId = v), + const SizedBox(height: 8), + + // 2) Contract & Vendor + AppFormToggleField( + label: 'Under AMC', + value: _isUnderAmc, + onChanged: _setUnderAmc, ), + if (_isUnderAmc) ...[ + const SizedBox(height: 8), + AppSearchableDropdown( + label: 'AMC Contract', + value: _amcContractId, + searchHint: 'Search AMC contract...', + options: amcContracts + .map((contract) { + final id = int.tryParse(contract.id); + if (id == null) return null; + final label = + contract.contractNo?.trim().isNotEmpty == true + ? contract.contractNo! + : 'AMC #${contract.id}'; + return AppDropdownOption(value: id, label: label); + }) + .whereType>() + .toList(), + onChanged: (v) => _onAmcContractChanged(v, amcContracts), + ), + ], const SizedBox(height: 12), SidePanelFormRow( left: lookupsAsync.when( @@ -728,13 +796,10 @@ class _LogServiceVisitPanelState extends ConsumerState { label: 'Vendor', value: _vendorId, searchHint: 'Search vendor...', - options: lookups.vendors - .map((vendor) => AppDropdownOption( - value: int.tryParse(vendor.id) ?? 0, - label: vendor.name, - )) - .where((option) => option.value != 0) - .toList(), + options: _vendorOptions( + vendors: lookups.vendors, + amcContracts: amcContracts, + ), onChanged: (v) => setState(() => _vendorId = v), ), ), @@ -747,6 +812,8 @@ class _LogServiceVisitPanelState extends ConsumerState { onChanged: (v) => setState(() => _assetConditionAfter = v), ), ), + + // 3) Complaint intake SidePanelFormRow( left: AppTextField( controller: _complaintNoController, @@ -767,6 +834,8 @@ class _LogServiceVisitPanelState extends ConsumerState { maxLines: 3, ), const SizedBox(height: 12), + + // 4) Engineer & work performed SidePanelFormRow( left: AppTextField( controller: _engineerNameController, @@ -792,6 +861,8 @@ class _LogServiceVisitPanelState extends ConsumerState { maxLines: 3, ), const SizedBox(height: 12), + + // 5) Outcome & cost SidePanelFormRow( left: _SidePanelDateField( label: 'Next Service Date', @@ -804,7 +875,8 @@ class _LogServiceVisitPanelState extends ConsumerState { right: AppTextField( controller: _downtimeHoursController, label: 'Downtime Hours', - keyboardType: const TextInputType.numberWithOptions(decimal: true), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), validator: (v) => Validators.optionalPositiveDouble( v, fieldName: 'Downtime Hours', @@ -827,12 +899,6 @@ class _LogServiceVisitPanelState extends ConsumerState { label: 'Remarks', maxLines: 3, ), - const SizedBox(height: 8), - AppFormToggleField( - label: 'Under AMC', - value: _isUnderAmc, - onChanged: (value) => setState(() => _isUnderAmc = value), - ), ], ), ); diff --git a/lib/modules/audit/presentation/widgets/audit_log_detail_panel.dart b/lib/modules/audit/presentation/widgets/audit_log_detail_panel.dart index d88c85a..f7f26ff 100644 --- a/lib/modules/audit/presentation/widgets/audit_log_detail_panel.dart +++ b/lib/modules/audit/presentation/widgets/audit_log_detail_panel.dart @@ -96,6 +96,10 @@ class _DetailRow extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final labelStyle = theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ); + return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( @@ -103,22 +107,20 @@ class _DetailRow extends StatelessWidget { children: [ SizedBox( width: 140, - child: Text( - label, - style: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + child: Text(label, style: labelStyle), + ), + if (child != null) + // Keep badge left-aligned with text values (do not expand Chip). + child! + else + Expanded( + child: SelectableText( + value ?? '—', + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, + ), ), ), - ), - Expanded( - child: child ?? - SelectableText( - value ?? '—', - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - ), ], ), ); diff --git a/lib/modules/reports/domain/entities/depreciation_report.dart b/lib/modules/reports/domain/entities/depreciation_report.dart index caf9aa3..23ca69b 100644 --- a/lib/modules/reports/domain/entities/depreciation_report.dart +++ b/lib/modules/reports/domain/entities/depreciation_report.dart @@ -170,6 +170,7 @@ class DepreciationReportRow { this.usefulLifeYears, this.yearsElapsed, this.salvageValue, + this.salvagePercentage, this.annualDepreciation, this.accumulatedDepreciation, this.bookValue, @@ -190,6 +191,7 @@ class DepreciationReportRow { final int? usefulLifeYears; final double? yearsElapsed; final double? salvageValue; + final double? salvagePercentage; final double? annualDepreciation; final double? accumulatedDepreciation; final double? bookValue; @@ -300,6 +302,10 @@ class DepreciationReportRow { json['salvage_value'] ?? json['salvageValue'], ) ?? toDouble(depMap['salvage_value']), + salvagePercentage: toDouble( + json['salvage_percentage'] ?? json['salvagePercentage'], + ) ?? + toDouble(depMap['salvage_percentage']), annualDepreciation: toDouble( json['annual_depreciation'] ?? json['annualDepreciation'], ) ?? diff --git a/lib/shared/models/asset_model.dart b/lib/shared/models/asset_model.dart index 2782572..cf30c8d 100644 --- a/lib/shared/models/asset_model.dart +++ b/lib/shared/models/asset_model.dart @@ -200,6 +200,16 @@ class AssetModel with _$AssetModel { @JsonKey(name: 'depreciation_rate', fromJson: _doubleFromJsonNullable) double? depreciationRate, @JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) double? salvageValue, + @JsonKey(name: 'salvage_percentage', fromJson: _doubleFromJsonNullable) + double? salvagePercentage, + @JsonKey(name: 'current_value', fromJson: _doubleFromJsonNullable) + double? currentValue, + @JsonKey( + name: 'depreciation', + fromJson: AssetDepreciationSummary.fromJsonNullable, + toJson: AssetDepreciationSummary.toJsonNullable, + ) + AssetDepreciationSummary? depreciation, @JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable) DateTime? warrantyExpiryDate, String? condition, @@ -223,6 +233,84 @@ class AssetModel with _$AssetModel { factory AssetModel.fromJson(Map json) => _$AssetModelFromJson(json); } +extension AssetValueX on AssetModel { + /// Book / WDV — prefer top-level `current_value`. + double? get resolvedCurrentValue => + currentValue ?? depreciation?.currentValue ?? depreciation?.bookValue; + + /// Total depreciated so far — `depreciation.accumulated_depreciation`. + double? get depreciatedAmount => depreciation?.accumulatedDepreciation; +} + +class AssetDepreciationSummary { + const AssetDepreciationSummary({ + this.depreciationMethod, + this.depreciationRate, + this.annualDepreciation, + this.accumulatedDepreciation, + this.bookValue, + this.currentValue, + this.purchaseCost, + this.salvageValue, + this.salvagePercentage, + this.usefulLifeYears, + }); + + final String? depreciationMethod; + final double? depreciationRate; + final double? annualDepreciation; + final double? accumulatedDepreciation; + final double? bookValue; + final double? currentValue; + final double? purchaseCost; + final double? salvageValue; + final double? salvagePercentage; + final int? usefulLifeYears; + + static AssetDepreciationSummary? fromJsonNullable(Object? value) { + if (value is! Map) return null; + return AssetDepreciationSummary.fromJson(Map.from(value)); + } + + static Object? toJsonNullable(AssetDepreciationSummary? value) { + if (value == null) return null; + return value.toJson(); + } + + factory AssetDepreciationSummary.fromJson(Map json) { + return AssetDepreciationSummary( + depreciationMethod: json['depreciation_method']?.toString(), + depreciationRate: _doubleFromJsonNullable(json['depreciation_rate']), + annualDepreciation: _doubleFromJsonNullable(json['annual_depreciation']), + accumulatedDepreciation: + _doubleFromJsonNullable(json['accumulated_depreciation']), + bookValue: _doubleFromJsonNullable(json['book_value']), + currentValue: _doubleFromJsonNullable(json['current_value']), + purchaseCost: _doubleFromJsonNullable(json['purchase_cost']), + salvageValue: _doubleFromJsonNullable(json['salvage_value']), + salvagePercentage: _doubleFromJsonNullable(json['salvage_percentage']), + usefulLifeYears: _intFromJsonNullable(json['useful_life_years']), + ); + } + + Map toJson() => { + if (depreciationMethod != null) + 'depreciation_method': depreciationMethod, + if (depreciationRate != null) 'depreciation_rate': depreciationRate, + if (annualDepreciation != null) + 'annual_depreciation': annualDepreciation, + if (accumulatedDepreciation != null) + 'accumulated_depreciation': accumulatedDepreciation, + if (bookValue != null) 'book_value': bookValue, + if (currentValue != null) 'current_value': currentValue, + if (purchaseCost != null) 'purchase_cost': purchaseCost, + if (salvageValue != null) 'salvage_value': salvageValue, + if (salvagePercentage != null) + 'salvage_percentage': salvagePercentage, + if (usefulLifeYears != null) 'useful_life_years': usefulLifeYears, + }; +} + class AssetMaintenanceChecklistItem { const AssetMaintenanceChecklistItem({ required this.label, @@ -708,6 +796,7 @@ class AssetDepreciationPreviewModel { required this.yearsElapsed, required this.purchaseCost, required this.salvageValue, + this.salvagePercentage, required this.usefulLifeYears, }); @@ -719,6 +808,7 @@ class AssetDepreciationPreviewModel { final int yearsElapsed; final double purchaseCost; final double salvageValue; + final double? salvagePercentage; final int usefulLifeYears; factory AssetDepreciationPreviewModel.fromJson(Map json) { @@ -742,6 +832,7 @@ class AssetDepreciationPreviewModel { yearsElapsed: toInt(json['years_elapsed']), purchaseCost: toDouble(json['purchase_cost']), salvageValue: toDouble(json['salvage_value']), + salvagePercentage: _doubleFromJsonNullable(json['salvage_percentage']), usefulLifeYears: toInt(json['useful_life_years']), ); } diff --git a/lib/shared/models/asset_model.freezed.dart b/lib/shared/models/asset_model.freezed.dart index cbdbc8d..77b527d 100644 --- a/lib/shared/models/asset_model.freezed.dart +++ b/lib/shared/models/asset_model.freezed.dart @@ -498,6 +498,17 @@ mixin _$AssetModel { double? get depreciationRate => throw _privateConstructorUsedError; @JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) double? get salvageValue => throw _privateConstructorUsedError; + @JsonKey(name: 'salvage_percentage', fromJson: _doubleFromJsonNullable) + double? get salvagePercentage => throw _privateConstructorUsedError; + @JsonKey(name: 'current_value', fromJson: _doubleFromJsonNullable) + double? get currentValue => throw _privateConstructorUsedError; + @JsonKey( + name: 'depreciation', + fromJson: AssetDepreciationSummary.fromJsonNullable, + toJson: AssetDepreciationSummary.toJsonNullable, + ) + AssetDepreciationSummary? get depreciation => + throw _privateConstructorUsedError; @JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable) DateTime? get warrantyExpiryDate => throw _privateConstructorUsedError; String? get condition => throw _privateConstructorUsedError; @@ -617,6 +628,16 @@ abstract class $AssetModelCopyWith<$Res> { double? depreciationRate, @JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) double? salvageValue, + @JsonKey(name: 'salvage_percentage', fromJson: _doubleFromJsonNullable) + double? salvagePercentage, + @JsonKey(name: 'current_value', fromJson: _doubleFromJsonNullable) + double? currentValue, + @JsonKey( + name: 'depreciation', + fromJson: AssetDepreciationSummary.fromJsonNullable, + toJson: AssetDepreciationSummary.toJsonNullable, + ) + AssetDepreciationSummary? depreciation, @JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable) DateTime? warrantyExpiryDate, String? condition, @@ -689,6 +710,9 @@ class _$AssetModelCopyWithImpl<$Res, $Val extends AssetModel> Object? depreciationMethod = freezed, Object? depreciationRate = freezed, Object? salvageValue = freezed, + Object? salvagePercentage = freezed, + Object? currentValue = freezed, + Object? depreciation = freezed, Object? warrantyExpiryDate = freezed, Object? condition = freezed, Object? status = freezed, @@ -832,6 +856,18 @@ class _$AssetModelCopyWithImpl<$Res, $Val extends AssetModel> ? _value.salvageValue : salvageValue // ignore: cast_nullable_to_non_nullable as double?, + salvagePercentage: freezed == salvagePercentage + ? _value.salvagePercentage + : salvagePercentage // ignore: cast_nullable_to_non_nullable + as double?, + currentValue: freezed == currentValue + ? _value.currentValue + : currentValue // ignore: cast_nullable_to_non_nullable + as double?, + depreciation: freezed == depreciation + ? _value.depreciation + : depreciation // ignore: cast_nullable_to_non_nullable + as AssetDepreciationSummary?, warrantyExpiryDate: freezed == warrantyExpiryDate ? _value.warrantyExpiryDate : warrantyExpiryDate // ignore: cast_nullable_to_non_nullable @@ -970,6 +1006,16 @@ abstract class _$$AssetModelImplCopyWith<$Res> double? depreciationRate, @JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) double? salvageValue, + @JsonKey(name: 'salvage_percentage', fromJson: _doubleFromJsonNullable) + double? salvagePercentage, + @JsonKey(name: 'current_value', fromJson: _doubleFromJsonNullable) + double? currentValue, + @JsonKey( + name: 'depreciation', + fromJson: AssetDepreciationSummary.fromJsonNullable, + toJson: AssetDepreciationSummary.toJsonNullable, + ) + AssetDepreciationSummary? depreciation, @JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable) DateTime? warrantyExpiryDate, String? condition, @@ -1041,6 +1087,9 @@ class __$$AssetModelImplCopyWithImpl<$Res> Object? depreciationMethod = freezed, Object? depreciationRate = freezed, Object? salvageValue = freezed, + Object? salvagePercentage = freezed, + Object? currentValue = freezed, + Object? depreciation = freezed, Object? warrantyExpiryDate = freezed, Object? condition = freezed, Object? status = freezed, @@ -1184,6 +1233,18 @@ class __$$AssetModelImplCopyWithImpl<$Res> ? _value.salvageValue : salvageValue // ignore: cast_nullable_to_non_nullable as double?, + salvagePercentage: freezed == salvagePercentage + ? _value.salvagePercentage + : salvagePercentage // ignore: cast_nullable_to_non_nullable + as double?, + currentValue: freezed == currentValue + ? _value.currentValue + : currentValue // ignore: cast_nullable_to_non_nullable + as double?, + depreciation: freezed == depreciation + ? _value.depreciation + : depreciation // ignore: cast_nullable_to_non_nullable + as AssetDepreciationSummary?, warrantyExpiryDate: freezed == warrantyExpiryDate ? _value.warrantyExpiryDate : warrantyExpiryDate // ignore: cast_nullable_to_non_nullable @@ -1315,6 +1376,16 @@ class _$AssetModelImpl implements _AssetModel { this.depreciationRate, @JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) this.salvageValue, + @JsonKey(name: 'salvage_percentage', fromJson: _doubleFromJsonNullable) + this.salvagePercentage, + @JsonKey(name: 'current_value', fromJson: _doubleFromJsonNullable) + this.currentValue, + @JsonKey( + name: 'depreciation', + fromJson: AssetDepreciationSummary.fromJsonNullable, + toJson: AssetDepreciationSummary.toJsonNullable, + ) + this.depreciation, @JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable) this.warrantyExpiryDate, this.condition, @@ -1466,6 +1537,19 @@ class _$AssetModelImpl implements _AssetModel { @JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) final double? salvageValue; @override + @JsonKey(name: 'salvage_percentage', fromJson: _doubleFromJsonNullable) + final double? salvagePercentage; + @override + @JsonKey(name: 'current_value', fromJson: _doubleFromJsonNullable) + final double? currentValue; + @override + @JsonKey( + name: 'depreciation', + fromJson: AssetDepreciationSummary.fromJsonNullable, + toJson: AssetDepreciationSummary.toJsonNullable, + ) + final AssetDepreciationSummary? depreciation; + @override @JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable) final DateTime? warrantyExpiryDate; @override @@ -1505,7 +1589,7 @@ class _$AssetModelImpl implements _AssetModel { @override String toString() { - return 'AssetModel(id: $id, assetName: $assetName, assetCode: $assetCode, assetCategoryId: $assetCategoryId, assetCategoryName: $assetCategoryName, assetSubcategoryId: $assetSubcategoryId, assetSubcategoryName: $assetSubcategoryName, locationId: $locationId, locationName: $locationName, brandModel: $brandModel, manufacturer: $manufacturer, serialNumber: $serialNumber, partNumber: $partNumber, departmentId: $departmentId, departmentName: $departmentName, locationDetail: $locationDetail, assignedToUserId: $assignedToUserId, maintenanceInchargeUserId: $maintenanceInchargeUserId, maintenanceFrequencyInDays: $maintenanceFrequencyInDays, maintenanceChecklistJson: $maintenanceChecklistJson, commencementDate: $commencementDate, vendorId: $vendorId, vendorName: $vendorName, poId: $poId, grnId: $grnId, grnItemId: $grnItemId, purchaseDate: $purchaseDate, purchaseCost: $purchaseCost, usefulLifeYears: $usefulLifeYears, depreciationMethod: $depreciationMethod, depreciationRate: $depreciationRate, salvageValue: $salvageValue, warrantyExpiryDate: $warrantyExpiryDate, condition: $condition, status: $status, qrCodeValue: $qrCodeValue, disposalDate: $disposalDate, disposalReason: $disposalReason, disposalValue: $disposalValue, remarks: $remarks, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, maintenance: $maintenance)'; + return 'AssetModel(id: $id, assetName: $assetName, assetCode: $assetCode, assetCategoryId: $assetCategoryId, assetCategoryName: $assetCategoryName, assetSubcategoryId: $assetSubcategoryId, assetSubcategoryName: $assetSubcategoryName, locationId: $locationId, locationName: $locationName, brandModel: $brandModel, manufacturer: $manufacturer, serialNumber: $serialNumber, partNumber: $partNumber, departmentId: $departmentId, departmentName: $departmentName, locationDetail: $locationDetail, assignedToUserId: $assignedToUserId, maintenanceInchargeUserId: $maintenanceInchargeUserId, maintenanceFrequencyInDays: $maintenanceFrequencyInDays, maintenanceChecklistJson: $maintenanceChecklistJson, commencementDate: $commencementDate, vendorId: $vendorId, vendorName: $vendorName, poId: $poId, grnId: $grnId, grnItemId: $grnItemId, purchaseDate: $purchaseDate, purchaseCost: $purchaseCost, usefulLifeYears: $usefulLifeYears, depreciationMethod: $depreciationMethod, depreciationRate: $depreciationRate, salvageValue: $salvageValue, salvagePercentage: $salvagePercentage, currentValue: $currentValue, depreciation: $depreciation, warrantyExpiryDate: $warrantyExpiryDate, condition: $condition, status: $status, qrCodeValue: $qrCodeValue, disposalDate: $disposalDate, disposalReason: $disposalReason, disposalValue: $disposalValue, remarks: $remarks, isActive: $isActive, createdAt: $createdAt, updatedAt: $updatedAt, maintenance: $maintenance)'; } @override @@ -1583,6 +1667,12 @@ class _$AssetModelImpl implements _AssetModel { other.depreciationRate == depreciationRate) && (identical(other.salvageValue, salvageValue) || other.salvageValue == salvageValue) && + (identical(other.salvagePercentage, salvagePercentage) || + other.salvagePercentage == salvagePercentage) && + (identical(other.currentValue, currentValue) || + other.currentValue == currentValue) && + (identical(other.depreciation, depreciation) || + other.depreciation == depreciation) && (identical(other.warrantyExpiryDate, warrantyExpiryDate) || other.warrantyExpiryDate == warrantyExpiryDate) && (identical(other.condition, condition) || @@ -1643,6 +1733,9 @@ class _$AssetModelImpl implements _AssetModel { depreciationMethod, depreciationRate, salvageValue, + salvagePercentage, + currentValue, + depreciation, warrantyExpiryDate, condition, status, @@ -1748,6 +1841,16 @@ abstract class _AssetModel implements AssetModel { final double? depreciationRate, @JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) final double? salvageValue, + @JsonKey(name: 'salvage_percentage', fromJson: _doubleFromJsonNullable) + final double? salvagePercentage, + @JsonKey(name: 'current_value', fromJson: _doubleFromJsonNullable) + final double? currentValue, + @JsonKey( + name: 'depreciation', + fromJson: AssetDepreciationSummary.fromJsonNullable, + toJson: AssetDepreciationSummary.toJsonNullable, + ) + final AssetDepreciationSummary? depreciation, @JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable) final DateTime? warrantyExpiryDate, final String? condition, @@ -1890,6 +1993,19 @@ abstract class _AssetModel implements AssetModel { @JsonKey(name: 'salvage_value', fromJson: _doubleFromJsonNullable) double? get salvageValue; @override + @JsonKey(name: 'salvage_percentage', fromJson: _doubleFromJsonNullable) + double? get salvagePercentage; + @override + @JsonKey(name: 'current_value', fromJson: _doubleFromJsonNullable) + double? get currentValue; + @override + @JsonKey( + name: 'depreciation', + fromJson: AssetDepreciationSummary.fromJsonNullable, + toJson: AssetDepreciationSummary.toJsonNullable, + ) + AssetDepreciationSummary? get depreciation; + @override @JsonKey(name: 'warranty_expiry_date', fromJson: _dateFromJsonNullable) DateTime? get warrantyExpiryDate; @override diff --git a/lib/shared/models/asset_model.g.dart b/lib/shared/models/asset_model.g.dart index a659729..badfcb7 100644 --- a/lib/shared/models/asset_model.g.dart +++ b/lib/shared/models/asset_model.g.dart @@ -42,67 +42,69 @@ Map _$$AssetCategoryModelImplToJson( 'updatedAt': instance.updatedAt?.toIso8601String(), }; -_$AssetModelImpl _$$AssetModelImplFromJson(Map json) => - _$AssetModelImpl( - id: _idFromJson(json['id']), - assetName: json['asset_name'] as String, - assetCode: json['asset_code'] as String?, - assetCategoryId: _intFromJsonNullable( - _readItemCategoryId(json, 'item_category_id'), - ), - assetCategoryName: - _readItemCategoryName(json, 'item_category_name') as String?, - assetSubcategoryId: _intFromJsonNullable( - _readItemSubcategoryId(json, 'item_subcategory_id'), - ), - assetSubcategoryName: - _readItemSubcategoryName(json, 'item_subcategory_name') as String?, - locationId: _intFromJsonNullable(_readLocationId(json, 'location_id')), - locationName: _readLocationName(json, 'location_name') as String?, - brandModel: json['brand_model'] as String?, - manufacturer: json['manufacturer'] as String?, - serialNumber: json['serial_number'] as String?, - partNumber: json['part_number'] as String?, - departmentId: _intFromJsonNullable(json['department_id']), - departmentName: _readDepartmentName(json, 'department_name') as String?, - locationDetail: json['location_detail'] as String?, - assignedToUserId: _intFromJsonNullable(json['assigned_to_user_id']), - maintenanceInchargeUserId: _intFromJsonNullable( - json['maintenance_incharge_user_id'], - ), - maintenanceFrequencyInDays: _intFromJsonNullable( - json['maintenance_frequency_in_days'], - ), - maintenanceChecklistJson: _checklistFromJson( - json['maintenance_checklist_json'], - ), - commencementDate: _dateFromJsonNullable(json['commencement_date']), - vendorId: _intFromJsonNullable(json['vendor_id']), - vendorName: _readVendorNameFromNested(json, 'vendor_name') as String?, - poId: _intFromJsonNullable(json['po_id']), - grnId: _intFromJsonNullable(json['grn_id']), - grnItemId: _intFromJsonNullable(json['grn_item_id']), - purchaseDate: _dateFromJsonNullable(json['purchase_date']), - purchaseCost: _doubleFromJsonNullable(json['purchase_cost']), - usefulLifeYears: _intFromJsonNullable(json['useful_life_years']), - depreciationMethod: json['depreciation_method'] as String?, - depreciationRate: _doubleFromJsonNullable(json['depreciation_rate']), - salvageValue: _doubleFromJsonNullable(json['salvage_value']), - warrantyExpiryDate: _dateFromJsonNullable(json['warranty_expiry_date']), - condition: json['condition'] as String?, - status: json['status'] as String?, - qrCodeValue: json['qr_code_value'] as String?, - disposalDate: _dateFromJsonNullable(json['disposal_date']), - disposalReason: json['disposal_reason'] as String?, - disposalValue: _doubleFromJsonNullable(json['disposal_value']), - remarks: json['remarks'] as String?, - isActive: json['is_active'] as bool? ?? true, - createdAt: _dateFromJsonNullable(json['created_at']), - updatedAt: _dateFromJsonNullable(json['updated_at']), - maintenance: AssetMaintenanceSummary.fromJsonNullable( - json['maintenance'], - ), - ); +_$AssetModelImpl _$$AssetModelImplFromJson( + Map json, +) => _$AssetModelImpl( + id: _idFromJson(json['id']), + assetName: json['asset_name'] as String, + assetCode: json['asset_code'] as String?, + assetCategoryId: _intFromJsonNullable( + _readItemCategoryId(json, 'item_category_id'), + ), + assetCategoryName: + _readItemCategoryName(json, 'item_category_name') as String?, + assetSubcategoryId: _intFromJsonNullable( + _readItemSubcategoryId(json, 'item_subcategory_id'), + ), + assetSubcategoryName: + _readItemSubcategoryName(json, 'item_subcategory_name') as String?, + locationId: _intFromJsonNullable(_readLocationId(json, 'location_id')), + locationName: _readLocationName(json, 'location_name') as String?, + brandModel: json['brand_model'] as String?, + manufacturer: json['manufacturer'] as String?, + serialNumber: json['serial_number'] as String?, + partNumber: json['part_number'] as String?, + departmentId: _intFromJsonNullable(json['department_id']), + departmentName: _readDepartmentName(json, 'department_name') as String?, + locationDetail: json['location_detail'] as String?, + assignedToUserId: _intFromJsonNullable(json['assigned_to_user_id']), + maintenanceInchargeUserId: _intFromJsonNullable( + json['maintenance_incharge_user_id'], + ), + maintenanceFrequencyInDays: _intFromJsonNullable( + json['maintenance_frequency_in_days'], + ), + maintenanceChecklistJson: _checklistFromJson( + json['maintenance_checklist_json'], + ), + commencementDate: _dateFromJsonNullable(json['commencement_date']), + vendorId: _intFromJsonNullable(json['vendor_id']), + vendorName: _readVendorNameFromNested(json, 'vendor_name') as String?, + poId: _intFromJsonNullable(json['po_id']), + grnId: _intFromJsonNullable(json['grn_id']), + grnItemId: _intFromJsonNullable(json['grn_item_id']), + purchaseDate: _dateFromJsonNullable(json['purchase_date']), + purchaseCost: _doubleFromJsonNullable(json['purchase_cost']), + usefulLifeYears: _intFromJsonNullable(json['useful_life_years']), + depreciationMethod: json['depreciation_method'] as String?, + depreciationRate: _doubleFromJsonNullable(json['depreciation_rate']), + salvageValue: _doubleFromJsonNullable(json['salvage_value']), + salvagePercentage: _doubleFromJsonNullable(json['salvage_percentage']), + currentValue: _doubleFromJsonNullable(json['current_value']), + depreciation: AssetDepreciationSummary.fromJsonNullable(json['depreciation']), + warrantyExpiryDate: _dateFromJsonNullable(json['warranty_expiry_date']), + condition: json['condition'] as String?, + status: json['status'] as String?, + qrCodeValue: json['qr_code_value'] as String?, + disposalDate: _dateFromJsonNullable(json['disposal_date']), + disposalReason: json['disposal_reason'] as String?, + disposalValue: _doubleFromJsonNullable(json['disposal_value']), + remarks: json['remarks'] as String?, + isActive: json['is_active'] as bool? ?? true, + createdAt: _dateFromJsonNullable(json['created_at']), + updatedAt: _dateFromJsonNullable(json['updated_at']), + maintenance: AssetMaintenanceSummary.fromJsonNullable(json['maintenance']), +); Map _$$AssetModelImplToJson( _$AssetModelImpl instance, @@ -141,6 +143,11 @@ Map _$$AssetModelImplToJson( 'depreciation_method': instance.depreciationMethod, 'depreciation_rate': instance.depreciationRate, 'salvage_value': instance.salvageValue, + 'salvage_percentage': instance.salvagePercentage, + 'current_value': instance.currentValue, + 'depreciation': AssetDepreciationSummary.toJsonNullable( + instance.depreciation, + ), 'warranty_expiry_date': instance.warrantyExpiryDate?.toIso8601String(), 'condition': instance.condition, 'status': instance.status, diff --git a/lib/shared/widgets/app_sidebar.dart b/lib/shared/widgets/app_sidebar.dart index b58749f..d5da160 100644 --- a/lib/shared/widgets/app_sidebar.dart +++ b/lib/shared/widgets/app_sidebar.dart @@ -27,7 +27,7 @@ const _sidebarChildIndent = 28.0; /// Set to `true` to show the Light/Dark toggle in the sidebar again. /// Kept hidden for now — do not delete `_buildThemeToggle`. -const showSidebarThemeToggle = true; +const showSidebarThemeToggle = false; class AppSidebar extends ConsumerStatefulWidget { const AppSidebar({