Bharat erp - bug fixes
This commit is contained in:
parent
ad1c95d85d
commit
fa610b7efd
@ -40,9 +40,8 @@ class AssetRemoteDataSource {
|
||||
await dio.delete(ApiEndpoints.assetById(id));
|
||||
}
|
||||
|
||||
Future<AssetModel> transferAsset(String id, Map<String, dynamic> data) async {
|
||||
final response = await dio.post(ApiEndpoints.assetTransfer(id), data: data);
|
||||
return AssetModel.fromJson(response.data['data'] as Map<String, dynamic>);
|
||||
Future<void> transferAsset(String id, Map<String, dynamic> data) async {
|
||||
await dio.post(ApiEndpoints.assetTransfer(id), data: data);
|
||||
}
|
||||
|
||||
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>(
|
||||
dynamic body,
|
||||
T Function(Map<String, dynamic>) fromJson,
|
||||
) {
|
||||
if (body is! Map<String, dynamic>) return [];
|
||||
if (body is! Map) return [];
|
||||
final raw = body['data'];
|
||||
if (raw is List) {
|
||||
return raw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(fromJson)
|
||||
.whereType<Map>()
|
||||
.map((item) => fromJson(_asStringMap(item)))
|
||||
.toList();
|
||||
}
|
||||
if (raw is Map<String, dynamic>) {
|
||||
if (raw is Map) {
|
||||
final items = raw['items'];
|
||||
if (items is List) {
|
||||
return items
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(fromJson)
|
||||
.whereType<Map>()
|
||||
.map((item) => fromJson(_asStringMap(item)))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@ class AssetRepositoryImpl implements AssetRepository {
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ abstract class AssetRepository {
|
||||
Future<Result<AssetModel>> createAsset(Map<String, dynamic> data);
|
||||
Future<Result<AssetModel>> updateAsset(String id, Map<String, dynamic> data);
|
||||
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<AssetCategoryModel>>> getCategories();
|
||||
Future<Result<AssetDropdownOptionsModel>> getAssetOptions();
|
||||
|
||||
@ -206,13 +206,13 @@ class AssetDetailNotifier extends FamilyAsyncNotifier<AssetDetailState, String>
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<AssetModel?> transferAsset(Map<String, dynamic> data) async {
|
||||
Future<void> transferAsset(Map<String, dynamic> 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<AmcContractModel?> createAmc(Map<String, dynamic> data) async {
|
||||
|
||||
@ -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<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),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<AssetDetailScreen>
|
||||
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<AssetDetailScreen>
|
||||
);
|
||||
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 <FilterOptionModel>[];
|
||||
|
||||
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<FilterOptionModel> 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<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) {
|
||||
if (status.isEmpty) return '—';
|
||||
final normalized = status.replaceAll('_', ' ').toLowerCase();
|
||||
|
||||
@ -294,7 +294,7 @@ class _AssetListScreenState extends ConsumerState<AssetListScreen> {
|
||||
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,
|
||||
);
|
||||
|
||||
@ -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<TransferAssetPanel> {
|
||||
}
|
||||
} 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<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 {
|
||||
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<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(
|
||||
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]),
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@ -479,6 +479,24 @@ String masterCellValue(Map<String, dynamic> 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 "<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) {
|
||||
return value.toString().replaceAll('_', ' ');
|
||||
}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -36,6 +36,16 @@ class PurchaseOrderListScreen extends ConsumerStatefulWidget {
|
||||
|
||||
class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScreen> {
|
||||
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
|
||||
void dispose() {
|
||||
@ -68,9 +78,11 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
error is Failure ? error : Failure.unknown(message: error.toString()),
|
||||
onRetry: () => 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<PurchaseOrderListScree
|
||||
return _FiltersBar(
|
||||
searchController: _searchController,
|
||||
query: state.query,
|
||||
statusOptions: _statusFilterOptions(state.orders),
|
||||
wrapped: constraints.maxWidth < 900,
|
||||
onSearch: ref.read(purchaseOrdersListProvider.notifier).setSearch,
|
||||
onStatusChanged:
|
||||
@ -148,8 +161,9 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -189,12 +203,39 @@ class _PurchaseOrderListScreenState extends ConsumerState<PurchaseOrderListScree
|
||||
if (confirmed != true || !mounted) return;
|
||||
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 {
|
||||
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<AppDropdownOption<String?>> statusOptions;
|
||||
final bool wrapped;
|
||||
final ValueChanged<String> onSearch;
|
||||
final ValueChanged<String?> 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,
|
||||
),
|
||||
),
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -162,7 +162,7 @@ class _UserListScreenState extends ConsumerState<UserListScreen> {
|
||||
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,
|
||||
);
|
||||
|
||||
@ -320,10 +320,19 @@ class VendorFormNotifier extends FamilyAsyncNotifier<VendorModel?, String?> {
|
||||
|
||||
Future<VendorModel> submitUpdate(String id, Map<String, dynamic> data) async {
|
||||
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 (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!;
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,6 +61,11 @@ class _VendorDetailScreenState extends ConsumerState<VendorDetailScreen>
|
||||
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<VendorDetailScreen>
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@ -163,7 +163,7 @@ class _VendorListScreenState extends ConsumerState<VendorListScreen> {
|
||||
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,
|
||||
);
|
||||
|
||||
@ -63,6 +63,7 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
String? _sourceOfSupply;
|
||||
int? _paymentTermId;
|
||||
bool _isActive = true;
|
||||
String? _currentStatus;
|
||||
bool _isSubmitting = false;
|
||||
String? _populatedSignature;
|
||||
|
||||
@ -79,9 +80,10 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
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<VendorFormPanel> {
|
||||
_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<String, dynamic> _buildPayload() {
|
||||
return {
|
||||
'vendor_name': _nameController.text.trim(),
|
||||
@ -112,7 +127,8 @@ class _VendorFormPanelState extends ConsumerState<VendorFormPanel> {
|
||||
'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<VendorFormPanel> {
|
||||
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),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -93,7 +93,12 @@ Object? _readWarehouseName(Map<dynamic, dynamic> json, String key) {
|
||||
Object? _readVendorNameFromNested(Map<dynamic, dynamic> 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<String, dynamic>) {
|
||||
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;
|
||||
}
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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']),
|
||||
|
||||
@ -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(' ');
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(' ');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Widget>? 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,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user