From fa610b7efdab7c784db4daada36976c3aa14614e Mon Sep 17 00:00:00 2001 From: SurendarSuri30 Date: Thu, 9 Jul 2026 18:37:57 +0530 Subject: [PATCH] Bharat erp - bug fixes --- .../datasources/asset_remote_data_source.dart | 23 +- .../repositories/asset_repository_impl.dart | 2 +- .../domain/repositories/asset_repository.dart | 2 +- .../providers/assets_provider.dart | 4 +- .../screens/asset_alerts_screen.dart | 324 +++++++++++- .../screens/asset_detail_screen.dart | 41 +- .../screens/asset_list_screen.dart | 2 +- .../widgets/asset_side_panels.dart | 288 ++++++++-- .../domain/entities/master_definition.dart | 18 + .../screens/purchase_order_detail_screen.dart | 2 +- .../screens/purchase_order_list_screen.dart | 59 ++- .../screens/users_role_management_screen.dart | 4 +- .../screens/user_list_screen.dart | 2 +- .../providers/vendors_provider.dart | 13 +- .../screens/vendor_detail_screen.dart | 500 ++++++++++-------- .../screens/vendor_list_screen.dart | 2 +- .../widgets/vendor_form_panel.dart | 38 +- lib/shared/models/asset_model.dart | 21 +- lib/shared/models/asset_model.freezed.dart | 36 +- lib/shared/models/asset_model.g.dart | 4 +- lib/shared/models/purchase_order_model.dart | 12 +- lib/shared/widgets/api_feedback.dart | 10 + lib/shared/widgets/app_status_chip.dart | 26 +- lib/shared/widgets/page_header.dart | 21 +- 24 files changed, 1091 insertions(+), 363 deletions(-) diff --git a/lib/modules/assets/data/datasources/asset_remote_data_source.dart b/lib/modules/assets/data/datasources/asset_remote_data_source.dart index 1b12529..5afb122 100644 --- a/lib/modules/assets/data/datasources/asset_remote_data_source.dart +++ b/lib/modules/assets/data/datasources/asset_remote_data_source.dart @@ -40,9 +40,8 @@ class AssetRemoteDataSource { await dio.delete(ApiEndpoints.assetById(id)); } - Future transferAsset(String id, Map data) async { - final response = await dio.post(ApiEndpoints.assetTransfer(id), data: data); - return AssetModel.fromJson(response.data['data'] as Map); + Future transferAsset(String id, Map data) async { + await dio.post(ApiEndpoints.assetTransfer(id), data: data); } Future> getTransferHistory(String assetId) async { @@ -291,24 +290,30 @@ class AssetRemoteDataSource { ); } + Map _asStringMap(dynamic value) { + if (value is Map) return value; + if (value is Map) return Map.from(value); + return const {}; + } + List _parseList( dynamic body, T Function(Map) fromJson, ) { - if (body is! Map) return []; + if (body is! Map) return []; final raw = body['data']; if (raw is List) { return raw - .whereType>() - .map(fromJson) + .whereType() + .map((item) => fromJson(_asStringMap(item))) .toList(); } - if (raw is Map) { + if (raw is Map) { final items = raw['items']; if (items is List) { return items - .whereType>() - .map(fromJson) + .whereType() + .map((item) => fromJson(_asStringMap(item))) .toList(); } } diff --git a/lib/modules/assets/data/repositories/asset_repository_impl.dart b/lib/modules/assets/data/repositories/asset_repository_impl.dart index ac56110..3519759 100644 --- a/lib/modules/assets/data/repositories/asset_repository_impl.dart +++ b/lib/modules/assets/data/repositories/asset_repository_impl.dart @@ -46,7 +46,7 @@ class AssetRepositoryImpl implements AssetRepository { } @override - Future> transferAsset(String id, Map data) { + Future> transferAsset(String id, Map data) { return safeApiCall(() => dataSource.transferAsset(id, data)); } diff --git a/lib/modules/assets/domain/repositories/asset_repository.dart b/lib/modules/assets/domain/repositories/asset_repository.dart index f64b230..beec77b 100644 --- a/lib/modules/assets/domain/repositories/asset_repository.dart +++ b/lib/modules/assets/domain/repositories/asset_repository.dart @@ -8,7 +8,7 @@ abstract class AssetRepository { Future> createAsset(Map data); Future> updateAsset(String id, Map data); Future> deleteAsset(String id); - Future> transferAsset(String id, Map data); + Future> transferAsset(String id, Map data); Future>> getTransferHistory(String assetId); Future>> getCategories(); Future> getAssetOptions(); diff --git a/lib/modules/assets/presentation/providers/assets_provider.dart b/lib/modules/assets/presentation/providers/assets_provider.dart index 29b576d..f0bc08b 100644 --- a/lib/modules/assets/presentation/providers/assets_provider.dart +++ b/lib/modules/assets/presentation/providers/assets_provider.dart @@ -206,13 +206,13 @@ class AssetDetailNotifier extends FamilyAsyncNotifier return true; } - Future transferAsset(Map data) async { + Future transferAsset(Map data) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.transferAsset(arg, data); if (result.failure != null) throw result.failure!; await reload(); ref.invalidate(assetsListProvider); - return result.data; + ref.invalidate(transferHistoryProvider(arg)); } Future createAmc(Map data) async { diff --git a/lib/modules/assets/presentation/screens/asset_alerts_screen.dart b/lib/modules/assets/presentation/screens/asset_alerts_screen.dart index 773e501..12188b6 100644 --- a/lib/modules/assets/presentation/screens/asset_alerts_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_alerts_screen.dart @@ -148,6 +148,11 @@ class _ExpiryAlertsTab extends ConsumerWidget { ], ), const SizedBox(height: 16), + _AlertsOverview( + total: state.expiryAlerts.length, + label: 'Expiry alerts in selected window', + ), + const SizedBox(height: 12), Expanded( child: state.expiryAlerts.isEmpty ? const AppEmptyState( @@ -230,6 +235,11 @@ class _ServiceAlertsTab extends ConsumerWidget { ], ), const SizedBox(height: 16), + _AlertsOverview( + total: state.serviceAlerts.length, + label: 'Service reminders', + ), + const SizedBox(height: 12), Expanded( child: state.serviceAlerts.isEmpty ? const AppEmptyState( @@ -272,30 +282,304 @@ class _AlertCard extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final visual = _alertVisualStyle(alert); + final subtitleParts = [ + if (alert.assetCode?.trim().isNotEmpty == true) alert.assetCode!.trim(), + if (alert.plantName?.trim().isNotEmpty == true) alert.plantName!.trim(), + if (dateLabel != null) 'Due: $dateLabel', + ]; + return AppCard( - child: ListTile( + child: InkWell( + borderRadius: BorderRadius.circular(12), onTap: onTap, - leading: Icon( - alert.type?.toUpperCase() == 'AMC' - ? Icons.handshake_outlined - : alert.type?.toUpperCase() == 'INSURANCE' - ? Icons.shield_outlined - : Icons.notifications_outlined, + child: LayoutBuilder( + builder: (context, constraints) { + final isCompact = constraints.maxWidth < 680; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: visual.color.withValues(alpha: 0.14), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + _alertTypeIcon(alert.type), + color: visual.color, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isCompact) ...[ + Text( + alert.assetName ?? alert.title ?? 'Asset ${alert.assetId ?? ''}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 6), + Align( + alignment: Alignment.centerRight, + child: Text( + visual.description, + textAlign: TextAlign.right, + style: theme.textTheme.bodySmall?.copyWith( + color: visual.color, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ), + ] else + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + alert.assetName ?? + alert.title ?? + 'Asset ${alert.assetId ?? ''}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ), + const SizedBox(width: 10), + Flexible( + child: Text( + visual.description, + textAlign: TextAlign.right, + style: theme.textTheme.bodySmall?.copyWith( + color: visual.color, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + ), + ), + ], + ), + if (subtitleParts.isNotEmpty) ...[ + const SizedBox(height: 3), + Text( + subtitleParts.join(' · '), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 6, + children: [ + if (alert.type?.trim().isNotEmpty == true) + _AlertTag( + label: alert.type!.replaceAll('_', ' ').toUpperCase(), + color: theme.colorScheme.primary, + ), + if (alert.status?.trim().isNotEmpty == true) + _AlertTag( + label: alert.status!.replaceAll('_', ' '), + color: theme.colorScheme.secondary, + ), + ], + ), + ], + ), + ), + ], + ), + ); + }, ), - title: Text(alert.assetName ?? alert.title ?? 'Asset ${alert.assetId ?? ''}'), - subtitle: Text( - [ - if (alert.assetCode != null) alert.assetCode, - if (alert.type != null) alert.type, - if (alert.plantName != null) alert.plantName, - if (dateLabel != null) 'Due: $dateLabel', - if (alert.daysRemaining != null) '${alert.daysRemaining} days', - ].whereType().join(' · '), - ), - trailing: alert.status != null - ? Chip(label: Text(alert.status!, style: const TextStyle(fontSize: 11))) - : null, ), ); } } + +class _AlertTag extends StatelessWidget { + const _AlertTag({ + required this.label, + required this.color, + this.isEmphasized = false, + }); + + final String label; + final Color color; + final bool isEmphasized; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), + decoration: BoxDecoration( + color: color.withValues(alpha: isEmphasized ? 0.16 : 0.12), + borderRadius: BorderRadius.circular(999), + ), + child: Text( + label, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: color, + fontWeight: FontWeight.w700, + letterSpacing: 0.2, + ), + ), + ); + } +} + +class _AlertVisualStyle { + const _AlertVisualStyle({ + required this.levelLabel, + required this.description, + required this.color, + }); + + final String levelLabel; + final String description; + final Color color; +} + +_AlertVisualStyle _alertVisualStyle(AssetAlertModel alert) { + final days = alert.daysRemaining; + if (days == null) { + return const _AlertVisualStyle( + levelLabel: 'warning', + description: 'Check this asset notification.', + color: Color(0xFF2563EB), + ); + } + + if (days <= -2) { + final expiredDays = days.abs(); + return _AlertVisualStyle( + levelLabel: 'warning', + description: 'Expired $expiredDays day${expiredDays == 1 ? '' : 's'} ago', + color: const Color(0xFFDC2626), + ); + } + + if (days == -1) { + return const _AlertVisualStyle( + levelLabel: 'warning', + description: 'Expired yesterday', + color: Color(0xFFEA580C), + ); + } + + if (days == 0) { + return const _AlertVisualStyle( + levelLabel: 'warning', + description: 'Expires today. Immediate action required.', + color: Color(0xFFCA8A04), + ); + } + + return _AlertVisualStyle( + levelLabel: 'warning', + description: 'Expires in $days day${days == 1 ? '' : 's'}', + color: const Color(0xFF16A34A), + ); +} + +IconData _alertTypeIcon(String? type) { + switch (type?.toUpperCase()) { + case 'AMC': + return Icons.handshake_outlined; + case 'INSURANCE': + return Icons.shield_outlined; + case 'WARRANTY': + return Icons.verified_user_outlined; + case 'SERVICE': + return Icons.build_outlined; + default: + return Icons.notifications_outlined; + } +} + +class _AlertsOverview extends StatelessWidget { + const _AlertsOverview({ + required this.total, + required this.label, + }); + + final int total; + final String label; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.35), + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 12, + runSpacing: 8, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + '$total alerts', + style: theme.textTheme.labelLarge?.copyWith(fontWeight: FontWeight.w700), + ), + Text( + label, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const _LegendDot(color: Color(0xFFDC2626), label: 'Expired'), + const _LegendDot(color: Color(0xFFEA580C), label: 'Yesterday'), + const _LegendDot(color: Color(0xFFCA8A04), label: 'Today'), + const _LegendDot(color: Color(0xFF16A34A), label: 'Upcoming'), + ], + ), + ); + } +} + +class _LegendDot extends StatelessWidget { + const _LegendDot({ + required this.color, + required this.label, + }); + + final Color color; + final String label; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 8, + height: 8, + decoration: BoxDecoration(color: color, shape: BoxShape.circle), + ), + const SizedBox(width: 6), + Text(label, style: Theme.of(context).textTheme.labelSmall), + ], + ); + } +} diff --git a/lib/modules/assets/presentation/screens/asset_detail_screen.dart b/lib/modules/assets/presentation/screens/asset_detail_screen.dart index 6c8d1a5..a57ee96 100644 --- a/lib/modules/assets/presentation/screens/asset_detail_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_detail_screen.dart @@ -7,6 +7,7 @@ import '../../../../core/constants/enums.dart'; import '../../../../core/constants/route_constants.dart'; import '../../../../core/errors/failure.dart'; import '../../../../shared/models/asset_model.dart'; +import '../../../../shared/models/user_management_models.dart'; import '../../../../shared/providers/permissions_provider.dart'; import '../../../../shared/widgets/app_card.dart'; import '../../../../shared/widgets/app_confirmation_dialog.dart'; @@ -18,6 +19,7 @@ import '../../../../shared/widgets/error_view.dart'; import '../../../../shared/widgets/page_header.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../providers/assets_provider.dart'; +import '../providers/asset_form_lookups_provider.dart'; import '../widgets/asset_form_panel.dart'; import '../widgets/asset_side_panels.dart'; @@ -64,6 +66,11 @@ class _AssetDetailScreenState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.start, children: [ PageHeader( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + tooltip: 'Back to Asset Master', + onPressed: () => context.go(RouteConstants.assets), + ), title: state.asset.assetName, subtitle: state.asset.assetCode ?? 'Asset ID: ${state.asset.id}', actions: [ @@ -149,7 +156,7 @@ class _AssetDetailScreenState extends ConsumerState ); if (transferred == true && mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Asset transferred')), + const SnackBar(content: Text('Asset transferred successfully')), ); } } @@ -361,6 +368,9 @@ class _AmcTab extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final vendors = ref.watch(assetFormLookupsProvider).valueOrNull?.vendors ?? + const []; + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -396,6 +406,7 @@ class _AmcTab extends ConsumerWidget { itemBuilder: (context, index) { return _AssetAmcCard( contract: contracts[index], + vendors: vendors, onEdit: () => _openEditAmcPanel( context, ref, @@ -629,10 +640,12 @@ class _InsuranceTab extends ConsumerWidget { class _AssetAmcCard extends StatelessWidget { const _AssetAmcCard({ required this.contract, + required this.vendors, this.onEdit, }); final AmcContractModel contract; + final List vendors; final VoidCallback? onEdit; @override @@ -640,11 +653,7 @@ class _AssetAmcCard extends StatelessWidget { final theme = Theme.of(context); final dateFormat = DateFormat('dd MMM yyyy'); final statusColor = _assetStatusColor(contract.status, theme); - final vendor = contract.vendorName?.trim().isNotEmpty == true - ? contract.vendorName! - : contract.vendorId != null - ? 'Vendor ${contract.vendorId}' - : '—'; + final vendor = _contractVendorLabel(contract, vendors); return AppCard( elevation: 0, @@ -1066,6 +1075,26 @@ Color _assetStatusColor(String status, ThemeData theme) { } } +String _contractVendorLabel( + AmcContractModel contract, + List vendors, +) { + final direct = contract.vendorName?.trim(); + if (direct != null && direct.isNotEmpty) return direct; + + final vendorId = contract.vendorId; + if (vendorId != null) { + final id = vendorId.toString(); + for (final vendor in vendors) { + if (vendor.id == id && vendor.name.trim().isNotEmpty) { + return vendor.name.trim(); + } + } + } + + return '—'; +} + String _assetStatusLabel(String status) { if (status.isEmpty) return '—'; final normalized = status.replaceAll('_', ' ').toLowerCase(); diff --git a/lib/modules/assets/presentation/screens/asset_list_screen.dart b/lib/modules/assets/presentation/screens/asset_list_screen.dart index cec825c..edfcc82 100644 --- a/lib/modules/assets/presentation/screens/asset_list_screen.dart +++ b/lib/modules/assets/presentation/screens/asset_list_screen.dart @@ -294,7 +294,7 @@ class _AssetListScreenState extends ConsumerState { final confirmed = await showAppConfirmationDialog( context: context, title: 'Delete Asset', - message: 'Soft delete "${asset.assetName}"?', + message: 'Do you want to delete this "${asset.assetName}"?', confirmLabel: 'Delete', isDestructive: true, ); diff --git a/lib/modules/assets/presentation/widgets/asset_side_panels.dart b/lib/modules/assets/presentation/widgets/asset_side_panels.dart index cd641e7..07d311a 100644 --- a/lib/modules/assets/presentation/widgets/asset_side_panels.dart +++ b/lib/modules/assets/presentation/widgets/asset_side_panels.dart @@ -1,14 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../../core/errors/failure.dart'; import '../../../../core/utils/formatters.dart'; import '../../../../core/utils/validators.dart'; import '../../../../shared/models/asset_model.dart'; +import '../../../../shared/widgets/api_feedback.dart'; import '../../../../shared/widgets/app_button.dart'; +import '../../../../shared/widgets/app_card.dart'; import '../../../../shared/widgets/app_dropdown.dart'; +import '../../../../shared/widgets/app_empty_state.dart'; import '../../../../shared/widgets/app_searchable_dropdown.dart'; import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_text_field.dart'; +import '../../../../shared/widgets/error_view.dart'; import '../providers/asset_form_lookups_provider.dart'; import '../providers/assets_provider.dart'; @@ -1291,7 +1296,7 @@ class _TransferAssetPanelState extends ConsumerState { } } catch (e) { if (mounted) { - showSidePanelSnackBar(context, e.toString()); + showSidePanelApiError(context, e); } } finally { if (mounted) setState(() => _isSubmitting = false); @@ -1398,6 +1403,214 @@ class _TransferAssetPanelState extends ConsumerState { } } +class _TransferHistoryEntry extends StatelessWidget { + const _TransferHistoryEntry({required this.item}); + + final AssetTransferHistoryModel item; + + DateTime? _resolvedTransferDateTime() { + final transferDate = item.transferDate?.toLocal(); + final createdAt = item.createdAt?.toLocal(); + if (transferDate == null) return createdAt; + if (createdAt == null) return transferDate; + return DateTime( + transferDate.year, + transferDate.month, + transferDate.day, + createdAt.hour, + createdAt.minute, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final resolvedDateTime = _resolvedTransferDateTime(); + final transferDate = resolvedDateTime != null + ? DateFormatter.displayDateTime(resolvedDateTime) + : '—'; + final reason = item.reason?.trim(); + final showReason = reason != null && reason.isNotEmpty && reason.toLowerCase() != 'nil'; + + return AppCard( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.swap_horiz_rounded, + size: 18, + color: theme.colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + transferDate, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + ], + ), + const SizedBox(height: 16), + IntrinsicHeight( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: _TransferLocationBlock( + label: 'From', + plant: item.fromPlantName, + department: item.fromDepartmentName, + warehouse: item.fromWarehouseName, + user: item.fromUserName, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Icon( + Icons.arrow_forward_rounded, + size: 18, + color: theme.colorScheme.outline, + ), + ), + Expanded( + child: _TransferLocationBlock( + label: 'To', + plant: item.toPlantName, + department: item.toDepartmentName, + warehouse: item.toWarehouseName, + user: item.toUserName, + ), + ), + ], + ), + ), + if (showReason) ...[ + const SizedBox(height: 16), + Divider( + height: 1, + color: theme.colorScheme.outline.withValues(alpha: 0.2), + ), + const SizedBox(height: 12), + Text( + 'Reason', + style: theme.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 4), + Text( + reason, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ], + ), + ), + ); + } +} + +class _TransferLocationBlock extends StatelessWidget { + const _TransferLocationBlock({ + required this.label, + this.plant, + this.department, + this.warehouse, + this.user, + }); + + final String label; + final String? plant; + final String? department; + final String? warehouse; + final String? user; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: theme.textTheme.labelSmall?.copyWith( + fontWeight: FontWeight.w700, + letterSpacing: 0.6, + color: theme.colorScheme.onSurfaceVariant, + ), + ), + const SizedBox(height: 8), + _TransferLocationRow( + icon: Icons.factory_outlined, + value: plant, + ), + _TransferLocationRow( + icon: Icons.apartment_outlined, + value: department, + ), + _TransferLocationRow( + icon: Icons.warehouse_outlined, + value: warehouse, + ), + _TransferLocationRow( + icon: Icons.person_outline, + value: user, + ), + ], + ); + } +} + +class _TransferLocationRow extends StatelessWidget { + const _TransferLocationRow({ + required this.icon, + this.value, + }); + + final IconData icon; + final String? value; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final display = value?.trim().isNotEmpty == true ? value!.trim() : '—'; + + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon( + icon, + size: 14, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 6), + Expanded( + child: Text( + display, + style: theme.textTheme.bodySmall?.copyWith( + color: display == '—' + ? theme.colorScheme.onSurfaceVariant + : theme.colorScheme.onSurface, + ), + ), + ), + ], + ), + ); + } +} + class TransferHistoryPanel extends ConsumerWidget { const TransferHistoryPanel({super.key, required this.assetId}); @@ -1410,57 +1623,38 @@ class TransferHistoryPanel extends ConsumerWidget { title: 'Transfer History', child: historyAsync.when( loading: () => const Center(child: CircularProgressIndicator()), - error: (error, _) => Center(child: Text(error.toString())), + error: (error, _) => ErrorView( + message: error is Failure + ? error.message + : 'Unable to load transfer history', + onRetry: () => ref.invalidate(transferHistoryProvider(assetId)), + ), data: (history) { if (history.isEmpty) { - return const Center( - child: Text('No transfer history found'), + return const AppEmptyState( + title: 'No transfer history', + description: 'Transfers for this asset will appear here.', + icon: Icons.swap_horiz_outlined, ); } - return ListView.separated( - itemCount: history.length, - separatorBuilder: (_, _) => const Divider(height: 24), - itemBuilder: (context, index) { - final item = history[index]; - final transferDate = item.transferDate != null - ? DateFormatter.displayDate(item.transferDate) - : '—'; - final fromParts = [ - item.fromPlantName, - item.fromDepartmentName, - item.fromWarehouseName, - item.fromUserName, - ].whereType().where((e) => e.trim().isNotEmpty).toList(); - final toParts = [ - item.toPlantName, - item.toDepartmentName, - item.toWarehouseName, - item.toUserName, - ].whereType().where((e) => e.trim().isNotEmpty).toList(); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - transferDate, - style: Theme.of(context).textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 8), - Text('From: ${fromParts.isEmpty ? '—' : fromParts.join(' · ')}'), - const SizedBox(height: 4), - Text('To: ${toParts.isEmpty ? '—' : toParts.join(' · ')}'), - if (item.reason?.trim().isNotEmpty == true) ...[ - const SizedBox(height: 8), - Text( - 'Reason: ${item.reason!}', - style: Theme.of(context).textTheme.bodyMedium, - ), - ], - ], - ); - }, + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${history.length} transfer${history.length == 1 ? '' : 's'}', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 16), + for (var index = 0; index < history.length; index++) ...[ + if (index > 0) const SizedBox(height: 12), + _TransferHistoryEntry(item: history[index]), + ], + ], ); }, ), diff --git a/lib/modules/master_data/domain/entities/master_definition.dart b/lib/modules/master_data/domain/entities/master_definition.dart index 57f1b1b..a229d81 100644 --- a/lib/modules/master_data/domain/entities/master_definition.dart +++ b/lib/modules/master_data/domain/entities/master_definition.dart @@ -479,6 +479,24 @@ 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(); + + // Prefer explicit "_name" or nested ".name" from API payloads + // (e.g. asset_category_id -> asset_category_name / asset_category.name) + if (field.key.endsWith('_id')) { + final baseKey = field.key.substring(0, field.key.length - 3); + final explicitName = row['${baseKey}_name']; + if (explicitName != null && explicitName.toString().trim().isNotEmpty) { + return explicitName.toString().trim(); + } + final nested = row[baseKey]; + if (nested is Map) { + final nestedName = nested['name']; + if (nestedName != null && nestedName.toString().trim().isNotEmpty) { + return nestedName.toString().trim(); + } + } + } + if (field.staticOptions != null) { return value.toString().replaceAll('_', ' '); } 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 index e4e1204..a4d0e88 100644 --- a/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart +++ b/lib/modules/purchase_orders/presentation/screens/purchase_order_detail_screen.dart @@ -290,7 +290,7 @@ class _PurchaseOrderDetailScreenState final confirmed = await showAppConfirmationDialog( context: context, title: 'Delete Purchase Order', - message: 'Soft delete this purchase order?', + message: 'Do you want to delete this purchase order?', confirmLabel: 'Delete', isDestructive: true, ); 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 index 693a4bc..6ce5a61 100644 --- a/lib/modules/purchase_orders/presentation/screens/purchase_order_list_screen.dart +++ b/lib/modules/purchase_orders/presentation/screens/purchase_order_list_screen.dart @@ -36,6 +36,16 @@ class PurchaseOrderListScreen extends ConsumerStatefulWidget { class _PurchaseOrderListScreenState extends ConsumerState { final _searchController = TextEditingController(); + final Map _knownStatuses = {}; + + @override + void initState() { + super.initState(); + // Keep base options visible even before first API payload. + for (final option in poStatusOptions) { + _knownStatuses[option.$1] = option.$2; + } + } @override void dispose() { @@ -68,9 +78,11 @@ class _PurchaseOrderListScreenState extends ConsumerState ref.invalidate(purchaseOrdersListProvider), ), - data: (state) => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + data: (state) { + _rememberStatuses(state.orders); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ PageHeader( title: 'Purchase Orders', subtitle: 'Create, approve and track procurement orders', @@ -94,6 +106,7 @@ class _PurchaseOrderListScreenState extends ConsumerState> _statusFilterOptions( + List orders, + ) { + final options = _knownStatuses.entries + .map((entry) => AppDropdownOption( + value: entry.key, + label: entry.value, + )) + .toList() + ..sort((a, b) => a.label.compareTo(b.label)); + + return [ + const AppDropdownOption(value: null, label: 'All statuses'), + ...options, + ]; + } + + void _rememberStatuses(List orders) { + for (final order in orders) { + final raw = order.status.trim(); + if (raw.isEmpty) continue; + final normalized = raw.toUpperCase().replaceAll(' ', '_'); + _knownStatuses.putIfAbsent(normalized, () => poStatusLabel(raw)); + } + } } class _FiltersBar extends StatelessWidget { const _FiltersBar({ required this.searchController, required this.query, + required this.statusOptions, required this.wrapped, required this.onSearch, required this.onStatusChanged, @@ -203,6 +244,7 @@ class _FiltersBar extends StatelessWidget { final TextEditingController searchController; final PurchaseOrderListQuery query; + final List> statusOptions; final bool wrapped; final ValueChanged onSearch; final ValueChanged onStatusChanged; @@ -227,12 +269,7 @@ class _FiltersBar extends StatelessWidget { 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), - ), - ], + options: statusOptions, onChanged: onStatusChanged, ), ), 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 2eb44a1..bba8140 100644 --- a/lib/modules/rbac/presentation/screens/users_role_management_screen.dart +++ b/lib/modules/rbac/presentation/screens/users_role_management_screen.dart @@ -114,7 +114,7 @@ class _UsersRoleManagementScreenState final confirmed = await showAppConfirmationDialog( context: context, title: 'Delete role', - message: 'Delete "${role.name}"? This is a soft delete.', + message: 'Do you want to delete this "${role.name}"?', confirmLabel: 'Delete', isDestructive: true, ); @@ -637,7 +637,7 @@ class _UsersTabState extends ConsumerState<_UsersTab> { final confirmed = await showAppConfirmationDialog( context: context, title: 'Deactivate user', - message: 'Deactivate ${user.fullName}? This is a soft delete.', + message: 'Do you want to deactivate ${user.fullName}?', confirmLabel: 'Deactivate', isDestructive: true, ); diff --git a/lib/modules/users/presentation/screens/user_list_screen.dart b/lib/modules/users/presentation/screens/user_list_screen.dart index 9c1e625..c58b8e5 100644 --- a/lib/modules/users/presentation/screens/user_list_screen.dart +++ b/lib/modules/users/presentation/screens/user_list_screen.dart @@ -162,7 +162,7 @@ class _UserListScreenState extends ConsumerState { final confirmed = await showAppConfirmationDialog( context: context, title: 'Deactivate user', - message: 'Deactivate ${user.fullName}? This is a soft delete.', + message: 'Do you want to deactivate ${user.fullName}?', confirmLabel: 'Deactivate', isDestructive: true, ); diff --git a/lib/modules/vendors/presentation/providers/vendors_provider.dart b/lib/modules/vendors/presentation/providers/vendors_provider.dart index aca8113..ac9569f 100644 --- a/lib/modules/vendors/presentation/providers/vendors_provider.dart +++ b/lib/modules/vendors/presentation/providers/vendors_provider.dart @@ -320,10 +320,19 @@ class VendorFormNotifier extends FamilyAsyncNotifier { Future submitUpdate(String id, Map data) async { final repository = ref.read(vendorRepositoryProvider); - final result = await repository.updateVendor(id, data); + final payload = Map.from(data); + final status = payload.remove('status')?.toString().trim(); + + final result = await repository.updateVendor(id, payload); if (result.failure != null) throw result.failure!; + + if (status != null && status.isNotEmpty) { + final statusResult = await repository.updateVendorStatus(id, status); + if (statusResult.failure != null) throw statusResult.failure!; + } + ref.invalidate(vendorsListProvider); ref.invalidate(vendorDetailProvider(id)); - return result.data!; + return (await repository.getVendorById(id)).data ?? result.data!; } } diff --git a/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart b/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart index 1eba3dc..db648b0 100644 --- a/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart +++ b/lib/modules/vendors/presentation/screens/vendor_detail_screen.dart @@ -61,6 +61,11 @@ class _VendorDetailScreenState extends ConsumerState crossAxisAlignment: CrossAxisAlignment.start, children: [ PageHeader( + leading: IconButton( + icon: const Icon(Icons.arrow_back), + tooltip: 'Back to Vendors', + onPressed: () => context.go(RouteConstants.vendors), + ), title: state.vendor.vendorName, subtitle: state.vendor.vendorCode ?? 'Vendor ID: ${state.vendor.id}', actions: [ @@ -181,7 +186,7 @@ class _VendorDetailScreenState extends ConsumerState final confirmed = await showAppConfirmationDialog( context: context, title: 'Delete Vendor', - message: 'Soft delete this vendor?', + message: 'Do you want to delete this vendor?', confirmLabel: 'Delete', isDestructive: true, ); @@ -837,128 +842,143 @@ class _VendorContactCard extends StatelessWidget { final statusColor = contact.isActive ? const Color(0xFF16A34A) : theme.colorScheme.onSurfaceVariant; + final isPrimary = contact.isPrimary; + final primaryColor = theme.colorScheme.primary; return AppCard( elevation: 0, enableHover: true, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: appearance.color.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - appearance.icon, - color: appearance.color, - size: 20, - ), - ), - const Spacer(), - if (canEdit) ...[ - IconButton( - icon: const Icon(Icons.edit_outlined, size: 18), - onPressed: onEdit, - visualDensity: VisualDensity.compact, - ), - IconButton( - icon: const Icon(Icons.delete_outline, size: 18), - onPressed: onDelete, - visualDensity: VisualDensity.compact, - ), - ], - ], - ), - const SizedBox(height: 8), - Text( - contact.contactName, - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: 4), - if (contact.designation?.trim().isNotEmpty == true) - Text( - contact.designation!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - if (contact.email?.trim().isNotEmpty == true) ...[ - const SizedBox(height: 8), + child: Container( + decoration: BoxDecoration( + color: isPrimary ? primaryColor.withValues(alpha: 0.05) : null, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isPrimary ? primaryColor.withValues(alpha: 0.35) : Colors.transparent, + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Row( children: [ - Icon( - Icons.email_outlined, - size: 14, - color: theme.colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 4), - Expanded( - child: Text( - contact.email!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: appearance.color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + appearance.icon, + color: appearance.color, + size: 20, ), ), + const Spacer(), + if (isPrimary) ...[ + _PrimaryBadge(color: primaryColor), + const SizedBox(width: 6), + ], + if (canEdit) ...[ + IconButton( + icon: const Icon(Icons.edit_outlined, size: 18), + onPressed: onEdit, + visualDensity: VisualDensity.compact, + ), + IconButton( + icon: const Icon(Icons.delete_outline, size: 18), + onPressed: onDelete, + visualDensity: VisualDensity.compact, + ), + ], ], ), - ], - const Spacer(), - Row( - children: [ - if (contact.phone?.trim().isNotEmpty == true) - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.phone_outlined, - size: 14, - color: theme.colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 4), - Text( - contact.phone!, + const SizedBox(height: 8), + Text( + contact.contactName, + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: 4), + if (contact.designation?.trim().isNotEmpty == true) + Text( + contact.designation!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + if (contact.email?.trim().isNotEmpty == true) ...[ + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.email_outlined, + size: 14, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + contact.email!, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), ), - ], - ), - const Spacer(), - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: statusColor, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 6), - Text( - contact.isActive ? 'Active' : 'Inactive', - style: theme.textTheme.bodySmall?.copyWith( - color: statusColor, - fontWeight: FontWeight.w600, - ), + ), + ], ), ], - ), - ], + const Spacer(), + Row( + children: [ + if (contact.phone?.trim().isNotEmpty == true) + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.phone_outlined, + size: 14, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Text( + contact.phone!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + const Spacer(), + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: statusColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + contact.isActive ? 'Active' : 'Inactive', + style: theme.textTheme.bodySmall?.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ], + ), ), ), ); @@ -992,6 +1012,8 @@ class _VendorBankDetailCard extends StatelessWidget { final statusColor = bankDetail.isActive ? const Color(0xFF16A34A) : theme.colorScheme.onSurfaceVariant; + final isPrimary = bankDetail.isPrimary; + final primaryColor = theme.colorScheme.primary; final subtitle = [ bankDetail.branch, bankDetail.accountHolderName, @@ -1000,131 +1022,169 @@ class _VendorBankDetailCard extends StatelessWidget { return AppCard( elevation: 0, enableHover: true, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - width: 36, - height: 36, - decoration: BoxDecoration( - color: appearance.color.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(8), - ), - child: Icon( - appearance.icon, - color: appearance.color, - size: 20, - ), - ), - const Spacer(), - if (canEdit) ...[ - IconButton( - icon: const Icon(Icons.edit_outlined, size: 18), - onPressed: onEdit, - visualDensity: VisualDensity.compact, - ), - IconButton( - icon: const Icon(Icons.delete_outline, size: 18), - onPressed: onDelete, - visualDensity: VisualDensity.compact, - ), - ], - ], - ), - const SizedBox(height: 8), - Text( - bankDetail.bankName?.trim().isNotEmpty == true - ? bankDetail.bankName! - : 'Bank Account', - style: theme.textTheme.titleSmall?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - if (subtitle.isNotEmpty) ...[ - const SizedBox(height: 4), - Text( - subtitle, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - if (bankDetail.ifsc?.trim().isNotEmpty == true) ...[ - const SizedBox(height: 8), + child: Container( + decoration: BoxDecoration( + color: isPrimary ? primaryColor.withValues(alpha: 0.05) : null, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isPrimary ? primaryColor.withValues(alpha: 0.35) : Colors.transparent, + ), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Row( children: [ - Icon( - Icons.tag_outlined, - size: 14, + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: appearance.color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + appearance.icon, + color: appearance.color, + size: 20, + ), + ), + const Spacer(), + if (isPrimary) ...[ + _PrimaryBadge(color: primaryColor), + const SizedBox(width: 6), + ], + if (canEdit) ...[ + IconButton( + icon: const Icon(Icons.edit_outlined, size: 18), + onPressed: onEdit, + visualDensity: VisualDensity.compact, + ), + IconButton( + icon: const Icon(Icons.delete_outline, size: 18), + onPressed: onDelete, + visualDensity: VisualDensity.compact, + ), + ], + ], + ), + const SizedBox(height: 8), + Text( + bankDetail.bankName?.trim().isNotEmpty == true + ? bankDetail.bankName! + : 'Bank Account', + style: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + if (subtitle.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + subtitle, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( color: theme.colorScheme.onSurfaceVariant, ), - const SizedBox(width: 4), - Expanded( - child: Text( - bankDetail.ifsc!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, + ), + ], + if (bankDetail.ifsc?.trim().isNotEmpty == true) ...[ + const SizedBox(height: 8), + Row( + children: [ + Icon( + Icons.tag_outlined, + size: 14, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Expanded( + child: Text( + bankDetail.ifsc!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), ), ), + ], + ), + ], + const Spacer(), + Row( + children: [ + if (bankDetail.accountNumber?.trim().isNotEmpty == true) + Flexible( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.numbers_outlined, + size: 14, + color: theme.colorScheme.onSurfaceVariant, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + 'A/C: ${bankDetail.accountNumber!}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + ], + ), + ), + const Spacer(), + Container( + width: 6, + height: 6, + decoration: BoxDecoration( + color: statusColor, + shape: BoxShape.circle, + ), + ), + const SizedBox(width: 6), + Text( + bankDetail.isActive ? 'Active' : 'Inactive', + style: theme.textTheme.bodySmall?.copyWith( + color: statusColor, + fontWeight: FontWeight.w600, + ), ), ], ), ], - const Spacer(), - Row( - children: [ - if (bankDetail.accountNumber?.trim().isNotEmpty == true) - Flexible( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.numbers_outlined, - size: 14, - color: theme.colorScheme.onSurfaceVariant, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - 'A/C: ${bankDetail.accountNumber!}', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ), - ], - ), - ), - const Spacer(), - Container( - width: 6, - height: 6, - decoration: BoxDecoration( - color: statusColor, - shape: BoxShape.circle, - ), - ), - const SizedBox(width: 6), - Text( - bankDetail.isActive ? 'Active' : 'Inactive', - style: theme.textTheme.bodySmall?.copyWith( - color: statusColor, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ], + ), + ), + ), + ); + } +} + +class _PrimaryBadge extends StatelessWidget { + const _PrimaryBadge({required this.color}); + + final Color color; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(999), + border: Border.all(color: color.withValues(alpha: 0.3)), + ), + child: Text( + 'Primary', + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: color, + fontWeight: FontWeight.w700, ), ), ); diff --git a/lib/modules/vendors/presentation/screens/vendor_list_screen.dart b/lib/modules/vendors/presentation/screens/vendor_list_screen.dart index 9dcfcc2..4964810 100644 --- a/lib/modules/vendors/presentation/screens/vendor_list_screen.dart +++ b/lib/modules/vendors/presentation/screens/vendor_list_screen.dart @@ -163,7 +163,7 @@ class _VendorListScreenState extends ConsumerState { final confirmed = await showAppConfirmationDialog( context: context, title: 'Delete Vendor', - message: 'Soft delete "${vendor.vendorName}"?', + message: 'Do you want to delete this "${vendor.vendorName}"?', confirmLabel: 'Delete', isDestructive: true, ); diff --git a/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart b/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart index cfc9fd6..833fe41 100644 --- a/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart +++ b/lib/modules/vendors/presentation/widgets/vendor_form_panel.dart @@ -63,6 +63,7 @@ class _VendorFormPanelState extends ConsumerState { String? _sourceOfSupply; int? _paymentTermId; bool _isActive = true; + String? _currentStatus; bool _isSubmitting = false; String? _populatedSignature; @@ -79,9 +80,10 @@ class _VendorFormPanelState extends ConsumerState { String _vendorSignature(VendorModel vendor) => '${vendor.id}:${vendor.vendorType}:${vendor.gstTreatment}:' '${vendor.sourceOfSupply}:${vendor.paymentTermId}:' - '${vendor.creditPeriodDays}:${vendor.isActive}:${vendor.vendorName}'; + '${vendor.creditPeriodDays}:${vendor.status}:${vendor.vendorName}'; void _populateFromVendor(VendorModel vendor) { + final normalizedStatus = (vendor.status ?? '').trim().toLowerCase(); setState(() { _nameController.text = vendor.vendorName; _vendorType = vendor.vendorType; @@ -93,10 +95,23 @@ class _VendorFormPanelState extends ConsumerState { _creditDaysController.text = vendor.creditPeriodDays?.toString() ?? ''; _remarksController.text = vendor.remarks ?? ''; - _isActive = vendor.isActive; + _currentStatus = normalizedStatus; + if (normalizedStatus == 'active' || normalizedStatus == 'inactive') { + _isActive = normalizedStatus == 'active'; + } else { + _isActive = vendor.isActive; + } }); } + bool get _showActiveSwitch { + if (!widget.isEditing) return true; + return _currentStatus == null || + _currentStatus!.isEmpty || + _currentStatus == 'active' || + _currentStatus == 'inactive'; + } + Map _buildPayload() { return { 'vendor_name': _nameController.text.trim(), @@ -112,7 +127,8 @@ class _VendorFormPanelState extends ConsumerState { 'credit_period_days': int.tryParse(_creditDaysController.text.trim()), if (_remarksController.text.trim().isNotEmpty) 'remarks': _remarksController.text.trim(), - 'is_active': _isActive, + if (!widget.isEditing || _showActiveSwitch) + 'status': _isActive ? 'active' : 'inactive', }; } @@ -309,13 +325,15 @@ class _VendorFormPanelState extends ConsumerState { label: 'Remarks', maxLines: 3, ), - const SizedBox(height: 12), - SwitchListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Active'), - value: _isActive, - onChanged: (v) => setState(() => _isActive = v), - ), + if (_showActiveSwitch) ...[ + const SizedBox(height: 12), + SwitchListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Active'), + value: _isActive, + onChanged: (v) => setState(() => _isActive = v), + ), + ], ], ), ); diff --git a/lib/shared/models/asset_model.dart b/lib/shared/models/asset_model.dart index f3db6e3..a80ee9a 100644 --- a/lib/shared/models/asset_model.dart +++ b/lib/shared/models/asset_model.dart @@ -93,7 +93,12 @@ Object? _readWarehouseName(Map json, String key) { Object? _readVendorNameFromNested(Map json, String key) { final flat = json['vendor_name']; if (flat is String && flat.isNotEmpty) return flat; - return _readNestedName(json, 'vendor'); + final nested = json['vendor']; + if (nested is Map) { + final name = nested['vendor_name'] ?? nested['name']; + if (name is String && name.isNotEmpty) return name; + } + return null; } @freezed @@ -293,7 +298,8 @@ class AmcContractModel with _$AmcContractModel { @JsonKey(fromJson: _idFromJson) required String id, @JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, - @JsonKey(name: 'vendor_name') String? vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + String? vendorName, @JsonKey(name: 'contract_no') String? contractNo, @JsonKey(name: 'contract_type') String? contractType, @JsonKey(name: 'start_date', fromJson: _dateFromJson) required DateTime startDate, @@ -334,7 +340,8 @@ class ServiceVisitModel with _$ServiceVisitModel { @JsonKey(name: 'engineer_name') String? engineerName, @JsonKey(name: 'engineer_phone') String? engineerPhone, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, - @JsonKey(name: 'vendor_name') String? vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + String? vendorName, @JsonKey(name: 'work_done') String? workDone, @JsonKey(name: 'parts_replaced') String? partsReplaced, @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @@ -445,9 +452,11 @@ class AssetTransferHistoryModel { String? readNestedName(String key) { final nested = json[key]; - if (nested is Map) { - final name = nested['name']; - if (name is String && name.trim().isNotEmpty) return name.trim(); + if (nested is Map) { + for (final field in ['name', 'full_name']) { + final value = nested[field]; + if (value is String && value.trim().isNotEmpty) return value.trim(); + } } return null; } diff --git a/lib/shared/models/asset_model.freezed.dart b/lib/shared/models/asset_model.freezed.dart index dad0362..0bc15ed 100644 --- a/lib/shared/models/asset_model.freezed.dart +++ b/lib/shared/models/asset_model.freezed.dart @@ -1734,7 +1734,7 @@ mixin _$AmcContractModel { String? get assetId => throw _privateConstructorUsedError; @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? get vendorId => throw _privateConstructorUsedError; - @JsonKey(name: 'vendor_name') + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) String? get vendorName => throw _privateConstructorUsedError; @JsonKey(name: 'contract_no') String? get contractNo => throw _privateConstructorUsedError; @@ -1791,7 +1791,8 @@ abstract class $AmcContractModelCopyWith<$Res> { @JsonKey(fromJson: _idFromJson) String id, @JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, - @JsonKey(name: 'vendor_name') String? vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + String? vendorName, @JsonKey(name: 'contract_no') String? contractNo, @JsonKey(name: 'contract_type') String? contractType, @JsonKey(name: 'start_date', fromJson: _dateFromJson) DateTime startDate, @@ -1964,7 +1965,8 @@ abstract class _$$AmcContractModelImplCopyWith<$Res> @JsonKey(fromJson: _idFromJson) String id, @JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, - @JsonKey(name: 'vendor_name') String? vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + String? vendorName, @JsonKey(name: 'contract_no') String? contractNo, @JsonKey(name: 'contract_type') String? contractType, @JsonKey(name: 'start_date', fromJson: _dateFromJson) DateTime startDate, @@ -2129,7 +2131,8 @@ class _$AmcContractModelImpl implements _AmcContractModel { @JsonKey(fromJson: _idFromJson) required this.id, @JsonKey(name: 'asset_id', fromJson: _idFromJson) this.assetId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId, - @JsonKey(name: 'vendor_name') this.vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + this.vendorName, @JsonKey(name: 'contract_no') this.contractNo, @JsonKey(name: 'contract_type') this.contractType, @JsonKey(name: 'start_date', fromJson: _dateFromJson) @@ -2168,7 +2171,7 @@ class _$AmcContractModelImpl implements _AmcContractModel { @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) final int? vendorId; @override - @JsonKey(name: 'vendor_name') + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) final String? vendorName; @override @JsonKey(name: 'contract_no') @@ -2325,7 +2328,8 @@ abstract class _AmcContractModel implements AmcContractModel { @JsonKey(name: 'asset_id', fromJson: _idFromJson) final String? assetId, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) final int? vendorId, - @JsonKey(name: 'vendor_name') final String? vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + final String? vendorName, @JsonKey(name: 'contract_no') final String? contractNo, @JsonKey(name: 'contract_type') final String? contractType, @JsonKey(name: 'start_date', fromJson: _dateFromJson) @@ -2365,7 +2369,7 @@ abstract class _AmcContractModel implements AmcContractModel { @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? get vendorId; @override - @JsonKey(name: 'vendor_name') + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) String? get vendorName; @override @JsonKey(name: 'contract_no') @@ -2457,7 +2461,7 @@ mixin _$ServiceVisitModel { String? get engineerPhone => throw _privateConstructorUsedError; @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? get vendorId => throw _privateConstructorUsedError; - @JsonKey(name: 'vendor_name') + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) String? get vendorName => throw _privateConstructorUsedError; @JsonKey(name: 'work_done') String? get workDone => throw _privateConstructorUsedError; @@ -2511,7 +2515,8 @@ abstract class $ServiceVisitModelCopyWith<$Res> { @JsonKey(name: 'engineer_name') String? engineerName, @JsonKey(name: 'engineer_phone') String? engineerPhone, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, - @JsonKey(name: 'vendor_name') String? vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + String? vendorName, @JsonKey(name: 'work_done') String? workDone, @JsonKey(name: 'parts_replaced') String? partsReplaced, @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @@ -2693,7 +2698,8 @@ abstract class _$$ServiceVisitModelImplCopyWith<$Res> @JsonKey(name: 'engineer_name') String? engineerName, @JsonKey(name: 'engineer_phone') String? engineerPhone, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, - @JsonKey(name: 'vendor_name') String? vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + String? vendorName, @JsonKey(name: 'work_done') String? workDone, @JsonKey(name: 'parts_replaced') String? partsReplaced, @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @@ -2868,7 +2874,8 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel { @JsonKey(name: 'engineer_name') this.engineerName, @JsonKey(name: 'engineer_phone') this.engineerPhone, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId, - @JsonKey(name: 'vendor_name') this.vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + this.vendorName, @JsonKey(name: 'work_done') this.workDone, @JsonKey(name: 'parts_replaced') this.partsReplaced, @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @@ -2925,7 +2932,7 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel { @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) final int? vendorId; @override - @JsonKey(name: 'vendor_name') + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) final String? vendorName; @override @JsonKey(name: 'work_done') @@ -3076,7 +3083,8 @@ abstract class _ServiceVisitModel implements ServiceVisitModel { @JsonKey(name: 'engineer_phone') final String? engineerPhone, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) final int? vendorId, - @JsonKey(name: 'vendor_name') final String? vendorName, + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) + final String? vendorName, @JsonKey(name: 'work_done') final String? workDone, @JsonKey(name: 'parts_replaced') final String? partsReplaced, @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @@ -3133,7 +3141,7 @@ abstract class _ServiceVisitModel implements ServiceVisitModel { @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? get vendorId; @override - @JsonKey(name: 'vendor_name') + @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested) String? get vendorName; @override @JsonKey(name: 'work_done') diff --git a/lib/shared/models/asset_model.g.dart b/lib/shared/models/asset_model.g.dart index 4991cab..2649095 100644 --- a/lib/shared/models/asset_model.g.dart +++ b/lib/shared/models/asset_model.g.dart @@ -142,7 +142,7 @@ _$AmcContractModelImpl _$$AmcContractModelImplFromJson( id: _idFromJson(json['id']), assetId: _idFromJson(json['asset_id']), vendorId: _intFromJsonNullable(json['vendor_id']), - vendorName: json['vendor_name'] as String?, + vendorName: _readVendorNameFromNested(json, 'vendor_name') as String?, contractNo: json['contract_no'] as String?, contractType: json['contract_type'] as String?, startDate: _dateFromJson(json['start_date']), @@ -205,7 +205,7 @@ _$ServiceVisitModelImpl _$$ServiceVisitModelImplFromJson( engineerName: json['engineer_name'] as String?, engineerPhone: json['engineer_phone'] as String?, vendorId: _intFromJsonNullable(json['vendor_id']), - vendorName: json['vendor_name'] as String?, + vendorName: _readVendorNameFromNested(json, 'vendor_name') as String?, workDone: json['work_done'] as String?, partsReplaced: json['parts_replaced'] as String?, nextServiceDate: _dateFromJsonNullable(json['next_service_date']), diff --git a/lib/shared/models/purchase_order_model.dart b/lib/shared/models/purchase_order_model.dart index 6247aa2..8c2b5a4 100644 --- a/lib/shared/models/purchase_order_model.dart +++ b/lib/shared/models/purchase_order_model.dart @@ -211,7 +211,7 @@ const poTypeOptions = [ const poStatusOptions = [ ('DRAFT', 'Draft'), - ('SUBMITTED', 'Submitted'), + // ('SUBMITTED', 'Submitted'), ('PENDING_APPROVAL', 'Pending Approval'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), @@ -231,9 +231,15 @@ String poTypeLabel(String? value) { String poStatusLabel(String? value) { if (value == null) return '—'; + final normalizedKey = value.trim().toUpperCase().replaceAll(' ', '_'); return poStatusOptions - .where((e) => e.$1 == value) + .where((e) => e.$1 == normalizedKey) .map((e) => e.$2) .firstOrNull ?? - value.replaceAll('_', ' '); + normalizedKey + .toLowerCase() + .split('_') + .where((word) => word.isNotEmpty) + .map((word) => '${word[0].toUpperCase()}${word.substring(1)}') + .join(' '); } diff --git a/lib/shared/widgets/api_feedback.dart b/lib/shared/widgets/api_feedback.dart index 87b7c5b..c699319 100644 --- a/lib/shared/widgets/api_feedback.dart +++ b/lib/shared/widgets/api_feedback.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import '../../core/errors/failure.dart'; import '../../core/network/api_handler.dart'; +import 'app_side_panel.dart'; void showAccessDeniedSnackBar(BuildContext context, {String? message}) { ScaffoldMessenger.of(context).showSnackBar( @@ -21,6 +22,15 @@ void showAccessDeniedSnackBar(BuildContext context, {String? message}) { ); } +void showSidePanelApiError(BuildContext context, Object error) { + final message = error is Failure + ? (error is ValidationFailure + ? validationErrorMessage(error) + : error.message) + : 'Something went wrong. Please try again.'; + showSidePanelSnackBar(context, message); +} + void showApiFailureSnackBar(BuildContext context, Failure failure) { if (isForbiddenFailure(failure)) { showAccessDeniedSnackBar(context, message: failure.message); diff --git a/lib/shared/widgets/app_status_chip.dart b/lib/shared/widgets/app_status_chip.dart index 0ec085a..e3fc061 100644 --- a/lib/shared/widgets/app_status_chip.dart +++ b/lib/shared/widgets/app_status_chip.dart @@ -32,15 +32,37 @@ class AppStatusChip extends StatelessWidget { } (Color, String) _resolveStatus(String raw) { - switch (raw.toLowerCase()) { + final normalized = raw.trim().toLowerCase().replaceAll(' ', '_'); + switch (normalized) { case 'active': return (Colors.green.shade700, EntityStatus.active.label); case 'inactive': return (Colors.grey.shade700, EntityStatus.inactive.label); case 'locked': return (Colors.orange.shade800, 'Locked'); + case 'in_use': + return (const Color(0xFF16A34A), 'In Use'); + case 'under_maintenance': + return (const Color(0xFFCA8A04), 'Under Maintenance'); + case 'available': + return (const Color(0xFF2563EB), 'Available'); + case 'disposed': + case 'scrapped': + return (Colors.grey.shade700, _humanizeStatus(raw)); default: - return (Colors.blueGrey, raw); + return (Colors.blueGrey, _humanizeStatus(raw)); } } + + String _humanizeStatus(String value) { + final normalized = value.trim().replaceAll('_', ' '); + if (normalized.isEmpty) return value; + return normalized + .split(RegExp(r'\s+')) + .map((word) { + final lower = word.toLowerCase(); + return '${lower[0].toUpperCase()}${lower.substring(1)}'; + }) + .join(' '); + } } diff --git a/lib/shared/widgets/page_header.dart b/lib/shared/widgets/page_header.dart index 6e521e9..31ecde6 100644 --- a/lib/shared/widgets/page_header.dart +++ b/lib/shared/widgets/page_header.dart @@ -8,11 +8,13 @@ class PageHeader extends StatelessWidget { required this.title, this.subtitle, this.actions, + this.leading, }); final String title; final String? subtitle; final List? actions; + final Widget? leading; @override Widget build(BuildContext context) { @@ -22,7 +24,20 @@ class PageHeader extends StatelessWidget { ? Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(title, style: Theme.of(context).textTheme.headlineSmall), + Row( + children: [ + if (leading != null) ...[ + leading!, + const SizedBox(width: 8), + ], + Expanded( + child: Text( + title, + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + ], + ), if (subtitle != null) ...[ const SizedBox(height: 4), Text( @@ -40,6 +55,10 @@ class PageHeader extends StatelessWidget { ) : Row( children: [ + if (leading != null) ...[ + leading!, + const SizedBox(width: 8), + ], Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start,