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));
|
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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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('_', ' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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!;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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')
|
||||||
|
|||||||
@ -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']),
|
||||||
|
|||||||
@ -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(' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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(' ');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user