Bharat erp - bug fixes

This commit is contained in:
Surendiran 2026-07-09 18:37:57 +05:30
parent ad1c95d85d
commit fa610b7efd
24 changed files with 1091 additions and 363 deletions

View File

@ -40,9 +40,8 @@ class AssetRemoteDataSource {
await dio.delete(ApiEndpoints.assetById(id)); await dio.delete(ApiEndpoints.assetById(id));
} }
Future<AssetModel> transferAsset(String id, Map<String, dynamic> data) async { Future<void> transferAsset(String id, Map<String, dynamic> data) async {
final response = await dio.post(ApiEndpoints.assetTransfer(id), data: data); await dio.post(ApiEndpoints.assetTransfer(id), data: data);
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
} }
Future<List<AssetTransferHistoryModel>> getTransferHistory(String assetId) async { Future<List<AssetTransferHistoryModel>> getTransferHistory(String assetId) async {
@ -291,24 +290,30 @@ class AssetRemoteDataSource {
); );
} }
Map<String, dynamic> _asStringMap(dynamic value) {
if (value is Map<String, dynamic>) return value;
if (value is Map) return Map<String, dynamic>.from(value);
return const {};
}
List<T> _parseList<T>( List<T> _parseList<T>(
dynamic body, dynamic body,
T Function(Map<String, dynamic>) fromJson, T Function(Map<String, dynamic>) fromJson,
) { ) {
if (body is! Map<String, dynamic>) return []; if (body is! Map) return [];
final raw = body['data']; final raw = body['data'];
if (raw is List) { if (raw is List) {
return raw return raw
.whereType<Map<String, dynamic>>() .whereType<Map>()
.map(fromJson) .map((item) => fromJson(_asStringMap(item)))
.toList(); .toList();
} }
if (raw is Map<String, dynamic>) { if (raw is Map) {
final items = raw['items']; final items = raw['items'];
if (items is List) { if (items is List) {
return items return items
.whereType<Map<String, dynamic>>() .whereType<Map>()
.map(fromJson) .map((item) => fromJson(_asStringMap(item)))
.toList(); .toList();
} }
} }

View File

@ -46,7 +46,7 @@ class AssetRepositoryImpl implements AssetRepository {
} }
@override @override
Future<Result<AssetModel>> transferAsset(String id, Map<String, dynamic> data) { Future<Result<void>> transferAsset(String id, Map<String, dynamic> data) {
return safeApiCall(() => dataSource.transferAsset(id, data)); return safeApiCall(() => dataSource.transferAsset(id, data));
} }

View File

@ -8,7 +8,7 @@ abstract class AssetRepository {
Future<Result<AssetModel>> createAsset(Map<String, dynamic> data); Future<Result<AssetModel>> createAsset(Map<String, dynamic> data);
Future<Result<AssetModel>> updateAsset(String id, Map<String, dynamic> data); Future<Result<AssetModel>> updateAsset(String id, Map<String, dynamic> data);
Future<Result<void>> deleteAsset(String id); Future<Result<void>> deleteAsset(String id);
Future<Result<AssetModel>> transferAsset(String id, Map<String, dynamic> data); Future<Result<void>> transferAsset(String id, Map<String, dynamic> data);
Future<Result<List<AssetTransferHistoryModel>>> getTransferHistory(String assetId); Future<Result<List<AssetTransferHistoryModel>>> getTransferHistory(String assetId);
Future<Result<List<AssetCategoryModel>>> getCategories(); Future<Result<List<AssetCategoryModel>>> getCategories();
Future<Result<AssetDropdownOptionsModel>> getAssetOptions(); Future<Result<AssetDropdownOptionsModel>> getAssetOptions();

View File

@ -206,13 +206,13 @@ class AssetDetailNotifier extends FamilyAsyncNotifier<AssetDetailState, String>
return true; return true;
} }
Future<AssetModel?> transferAsset(Map<String, dynamic> data) async { Future<void> transferAsset(Map<String, dynamic> data) async {
final repository = ref.read(assetRepositoryProvider); final repository = ref.read(assetRepositoryProvider);
final result = await repository.transferAsset(arg, data); final result = await repository.transferAsset(arg, data);
if (result.failure != null) throw result.failure!; if (result.failure != null) throw result.failure!;
await reload(); await reload();
ref.invalidate(assetsListProvider); ref.invalidate(assetsListProvider);
return result.data; ref.invalidate(transferHistoryProvider(arg));
} }
Future<AmcContractModel?> createAmc(Map<String, dynamic> data) async { Future<AmcContractModel?> createAmc(Map<String, dynamic> data) async {

View File

@ -148,6 +148,11 @@ class _ExpiryAlertsTab extends ConsumerWidget {
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_AlertsOverview(
total: state.expiryAlerts.length,
label: 'Expiry alerts in selected window',
),
const SizedBox(height: 12),
Expanded( Expanded(
child: state.expiryAlerts.isEmpty child: state.expiryAlerts.isEmpty
? const AppEmptyState( ? const AppEmptyState(
@ -230,6 +235,11 @@ class _ServiceAlertsTab extends ConsumerWidget {
], ],
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
_AlertsOverview(
total: state.serviceAlerts.length,
label: 'Service reminders',
),
const SizedBox(height: 12),
Expanded( Expanded(
child: state.serviceAlerts.isEmpty child: state.serviceAlerts.isEmpty
? const AppEmptyState( ? const AppEmptyState(
@ -272,30 +282,304 @@ class _AlertCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { 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( return AppCard(
child: ListTile( child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: onTap, onTap: onTap,
leading: Icon( child: LayoutBuilder(
alert.type?.toUpperCase() == 'AMC' builder: (context, constraints) {
? Icons.handshake_outlined final isCompact = constraints.maxWidth < 680;
: alert.type?.toUpperCase() == 'INSURANCE'
? Icons.shield_outlined return Padding(
: Icons.notifications_outlined, 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<String>().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),
],
);
}
}

View File

@ -7,6 +7,7 @@ import '../../../../core/constants/enums.dart';
import '../../../../core/constants/route_constants.dart'; import '../../../../core/constants/route_constants.dart';
import '../../../../core/errors/failure.dart'; import '../../../../core/errors/failure.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/models/user_management_models.dart';
import '../../../../shared/providers/permissions_provider.dart'; import '../../../../shared/providers/permissions_provider.dart';
import '../../../../shared/widgets/app_card.dart'; import '../../../../shared/widgets/app_card.dart';
import '../../../../shared/widgets/app_confirmation_dialog.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/page_header.dart';
import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_side_panel.dart';
import '../providers/assets_provider.dart'; import '../providers/assets_provider.dart';
import '../providers/asset_form_lookups_provider.dart';
import '../widgets/asset_form_panel.dart'; import '../widgets/asset_form_panel.dart';
import '../widgets/asset_side_panels.dart'; import '../widgets/asset_side_panels.dart';
@ -64,6 +66,11 @@ class _AssetDetailScreenState extends ConsumerState<AssetDetailScreen>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
PageHeader( PageHeader(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: 'Back to Asset Master',
onPressed: () => context.go(RouteConstants.assets),
),
title: state.asset.assetName, title: state.asset.assetName,
subtitle: state.asset.assetCode ?? 'Asset ID: ${state.asset.id}', subtitle: state.asset.assetCode ?? 'Asset ID: ${state.asset.id}',
actions: [ actions: [
@ -149,7 +156,7 @@ class _AssetDetailScreenState extends ConsumerState<AssetDetailScreen>
); );
if (transferred == true && mounted) { if (transferred == true && mounted) {
ScaffoldMessenger.of(context).showSnackBar( 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final vendors = ref.watch(assetFormLookupsProvider).valueOrNull?.vendors ??
const <FilterOptionModel>[];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -396,6 +406,7 @@ class _AmcTab extends ConsumerWidget {
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _AssetAmcCard( return _AssetAmcCard(
contract: contracts[index], contract: contracts[index],
vendors: vendors,
onEdit: () => _openEditAmcPanel( onEdit: () => _openEditAmcPanel(
context, context,
ref, ref,
@ -629,10 +640,12 @@ class _InsuranceTab extends ConsumerWidget {
class _AssetAmcCard extends StatelessWidget { class _AssetAmcCard extends StatelessWidget {
const _AssetAmcCard({ const _AssetAmcCard({
required this.contract, required this.contract,
required this.vendors,
this.onEdit, this.onEdit,
}); });
final AmcContractModel contract; final AmcContractModel contract;
final List<FilterOptionModel> vendors;
final VoidCallback? onEdit; final VoidCallback? onEdit;
@override @override
@ -640,11 +653,7 @@ class _AssetAmcCard extends StatelessWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final dateFormat = DateFormat('dd MMM yyyy'); final dateFormat = DateFormat('dd MMM yyyy');
final statusColor = _assetStatusColor(contract.status, theme); final statusColor = _assetStatusColor(contract.status, theme);
final vendor = contract.vendorName?.trim().isNotEmpty == true final vendor = _contractVendorLabel(contract, vendors);
? contract.vendorName!
: contract.vendorId != null
? 'Vendor ${contract.vendorId}'
: '';
return AppCard( return AppCard(
elevation: 0, elevation: 0,
@ -1066,6 +1075,26 @@ Color _assetStatusColor(String status, ThemeData theme) {
} }
} }
String _contractVendorLabel(
AmcContractModel contract,
List<FilterOptionModel> 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) { String _assetStatusLabel(String status) {
if (status.isEmpty) return ''; if (status.isEmpty) return '';
final normalized = status.replaceAll('_', ' ').toLowerCase(); final normalized = status.replaceAll('_', ' ').toLowerCase();

View File

@ -294,7 +294,7 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
title: 'Delete Asset', title: 'Delete Asset',
message: 'Soft delete "${asset.assetName}"?', message: 'Do you want to delete this "${asset.assetName}"?',
confirmLabel: 'Delete', confirmLabel: 'Delete',
isDestructive: true, isDestructive: true,
); );

View File

@ -1,14 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/errors/failure.dart';
import '../../../../core/utils/formatters.dart'; import '../../../../core/utils/formatters.dart';
import '../../../../core/utils/validators.dart'; import '../../../../core/utils/validators.dart';
import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/asset_model.dart';
import '../../../../shared/widgets/api_feedback.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/app_card.dart';
import '../../../../shared/widgets/app_dropdown.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_searchable_dropdown.dart';
import '../../../../shared/widgets/app_side_panel.dart'; import '../../../../shared/widgets/app_side_panel.dart';
import '../../../../shared/widgets/app_text_field.dart'; import '../../../../shared/widgets/app_text_field.dart';
import '../../../../shared/widgets/error_view.dart';
import '../providers/asset_form_lookups_provider.dart'; import '../providers/asset_form_lookups_provider.dart';
import '../providers/assets_provider.dart'; import '../providers/assets_provider.dart';
@ -1291,7 +1296,7 @@ class _TransferAssetPanelState extends ConsumerState<TransferAssetPanel> {
} }
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {
showSidePanelSnackBar(context, e.toString()); showSidePanelApiError(context, e);
} }
} finally { } finally {
if (mounted) setState(() => _isSubmitting = false); if (mounted) setState(() => _isSubmitting = false);
@ -1398,6 +1403,214 @@ class _TransferAssetPanelState extends ConsumerState<TransferAssetPanel> {
} }
} }
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 { class TransferHistoryPanel extends ConsumerWidget {
const TransferHistoryPanel({super.key, required this.assetId}); const TransferHistoryPanel({super.key, required this.assetId});
@ -1410,57 +1623,38 @@ class TransferHistoryPanel extends ConsumerWidget {
title: 'Transfer History', title: 'Transfer History',
child: historyAsync.when( child: historyAsync.when(
loading: () => const Center(child: CircularProgressIndicator()), 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) { data: (history) {
if (history.isEmpty) { if (history.isEmpty) {
return const Center( return const AppEmptyState(
child: Text('No transfer history found'), 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<String>().where((e) => e.trim().isNotEmpty).toList();
final toParts = [
item.toPlantName,
item.toDepartmentName,
item.toWarehouseName,
item.toUserName,
].whereType<String>().where((e) => e.trim().isNotEmpty).toList();
return Column( final theme = Theme.of(context);
crossAxisAlignment: CrossAxisAlignment.start, return Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text( children: [
transferDate, Text(
style: Theme.of(context).textTheme.titleSmall?.copyWith( '${history.length} transfer${history.length == 1 ? '' : 's'}',
fontWeight: FontWeight.w700, style: theme.textTheme.bodySmall?.copyWith(
), color: theme.colorScheme.onSurfaceVariant,
), fontWeight: FontWeight.w600,
const SizedBox(height: 8), ),
Text('From: ${fromParts.isEmpty ? '' : fromParts.join(' · ')}'), ),
const SizedBox(height: 4), const SizedBox(height: 16),
Text('To: ${toParts.isEmpty ? '' : toParts.join(' · ')}'), for (var index = 0; index < history.length; index++) ...[
if (item.reason?.trim().isNotEmpty == true) ...[ if (index > 0) const SizedBox(height: 12),
const SizedBox(height: 8), _TransferHistoryEntry(item: history[index]),
Text( ],
'Reason: ${item.reason!}', ],
style: Theme.of(context).textTheme.bodyMedium,
),
],
],
);
},
); );
}, },
), ),

View File

@ -479,6 +479,24 @@ String masterCellValue(Map<String, dynamic> row, MasterFieldDef field) {
if (field.type == MasterFieldType.dropdown) { if (field.type == MasterFieldType.dropdown) {
final label = row['${field.key}_label']; final label = row['${field.key}_label'];
if (label != null && label.toString().isNotEmpty) return label.toString(); if (label != null && label.toString().isNotEmpty) return label.toString();
// Prefer explicit "<base>_name" or nested "<base>.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) { if (field.staticOptions != null) {
return value.toString().replaceAll('_', ' '); return value.toString().replaceAll('_', ' ');
} }

View File

@ -290,7 +290,7 @@ class _PurchaseOrderDetailScreenState
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
title: 'Delete Purchase Order', title: 'Delete Purchase Order',
message: 'Soft delete this purchase order?', message: 'Do you want to delete this purchase order?',
confirmLabel: 'Delete', confirmLabel: 'Delete',
isDestructive: true, isDestructive: true,
); );

View File

@ -36,6 +36,16 @@ class PurchaseOrderListScreen extends ConsumerStatefulWidget {
class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScreen> { class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScreen> {
final _searchController = TextEditingController(); final _searchController = TextEditingController();
final Map<String, String> _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 @override
void dispose() { void dispose() {
@ -68,9 +78,11 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
error is Failure ? error : Failure.unknown(message: error.toString()), error is Failure ? error : Failure.unknown(message: error.toString()),
onRetry: () => ref.invalidate(purchaseOrdersListProvider), onRetry: () => ref.invalidate(purchaseOrdersListProvider),
), ),
data: (state) => Column( data: (state) {
crossAxisAlignment: CrossAxisAlignment.start, _rememberStatuses(state.orders);
children: [ return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
PageHeader( PageHeader(
title: 'Purchase Orders', title: 'Purchase Orders',
subtitle: 'Create, approve and track procurement orders', subtitle: 'Create, approve and track procurement orders',
@ -94,6 +106,7 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
return _FiltersBar( return _FiltersBar(
searchController: _searchController, searchController: _searchController,
query: state.query, query: state.query,
statusOptions: _statusFilterOptions(state.orders),
wrapped: constraints.maxWidth < 900, wrapped: constraints.maxWidth < 900,
onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch, onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch,
onStatusChanged: onStatusChanged:
@ -148,8 +161,9 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
), ),
), ),
), ),
], ],
), );
},
), ),
); );
} }
@ -189,12 +203,39 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
if (confirmed != true || !mounted) return; if (confirmed != true || !mounted) return;
await ref.read(purchaseOrdersListProvider.notifier).deletePurchaseOrder(order.id); await ref.read(purchaseOrdersListProvider.notifier).deletePurchaseOrder(order.id);
} }
List<AppDropdownOption<String?>> _statusFilterOptions(
List<PurchaseOrderModel> orders,
) {
final options = _knownStatuses.entries
.map((entry) => AppDropdownOption<String?>(
value: entry.key,
label: entry.value,
))
.toList()
..sort((a, b) => a.label.compareTo(b.label));
return [
const AppDropdownOption<String?>(value: null, label: 'All statuses'),
...options,
];
}
void _rememberStatuses(List<PurchaseOrderModel> 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 { class _FiltersBar extends StatelessWidget {
const _FiltersBar({ const _FiltersBar({
required this.searchController, required this.searchController,
required this.query, required this.query,
required this.statusOptions,
required this.wrapped, required this.wrapped,
required this.onSearch, required this.onSearch,
required this.onStatusChanged, required this.onStatusChanged,
@ -203,6 +244,7 @@ class _FiltersBar extends StatelessWidget {
final TextEditingController searchController; final TextEditingController searchController;
final PurchaseOrderListQuery query; final PurchaseOrderListQuery query;
final List<AppDropdownOption<String?>> statusOptions;
final bool wrapped; final bool wrapped;
final ValueChanged<String> onSearch; final ValueChanged<String> onSearch;
final ValueChanged<String?> onStatusChanged; final ValueChanged<String?> onStatusChanged;
@ -227,12 +269,7 @@ class _FiltersBar extends StatelessWidget {
value: query.status, value: query.status,
searchHint: 'Search status...', searchHint: 'Search status...',
isDense: true, isDense: true,
options: [ options: statusOptions,
const AppDropdownOption(value: null, label: 'All statuses'),
...poStatusOptions.map(
(e) => AppDropdownOption(value: e.$1, label: e.$2),
),
],
onChanged: onStatusChanged, onChanged: onStatusChanged,
), ),
), ),

View File

@ -114,7 +114,7 @@ class _UsersRoleManagementScreenState
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
title: 'Delete role', title: 'Delete role',
message: 'Delete "${role.name}"? This is a soft delete.', message: 'Do you want to delete this "${role.name}"?',
confirmLabel: 'Delete', confirmLabel: 'Delete',
isDestructive: true, isDestructive: true,
); );
@ -637,7 +637,7 @@ class _UsersTabState extends ConsumerState<_UsersTab> {
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
title: 'Deactivate user', title: 'Deactivate user',
message: 'Deactivate ${user.fullName}? This is a soft delete.', message: 'Do you want to deactivate ${user.fullName}?',
confirmLabel: 'Deactivate', confirmLabel: 'Deactivate',
isDestructive: true, isDestructive: true,
); );

View File

@ -162,7 +162,7 @@ class _UserListScreenState extends ConsumerState<UserListScreen> {
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
title: 'Deactivate user', title: 'Deactivate user',
message: 'Deactivate ${user.fullName}? This is a soft delete.', message: 'Do you want to deactivate ${user.fullName}?',
confirmLabel: 'Deactivate', confirmLabel: 'Deactivate',
isDestructive: true, isDestructive: true,
); );

View File

@ -320,10 +320,19 @@ class VendorFormNotifier extends FamilyAsyncNotifier<VendorModel?, String?> {
Future<VendorModel> submitUpdate(String id, Map<String, dynamic> data) async { Future<VendorModel> submitUpdate(String id, Map<String, dynamic> data) async {
final repository = ref.read(vendorRepositoryProvider); final repository = ref.read(vendorRepositoryProvider);
final result = await repository.updateVendor(id, data); final payload = Map<String, dynamic>.from(data);
final status = payload.remove('status')?.toString().trim();
final result = await repository.updateVendor(id, payload);
if (result.failure != null) throw result.failure!; 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(vendorsListProvider);
ref.invalidate(vendorDetailProvider(id)); ref.invalidate(vendorDetailProvider(id));
return result.data!; return (await repository.getVendorById(id)).data ?? result.data!;
} }
} }

View File

@ -61,6 +61,11 @@ class _VendorDetailScreenState extends ConsumerState<VendorDetailScreen>
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
PageHeader( PageHeader(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
tooltip: 'Back to Vendors',
onPressed: () => context.go(RouteConstants.vendors),
),
title: state.vendor.vendorName, title: state.vendor.vendorName,
subtitle: state.vendor.vendorCode ?? 'Vendor ID: ${state.vendor.id}', subtitle: state.vendor.vendorCode ?? 'Vendor ID: ${state.vendor.id}',
actions: [ actions: [
@ -181,7 +186,7 @@ class _VendorDetailScreenState extends ConsumerState<VendorDetailScreen>
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
title: 'Delete Vendor', title: 'Delete Vendor',
message: 'Soft delete this vendor?', message: 'Do you want to delete this vendor?',
confirmLabel: 'Delete', confirmLabel: 'Delete',
isDestructive: true, isDestructive: true,
); );
@ -837,128 +842,143 @@ class _VendorContactCard extends StatelessWidget {
final statusColor = contact.isActive final statusColor = contact.isActive
? const Color(0xFF16A34A) ? const Color(0xFF16A34A)
: theme.colorScheme.onSurfaceVariant; : theme.colorScheme.onSurfaceVariant;
final isPrimary = contact.isPrimary;
final primaryColor = theme.colorScheme.primary;
return AppCard( return AppCard(
elevation: 0, elevation: 0,
enableHover: true, enableHover: true,
child: Padding( child: Container(
padding: const EdgeInsets.all(16), decoration: BoxDecoration(
child: Column( color: isPrimary ? primaryColor.withValues(alpha: 0.05) : null,
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(12),
children: [ border: Border.all(
Row( color: isPrimary ? primaryColor.withValues(alpha: 0.35) : Colors.transparent,
children: [ ),
Container( ),
width: 36, child: Padding(
height: 36, padding: const EdgeInsets.all(16),
decoration: BoxDecoration( child: Column(
color: appearance.color.withValues(alpha: 0.12), crossAxisAlignment: CrossAxisAlignment.start,
borderRadius: BorderRadius.circular(8), children: [
),
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),
Row( Row(
children: [ children: [
Icon( Container(
Icons.email_outlined, width: 36,
size: 14, height: 36,
color: theme.colorScheme.onSurfaceVariant, decoration: BoxDecoration(
), color: appearance.color.withValues(alpha: 0.12),
const SizedBox(width: 4), borderRadius: BorderRadius.circular(8),
Expanded( ),
child: Text( child: Icon(
contact.email!, appearance.icon,
maxLines: 1, color: appearance.color,
overflow: TextOverflow.ellipsis, size: 20,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
), ),
), ),
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),
const Spacer(), Text(
Row( contact.contactName,
children: [ style: theme.textTheme.titleSmall?.copyWith(
if (contact.phone?.trim().isNotEmpty == true) fontWeight: FontWeight.w700,
Row( ),
mainAxisSize: MainAxisSize.min, ),
children: [ const SizedBox(height: 4),
Icon( if (contact.designation?.trim().isNotEmpty == true)
Icons.phone_outlined, Text(
size: 14, contact.designation!,
color: theme.colorScheme.onSurfaceVariant, maxLines: 1,
), overflow: TextOverflow.ellipsis,
const SizedBox(width: 4), style: theme.textTheme.bodySmall?.copyWith(
Text( color: theme.colorScheme.onSurfaceVariant,
contact.phone!, ),
),
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, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: theme.textTheme.bodySmall?.copyWith( style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant, 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 final statusColor = bankDetail.isActive
? const Color(0xFF16A34A) ? const Color(0xFF16A34A)
: theme.colorScheme.onSurfaceVariant; : theme.colorScheme.onSurfaceVariant;
final isPrimary = bankDetail.isPrimary;
final primaryColor = theme.colorScheme.primary;
final subtitle = [ final subtitle = [
bankDetail.branch, bankDetail.branch,
bankDetail.accountHolderName, bankDetail.accountHolderName,
@ -1000,131 +1022,169 @@ class _VendorBankDetailCard extends StatelessWidget {
return AppCard( return AppCard(
elevation: 0, elevation: 0,
enableHover: true, enableHover: true,
child: Padding( child: Container(
padding: const EdgeInsets.all(16), decoration: BoxDecoration(
child: Column( color: isPrimary ? primaryColor.withValues(alpha: 0.05) : null,
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(12),
children: [ border: Border.all(
Row( color: isPrimary ? primaryColor.withValues(alpha: 0.35) : Colors.transparent,
children: [ ),
Container( ),
width: 36, child: Padding(
height: 36, padding: const EdgeInsets.all(16),
decoration: BoxDecoration( child: Column(
color: appearance.color.withValues(alpha: 0.12), crossAxisAlignment: CrossAxisAlignment.start,
borderRadius: BorderRadius.circular(8), children: [
),
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),
Row( Row(
children: [ children: [
Icon( Container(
Icons.tag_outlined, width: 36,
size: 14, 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, color: theme.colorScheme.onSurfaceVariant,
), ),
const SizedBox(width: 4), ),
Expanded( ],
child: Text( if (bankDetail.ifsc?.trim().isNotEmpty == true) ...[
bankDetail.ifsc!, const SizedBox(height: 8),
maxLines: 1, Row(
overflow: TextOverflow.ellipsis, children: [
style: theme.textTheme.bodySmall?.copyWith( Icon(
color: theme.colorScheme.onSurfaceVariant, 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: [ class _PrimaryBadge extends StatelessWidget {
Icon( const _PrimaryBadge({required this.color});
Icons.numbers_outlined,
size: 14, final Color color;
color: theme.colorScheme.onSurfaceVariant,
), @override
const SizedBox(width: 4), Widget build(BuildContext context) {
Flexible( return Container(
child: Text( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
'A/C: ${bankDetail.accountNumber!}', decoration: BoxDecoration(
maxLines: 1, color: color.withValues(alpha: 0.12),
overflow: TextOverflow.ellipsis, borderRadius: BorderRadius.circular(999),
style: theme.textTheme.bodySmall?.copyWith( border: Border.all(color: color.withValues(alpha: 0.3)),
color: theme.colorScheme.onSurfaceVariant, ),
), child: Text(
), 'Primary',
), style: Theme.of(context).textTheme.labelSmall?.copyWith(
], color: color,
), fontWeight: FontWeight.w700,
),
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,
),
),
],
),
],
), ),
), ),
); );

View File

@ -163,7 +163,7 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
final confirmed = await showAppConfirmationDialog( final confirmed = await showAppConfirmationDialog(
context: context, context: context,
title: 'Delete Vendor', title: 'Delete Vendor',
message: 'Soft delete "${vendor.vendorName}"?', message: 'Do you want to delete this "${vendor.vendorName}"?',
confirmLabel: 'Delete', confirmLabel: 'Delete',
isDestructive: true, isDestructive: true,
); );

View File

@ -63,6 +63,7 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
String? _sourceOfSupply; String? _sourceOfSupply;
int? _paymentTermId; int? _paymentTermId;
bool _isActive = true; bool _isActive = true;
String? _currentStatus;
bool _isSubmitting = false; bool _isSubmitting = false;
String? _populatedSignature; String? _populatedSignature;
@ -79,9 +80,10 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
String _vendorSignature(VendorModel vendor) => String _vendorSignature(VendorModel vendor) =>
'${vendor.id}:${vendor.vendorType}:${vendor.gstTreatment}:' '${vendor.id}:${vendor.vendorType}:${vendor.gstTreatment}:'
'${vendor.sourceOfSupply}:${vendor.paymentTermId}:' '${vendor.sourceOfSupply}:${vendor.paymentTermId}:'
'${vendor.creditPeriodDays}:${vendor.isActive}:${vendor.vendorName}'; '${vendor.creditPeriodDays}:${vendor.status}:${vendor.vendorName}';
void _populateFromVendor(VendorModel vendor) { void _populateFromVendor(VendorModel vendor) {
final normalizedStatus = (vendor.status ?? '').trim().toLowerCase();
setState(() { setState(() {
_nameController.text = vendor.vendorName; _nameController.text = vendor.vendorName;
_vendorType = vendor.vendorType; _vendorType = vendor.vendorType;
@ -93,10 +95,23 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
_creditDaysController.text = _creditDaysController.text =
vendor.creditPeriodDays?.toString() ?? ''; vendor.creditPeriodDays?.toString() ?? '';
_remarksController.text = vendor.remarks ?? ''; _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<String, dynamic> _buildPayload() { Map<String, dynamic> _buildPayload() {
return { return {
'vendor_name': _nameController.text.trim(), 'vendor_name': _nameController.text.trim(),
@ -112,7 +127,8 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
'credit_period_days': int.tryParse(_creditDaysController.text.trim()), 'credit_period_days': int.tryParse(_creditDaysController.text.trim()),
if (_remarksController.text.trim().isNotEmpty) if (_remarksController.text.trim().isNotEmpty)
'remarks': _remarksController.text.trim(), 'remarks': _remarksController.text.trim(),
'is_active': _isActive, if (!widget.isEditing || _showActiveSwitch)
'status': _isActive ? 'active' : 'inactive',
}; };
} }
@ -309,13 +325,15 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
label: 'Remarks', label: 'Remarks',
maxLines: 3, maxLines: 3,
), ),
const SizedBox(height: 12), if (_showActiveSwitch) ...[
SwitchListTile( const SizedBox(height: 12),
contentPadding: EdgeInsets.zero, SwitchListTile(
title: const Text('Active'), contentPadding: EdgeInsets.zero,
value: _isActive, title: const Text('Active'),
onChanged: (v) => setState(() => _isActive = v), value: _isActive,
), onChanged: (v) => setState(() => _isActive = v),
),
],
], ],
), ),
); );

View File

@ -93,7 +93,12 @@ Object? _readWarehouseName(Map<dynamic, dynamic> json, String key) {
Object? _readVendorNameFromNested(Map<dynamic, dynamic> json, String key) { Object? _readVendorNameFromNested(Map<dynamic, dynamic> json, String key) {
final flat = json['vendor_name']; final flat = json['vendor_name'];
if (flat is String && flat.isNotEmpty) return flat; 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 @freezed
@ -293,7 +298,8 @@ class AmcContractModel with _$AmcContractModel {
@JsonKey(fromJson: _idFromJson) required String id, @JsonKey(fromJson: _idFromJson) required String id,
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId, @JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @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_no') String? contractNo,
@JsonKey(name: 'contract_type') String? contractType, @JsonKey(name: 'contract_type') String? contractType,
@JsonKey(name: 'start_date', fromJson: _dateFromJson) required DateTime startDate, @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_name') String? engineerName,
@JsonKey(name: 'engineer_phone') String? engineerPhone, @JsonKey(name: 'engineer_phone') String? engineerPhone,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @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: 'work_done') String? workDone,
@JsonKey(name: 'parts_replaced') String? partsReplaced, @JsonKey(name: 'parts_replaced') String? partsReplaced,
@JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable)
@ -445,9 +452,11 @@ class AssetTransferHistoryModel {
String? readNestedName(String key) { String? readNestedName(String key) {
final nested = json[key]; final nested = json[key];
if (nested is Map<String, dynamic>) { if (nested is Map) {
final name = nested['name']; for (final field in ['name', 'full_name']) {
if (name is String && name.trim().isNotEmpty) return name.trim(); final value = nested[field];
if (value is String && value.trim().isNotEmpty) return value.trim();
}
} }
return null; return null;
} }

View File

@ -1734,7 +1734,7 @@ mixin _$AmcContractModel {
String? get assetId => throw _privateConstructorUsedError; String? get assetId => throw _privateConstructorUsedError;
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable)
int? get vendorId => throw _privateConstructorUsedError; int? get vendorId => throw _privateConstructorUsedError;
@JsonKey(name: 'vendor_name') @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested)
String? get vendorName => throw _privateConstructorUsedError; String? get vendorName => throw _privateConstructorUsedError;
@JsonKey(name: 'contract_no') @JsonKey(name: 'contract_no')
String? get contractNo => throw _privateConstructorUsedError; String? get contractNo => throw _privateConstructorUsedError;
@ -1791,7 +1791,8 @@ abstract class $AmcContractModelCopyWith<$Res> {
@JsonKey(fromJson: _idFromJson) String id, @JsonKey(fromJson: _idFromJson) String id,
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId, @JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @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_no') String? contractNo,
@JsonKey(name: 'contract_type') String? contractType, @JsonKey(name: 'contract_type') String? contractType,
@JsonKey(name: 'start_date', fromJson: _dateFromJson) DateTime startDate, @JsonKey(name: 'start_date', fromJson: _dateFromJson) DateTime startDate,
@ -1964,7 +1965,8 @@ abstract class _$$AmcContractModelImplCopyWith<$Res>
@JsonKey(fromJson: _idFromJson) String id, @JsonKey(fromJson: _idFromJson) String id,
@JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId, @JsonKey(name: 'asset_id', fromJson: _idFromJson) String? assetId,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @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_no') String? contractNo,
@JsonKey(name: 'contract_type') String? contractType, @JsonKey(name: 'contract_type') String? contractType,
@JsonKey(name: 'start_date', fromJson: _dateFromJson) DateTime startDate, @JsonKey(name: 'start_date', fromJson: _dateFromJson) DateTime startDate,
@ -2129,7 +2131,8 @@ class _$AmcContractModelImpl implements _AmcContractModel {
@JsonKey(fromJson: _idFromJson) required this.id, @JsonKey(fromJson: _idFromJson) required this.id,
@JsonKey(name: 'asset_id', fromJson: _idFromJson) this.assetId, @JsonKey(name: 'asset_id', fromJson: _idFromJson) this.assetId,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId, @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_no') this.contractNo,
@JsonKey(name: 'contract_type') this.contractType, @JsonKey(name: 'contract_type') this.contractType,
@JsonKey(name: 'start_date', fromJson: _dateFromJson) @JsonKey(name: 'start_date', fromJson: _dateFromJson)
@ -2168,7 +2171,7 @@ class _$AmcContractModelImpl implements _AmcContractModel {
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable)
final int? vendorId; final int? vendorId;
@override @override
@JsonKey(name: 'vendor_name') @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested)
final String? vendorName; final String? vendorName;
@override @override
@JsonKey(name: 'contract_no') @JsonKey(name: 'contract_no')
@ -2325,7 +2328,8 @@ abstract class _AmcContractModel implements AmcContractModel {
@JsonKey(name: 'asset_id', fromJson: _idFromJson) final String? assetId, @JsonKey(name: 'asset_id', fromJson: _idFromJson) final String? assetId,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable)
final int? vendorId, 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_no') final String? contractNo,
@JsonKey(name: 'contract_type') final String? contractType, @JsonKey(name: 'contract_type') final String? contractType,
@JsonKey(name: 'start_date', fromJson: _dateFromJson) @JsonKey(name: 'start_date', fromJson: _dateFromJson)
@ -2365,7 +2369,7 @@ abstract class _AmcContractModel implements AmcContractModel {
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable)
int? get vendorId; int? get vendorId;
@override @override
@JsonKey(name: 'vendor_name') @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested)
String? get vendorName; String? get vendorName;
@override @override
@JsonKey(name: 'contract_no') @JsonKey(name: 'contract_no')
@ -2457,7 +2461,7 @@ mixin _$ServiceVisitModel {
String? get engineerPhone => throw _privateConstructorUsedError; String? get engineerPhone => throw _privateConstructorUsedError;
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable)
int? get vendorId => throw _privateConstructorUsedError; int? get vendorId => throw _privateConstructorUsedError;
@JsonKey(name: 'vendor_name') @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested)
String? get vendorName => throw _privateConstructorUsedError; String? get vendorName => throw _privateConstructorUsedError;
@JsonKey(name: 'work_done') @JsonKey(name: 'work_done')
String? get workDone => throw _privateConstructorUsedError; String? get workDone => throw _privateConstructorUsedError;
@ -2511,7 +2515,8 @@ abstract class $ServiceVisitModelCopyWith<$Res> {
@JsonKey(name: 'engineer_name') String? engineerName, @JsonKey(name: 'engineer_name') String? engineerName,
@JsonKey(name: 'engineer_phone') String? engineerPhone, @JsonKey(name: 'engineer_phone') String? engineerPhone,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @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: 'work_done') String? workDone,
@JsonKey(name: 'parts_replaced') String? partsReplaced, @JsonKey(name: 'parts_replaced') String? partsReplaced,
@JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable)
@ -2693,7 +2698,8 @@ abstract class _$$ServiceVisitModelImplCopyWith<$Res>
@JsonKey(name: 'engineer_name') String? engineerName, @JsonKey(name: 'engineer_name') String? engineerName,
@JsonKey(name: 'engineer_phone') String? engineerPhone, @JsonKey(name: 'engineer_phone') String? engineerPhone,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @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: 'work_done') String? workDone,
@JsonKey(name: 'parts_replaced') String? partsReplaced, @JsonKey(name: 'parts_replaced') String? partsReplaced,
@JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable)
@ -2868,7 +2874,8 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
@JsonKey(name: 'engineer_name') this.engineerName, @JsonKey(name: 'engineer_name') this.engineerName,
@JsonKey(name: 'engineer_phone') this.engineerPhone, @JsonKey(name: 'engineer_phone') this.engineerPhone,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) this.vendorId, @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: 'work_done') this.workDone,
@JsonKey(name: 'parts_replaced') this.partsReplaced, @JsonKey(name: 'parts_replaced') this.partsReplaced,
@JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable)
@ -2925,7 +2932,7 @@ class _$ServiceVisitModelImpl implements _ServiceVisitModel {
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable)
final int? vendorId; final int? vendorId;
@override @override
@JsonKey(name: 'vendor_name') @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested)
final String? vendorName; final String? vendorName;
@override @override
@JsonKey(name: 'work_done') @JsonKey(name: 'work_done')
@ -3076,7 +3083,8 @@ abstract class _ServiceVisitModel implements ServiceVisitModel {
@JsonKey(name: 'engineer_phone') final String? engineerPhone, @JsonKey(name: 'engineer_phone') final String? engineerPhone,
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable)
final int? vendorId, 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: 'work_done') final String? workDone,
@JsonKey(name: 'parts_replaced') final String? partsReplaced, @JsonKey(name: 'parts_replaced') final String? partsReplaced,
@JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable) @JsonKey(name: 'next_service_date', fromJson: _dateFromJsonNullable)
@ -3133,7 +3141,7 @@ abstract class _ServiceVisitModel implements ServiceVisitModel {
@JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable)
int? get vendorId; int? get vendorId;
@override @override
@JsonKey(name: 'vendor_name') @JsonKey(name: 'vendor_name', readValue: _readVendorNameFromNested)
String? get vendorName; String? get vendorName;
@override @override
@JsonKey(name: 'work_done') @JsonKey(name: 'work_done')

View File

@ -142,7 +142,7 @@ _$AmcContractModelImpl _$$AmcContractModelImplFromJson(
id: _idFromJson(json['id']), id: _idFromJson(json['id']),
assetId: _idFromJson(json['asset_id']), assetId: _idFromJson(json['asset_id']),
vendorId: _intFromJsonNullable(json['vendor_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?, contractNo: json['contract_no'] as String?,
contractType: json['contract_type'] as String?, contractType: json['contract_type'] as String?,
startDate: _dateFromJson(json['start_date']), startDate: _dateFromJson(json['start_date']),
@ -205,7 +205,7 @@ _$ServiceVisitModelImpl _$$ServiceVisitModelImplFromJson(
engineerName: json['engineer_name'] as String?, engineerName: json['engineer_name'] as String?,
engineerPhone: json['engineer_phone'] as String?, engineerPhone: json['engineer_phone'] as String?,
vendorId: _intFromJsonNullable(json['vendor_id']), vendorId: _intFromJsonNullable(json['vendor_id']),
vendorName: json['vendor_name'] as String?, vendorName: _readVendorNameFromNested(json, 'vendor_name') as String?,
workDone: json['work_done'] as String?, workDone: json['work_done'] as String?,
partsReplaced: json['parts_replaced'] as String?, partsReplaced: json['parts_replaced'] as String?,
nextServiceDate: _dateFromJsonNullable(json['next_service_date']), nextServiceDate: _dateFromJsonNullable(json['next_service_date']),

View File

@ -211,7 +211,7 @@ const poTypeOptions = [
const poStatusOptions = [ const poStatusOptions = [
('DRAFT', 'Draft'), ('DRAFT', 'Draft'),
('SUBMITTED', 'Submitted'), // ('SUBMITTED', 'Submitted'),
('PENDING_APPROVAL', 'Pending Approval'), ('PENDING_APPROVAL', 'Pending Approval'),
('APPROVED', 'Approved'), ('APPROVED', 'Approved'),
('REJECTED', 'Rejected'), ('REJECTED', 'Rejected'),
@ -231,9 +231,15 @@ String poTypeLabel(String? value) {
String poStatusLabel(String? value) { String poStatusLabel(String? value) {
if (value == null) return ''; if (value == null) return '';
final normalizedKey = value.trim().toUpperCase().replaceAll(' ', '_');
return poStatusOptions return poStatusOptions
.where((e) => e.$1 == value) .where((e) => e.$1 == normalizedKey)
.map((e) => e.$2) .map((e) => e.$2)
.firstOrNull ?? .firstOrNull ??
value.replaceAll('_', ' '); normalizedKey
.toLowerCase()
.split('_')
.where((word) => word.isNotEmpty)
.map((word) => '${word[0].toUpperCase()}${word.substring(1)}')
.join(' ');
} }

View File

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import '../../core/errors/failure.dart'; import '../../core/errors/failure.dart';
import '../../core/network/api_handler.dart'; import '../../core/network/api_handler.dart';
import 'app_side_panel.dart';
void showAccessDeniedSnackBar(BuildContext context, {String? message}) { void showAccessDeniedSnackBar(BuildContext context, {String? message}) {
ScaffoldMessenger.of(context).showSnackBar( 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) { void showApiFailureSnackBar(BuildContext context, Failure failure) {
if (isForbiddenFailure(failure)) { if (isForbiddenFailure(failure)) {
showAccessDeniedSnackBar(context, message: failure.message); showAccessDeniedSnackBar(context, message: failure.message);

View File

@ -32,15 +32,37 @@ class AppStatusChip extends StatelessWidget {
} }
(Color, String) _resolveStatus(String raw) { (Color, String) _resolveStatus(String raw) {
switch (raw.toLowerCase()) { final normalized = raw.trim().toLowerCase().replaceAll(' ', '_');
switch (normalized) {
case 'active': case 'active':
return (Colors.green.shade700, EntityStatus.active.label); return (Colors.green.shade700, EntityStatus.active.label);
case 'inactive': case 'inactive':
return (Colors.grey.shade700, EntityStatus.inactive.label); return (Colors.grey.shade700, EntityStatus.inactive.label);
case 'locked': case 'locked':
return (Colors.orange.shade800, '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: 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(' ');
}
} }

View File

@ -8,11 +8,13 @@ class PageHeader extends StatelessWidget {
required this.title, required this.title,
this.subtitle, this.subtitle,
this.actions, this.actions,
this.leading,
}); });
final String title; final String title;
final String? subtitle; final String? subtitle;
final List<Widget>? actions; final List<Widget>? actions;
final Widget? leading;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -22,7 +24,20 @@ class PageHeader extends StatelessWidget {
? Column( ? Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ 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) ...[ if (subtitle != null) ...[
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
@ -40,6 +55,10 @@ class PageHeader extends StatelessWidget {
) )
: Row( : Row(
children: [ children: [
if (leading != null) ...[
leading!,
const SizedBox(width: 8),
],
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,