import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/utils/column_search_paging.dart'; import '../../../../core/utils/table_search.dart'; import '../../../../shared/models/asset_model.dart'; import '../../../../shared/models/entity_attachment_model.dart'; import '../../../../shared/models/export_file_result.dart'; import '../../data/repositories/asset_repository_impl.dart'; import 'asset_categories_provider.dart'; class AssetsListState { const AssetsListState({ this.assets = const [], this.query = const AssetListQuery(limit: 20), this.total = 0, this.totalPages = 1, this.isRefreshing = false, this.isExporting = false, this.actionError, this.actionSuccess, }); final List assets; final AssetListQuery query; final int total; final int totalPages; final bool isRefreshing; final bool isExporting; final String? actionError; final String? actionSuccess; AssetsListState copyWith({ List? assets, AssetListQuery? query, int? total, int? totalPages, bool? isRefreshing, bool? isExporting, String? actionError, String? actionSuccess, bool clearMessages = false, }) { return AssetsListState( assets: assets ?? this.assets, query: query ?? this.query, total: total ?? this.total, totalPages: totalPages ?? this.totalPages, isRefreshing: isRefreshing ?? this.isRefreshing, isExporting: isExporting ?? this.isExporting, actionError: clearMessages ? null : actionError ?? this.actionError, actionSuccess: clearMessages ? null : actionSuccess ?? this.actionSuccess, ); } } final assetsListProvider = AsyncNotifierProvider.autoDispose( AssetsListNotifier.new, ); class AssetsListNotifier extends AutoDisposeAsyncNotifier { final _columnSearch = ColumnSearchPaging(); @override Future build() async { return _load(const AssetListQuery(limit: 20)); } Future _load(AssetListQuery query) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.getAssets(query); if (result.failure != null) throw result.failure!; final page = result.data!; var items = List.from(page.items); final search = TableSearch.normalize(query.search); // API search often ignores category name — pull matching categories too. if (search.isNotEmpty && query.itemCategoryId == null) { final categories = await ref.read(itemCategoriesProvider.future); var categoryMatches = 0; for (final category in categories) { if (!TableSearch.matches(search, [category.name])) continue; if (++categoryMatches > 5) break; final categoryId = int.tryParse(category.id); if (categoryId == null) continue; final byCategory = await repository.getAssets( query.copyWith(search: null, itemCategoryId: categoryId), ); if (byCategory.failure == null && byCategory.data != null) { items = TableSearch.mergeById( items, byCategory.data!.items, (asset) => asset.id, ); } } // Keep API page results + enriched matches; pagination stays server-driven. } return AssetsListState( assets: items, query: query, total: page.total, totalPages: page.totalPages, ); } Future refresh() async { final current = state.valueOrNull ?? const AssetsListState(); state = AsyncData(current.copyWith(isRefreshing: true, clearMessages: true)); try { state = AsyncData(await _load(current.query)); } catch (e, st) { state = AsyncError(e, st); } } Future applyQuery(AssetListQuery query) async { final previous = state.valueOrNull; if (previous == null) { state = const AsyncLoading(); } try { state = AsyncData(await _load(query)); } catch (e, st) { state = AsyncError(e, st); } } void setSearch(String search) { final current = state.valueOrNull; if (current == null) return; applyQuery( current.query.copyWith(search: TableSearch.normalize(search), page: 1), ); } Future ensureColumnSearchDataset() async { final current = state.valueOrNull; if (current == null) return; final limit = _columnSearch.beginFullDataset( currentLimit: current.query.limit, total: current.total, ); if (limit == null) return; await applyQuery( current.query.copyWith(search: null, page: 1, limit: limit), ); } void clearColumnSearchDataset() { final current = state.valueOrNull; if (current == null) return; final limit = _columnSearch.endFullDataset(); if (limit == null) return; applyQuery(current.query.copyWith(search: null, page: 1, limit: limit)); } void setStatusFilter(String? status) { final current = state.valueOrNull; if (current == null) return; applyQuery(current.query.copyWith(status: status, page: 1)); } void setCategoryFilter(int? itemCategoryId) { final current = state.valueOrNull; if (current == null) return; applyQuery(current.query.copyWith(itemCategoryId: itemCategoryId, page: 1)); } void setLocationFilter(int? locationId) { final current = state.valueOrNull; if (current == null) return; applyQuery(current.query.copyWith(locationId: locationId, page: 1)); } void setPage(int page) { final current = state.valueOrNull; if (current == null) return; applyQuery(current.query.copyWith(page: page)); } void setPageSize(int limit) { final current = state.valueOrNull; if (current == null) return; applyQuery(current.query.copyWith(limit: limit, page: 1)); } Future exportAssets() async { final current = state.valueOrNull; if (current == null) return null; state = AsyncData(current.copyWith(isExporting: true, clearMessages: true)); final result = await ref.read(assetRepositoryProvider).exportAssets(current.query); final latest = state.valueOrNull ?? current; if (result.failure != null) { state = AsyncData( latest.copyWith( isExporting: false, actionError: result.failure!.message, ), ); return null; } state = AsyncData(latest.copyWith(isExporting: false)); return result.data; } Future deleteAsset(String id) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.deleteAsset(id); if (result.failure != null) { final current = state.valueOrNull; if (current != null) { state = AsyncData(current.copyWith(actionError: result.failure!.message)); } return false; } await refresh(); ref.invalidate(myMaintenanceProvider); final current = state.valueOrNull; if (current != null) { state = AsyncData(current.copyWith(actionSuccess: 'Asset deleted')); } return true; } } final assetDetailProvider = AsyncNotifierProvider.family( AssetDetailNotifier.new, ); class AssetDetailState { const AssetDetailState({ required this.asset, this.amcContracts = const [], this.serviceVisits = const [], this.insurancePolicies = const [], this.attachments = const [], }); final AssetModel asset; final List amcContracts; final List serviceVisits; final List insurancePolicies; final List attachments; AssetDetailState copyWith({ AssetModel? asset, List? amcContracts, List? serviceVisits, List? insurancePolicies, List? attachments, }) { return AssetDetailState( asset: asset ?? this.asset, amcContracts: amcContracts ?? this.amcContracts, serviceVisits: serviceVisits ?? this.serviceVisits, insurancePolicies: insurancePolicies ?? this.insurancePolicies, attachments: attachments ?? this.attachments, ); } } class AssetDetailNotifier extends FamilyAsyncNotifier { @override Future build(String arg) async { return _loadAll(arg); } Future _loadAll(String assetId) async { final repository = ref.read(assetRepositoryProvider); final assetResult = await repository.getAssetById(assetId); if (assetResult.failure != null) throw assetResult.failure!; final amcResult = await repository.getAmcContracts(assetId); final visitsResult = await repository.getServiceVisits(assetId); final insuranceResult = await repository.getInsurancePolicies(assetId); final attachmentsResult = await repository.listAttachments(assetId); return AssetDetailState( asset: assetResult.data!, amcContracts: amcResult.data ?? [], serviceVisits: visitsResult.data ?? [], insurancePolicies: insuranceResult.data ?? [], attachments: attachmentsResult.data ?? [], ); } Future reload() async { state = const AsyncLoading(); state = AsyncData(await _loadAll(arg)); } Future uploadAttachment({ required List bytes, required String filename, String attachmentType = 'DOCUMENT', }) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.uploadAttachment( arg, bytes: bytes, filename: filename, attachmentType: attachmentType, ); if (result.failure != null) throw result.failure!; final current = state.valueOrNull; if (current == null) { await reload(); } else { state = AsyncData( current.copyWith( attachments: [...current.attachments, result.data!], ), ); } return result.data!; } Future> downloadAttachment(String attachmentId) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.downloadAttachment(arg, attachmentId); if (result.failure != null) throw result.failure!; return result.data ?? []; } Future deleteAttachment(String attachmentId) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.deleteAttachment(arg, attachmentId); if (result.failure != null) throw result.failure!; final current = state.valueOrNull; if (current == null) { await reload(); return; } state = AsyncData( current.copyWith( attachments: current.attachments .where((a) => a.id != attachmentId) .toList(), ), ); } Future updateAsset(Map data) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.updateAsset(arg, data); if (result.failure != null) throw result.failure!; await reload(); ref.invalidate(assetsListProvider); ref.invalidate(myMaintenanceProvider); return result.data; } Future deleteAsset() async { final repository = ref.read(assetRepositoryProvider); final result = await repository.deleteAsset(arg); if (result.failure != null) return false; ref.invalidate(assetsListProvider); ref.invalidate(myMaintenanceProvider); return true; } Future transferAsset(Map data) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.transferAsset(arg, data); if (result.failure != null) throw result.failure!; await reload(); ref.invalidate(assetsListProvider); ref.invalidate(transferHistoryProvider(arg)); } Future createAmc(Map data) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.createAmcContract(arg, data); if (result.failure != null) throw result.failure!; await reload(); return result.data; } Future updateAmc( String contractId, Map data, ) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.updateAmcContract(arg, contractId, data); if (result.failure != null) throw result.failure!; await reload(); return result.data; } Future logVisit(Map data) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.logServiceVisit(arg, data); if (result.failure != null) throw result.failure!; await reload(); return result.data; } Future updateVisit( String visitId, Map data, ) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.updateServiceVisit(arg, visitId, data); if (result.failure != null) throw result.failure!; await reload(); return result.data; } Future createInsurance(Map data) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.createInsurancePolicy(arg, data); if (result.failure != null) throw result.failure!; await reload(); return result.data; } Future updateInsurance( String policyId, Map data, ) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.updateInsurancePolicy(arg, policyId, data); if (result.failure != null) throw result.failure!; await reload(); return result.data; } } typedef AmcContractFormParams = ({String assetId, String contractId}); final amcContractFormProvider = FutureProvider.autoDispose .family((ref, params) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.getAmcContractById( params.assetId, params.contractId, ); if (result.failure != null) throw result.failure!; return result.data!; }); typedef ServiceVisitFormParams = ({String assetId, String visitId}); final serviceVisitFormProvider = FutureProvider.autoDispose .family((ref, params) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.getServiceVisitById( params.assetId, params.visitId, ); if (result.failure != null) throw result.failure!; return result.data!; }); typedef InsurancePolicyFormParams = ({String assetId, String policyId}); final insurancePolicyFormProvider = FutureProvider.autoDispose .family((ref, params) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.getInsurancePolicyById( params.assetId, params.policyId, ); if (result.failure != null) throw result.failure!; return result.data!; }); final transferHistoryProvider = FutureProvider.autoDispose .family, String>((ref, assetId) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.getTransferHistory(assetId); if (result.failure != null) throw result.failure!; return result.data ?? const []; }); final assetFormProvider = AsyncNotifierProvider.family.autoDispose( AssetFormNotifier.new, ); class AssetFormNotifier extends AutoDisposeFamilyAsyncNotifier { @override Future build(String? arg) async { if (arg == null) return null; final repository = ref.read(assetRepositoryProvider); final result = await repository.getAssetById(arg); if (result.failure != null) throw result.failure!; return result.data; } Future submitCreate(Map data) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.createAsset(data); if (result.failure != null) throw result.failure!; ref.invalidate(assetsListProvider); ref.invalidate(myMaintenanceProvider); return result.data; } Future submitUpdate(String id, Map data) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.updateAsset(id, data); if (result.failure != null) throw result.failure!; ref.invalidate(assetsListProvider); ref.invalidate(assetDetailProvider(id)); ref.invalidate(myMaintenanceProvider); final fresh = await repository.getAssetById(id); final asset = fresh.data ?? result.data; if (asset != null) { state = AsyncData(asset); } return asset; } } class AssetAlertsState { const AssetAlertsState({ this.expiryAlerts = const [], this.serviceAlerts = const [], this.expiryDays = 30, this.expiryType, this.serviceStatus, this.expiryPage = 1, this.expiryTotalPages = 1, this.isRefreshing = false, }); final List expiryAlerts; final List serviceAlerts; final int expiryDays; final String? expiryType; final String? serviceStatus; final int expiryPage; final int expiryTotalPages; final bool isRefreshing; AssetAlertsState copyWith({ List? expiryAlerts, List? serviceAlerts, int? expiryDays, String? expiryType, String? serviceStatus, int? expiryPage, int? expiryTotalPages, bool? isRefreshing, bool clearExpiryType = false, bool clearServiceStatus = false, }) { return AssetAlertsState( expiryAlerts: expiryAlerts ?? this.expiryAlerts, serviceAlerts: serviceAlerts ?? this.serviceAlerts, expiryDays: expiryDays ?? this.expiryDays, expiryType: clearExpiryType ? null : expiryType ?? this.expiryType, serviceStatus: clearServiceStatus ? null : serviceStatus ?? this.serviceStatus, expiryPage: expiryPage ?? this.expiryPage, expiryTotalPages: expiryTotalPages ?? this.expiryTotalPages, isRefreshing: isRefreshing ?? this.isRefreshing, ); } } final assetAlertsProvider = AsyncNotifierProvider(AssetAlertsNotifier.new); class AssetAlertsNotifier extends AsyncNotifier { @override Future build() async { return _load(const AssetAlertsState()); } Future _load(AssetAlertsState filters) async { final repository = ref.read(assetRepositoryProvider); final expiryResult = await repository.getExpiryAlerts( days: filters.expiryDays, type: filters.expiryType, page: filters.expiryPage, ); final serviceResult = await repository.getServiceAlerts(status: filters.serviceStatus); if (expiryResult.failure != null) throw expiryResult.failure!; final expiryPage = expiryResult.data!; return AssetAlertsState( expiryAlerts: expiryPage.items, serviceAlerts: serviceResult.data ?? [], expiryDays: filters.expiryDays, expiryType: filters.expiryType, serviceStatus: filters.serviceStatus, expiryPage: expiryPage.page, expiryTotalPages: expiryPage.totalPages, ); } Future refresh() async { final current = state.valueOrNull ?? const AssetAlertsState(); state = AsyncData(current.copyWith(isRefreshing: true)); try { state = AsyncData(await _load(current)); } catch (e, st) { state = AsyncError(e, st); } } Future setExpiryDays(int days) async { final current = state.valueOrNull ?? const AssetAlertsState(); state = const AsyncLoading(); state = AsyncData(await _load(current.copyWith(expiryDays: days, expiryPage: 1))); } Future setExpiryType(String? type) async { final current = state.valueOrNull ?? const AssetAlertsState(); state = const AsyncLoading(); state = AsyncData( await _load( current.copyWith( expiryType: type, expiryPage: 1, clearExpiryType: type == null, ), ), ); } Future setServiceStatus(String? status) async { final current = state.valueOrNull ?? const AssetAlertsState(); state = const AsyncLoading(); state = AsyncData( await _load( current.copyWith( serviceStatus: status, clearServiceStatus: status == null, ), ), ); } Future setExpiryPage(int page) async { final current = state.valueOrNull ?? const AssetAlertsState(); state = const AsyncLoading(); state = AsyncData(await _load(current.copyWith(expiryPage: page))); } } class MyMaintenanceState { const MyMaintenanceState({ this.assets = const [], this.dueOnly = true, this.page = 1, this.limit = 20, this.total = 0, this.totalPages = 1, this.isRefreshing = false, }); final List assets; final bool dueOnly; final int page; final int limit; final int total; final int totalPages; final bool isRefreshing; MyMaintenanceState copyWith({ List? assets, bool? dueOnly, int? page, int? limit, int? total, int? totalPages, bool? isRefreshing, }) { return MyMaintenanceState( assets: assets ?? this.assets, dueOnly: dueOnly ?? this.dueOnly, page: page ?? this.page, limit: limit ?? this.limit, total: total ?? this.total, totalPages: totalPages ?? this.totalPages, isRefreshing: isRefreshing ?? this.isRefreshing, ); } } final myMaintenanceProvider = AsyncNotifierProvider.autoDispose( MyMaintenanceNotifier.new, ); class MyMaintenanceNotifier extends AutoDisposeAsyncNotifier { @override Future build() async { return _load(const MyMaintenanceState()); } Future _load(MyMaintenanceState filters) async { final repository = ref.read(assetRepositoryProvider); final result = await repository.getMyMaintenanceAssets( dueOnly: filters.dueOnly, page: filters.page, limit: filters.limit, ); if (result.failure != null) throw result.failure!; final page = result.data!; return MyMaintenanceState( assets: page.items, dueOnly: filters.dueOnly, page: page.page, limit: filters.limit, total: page.total, totalPages: page.totalPages, ); } Future refresh() async { final current = state.valueOrNull ?? const MyMaintenanceState(); state = AsyncData(current.copyWith(isRefreshing: true)); try { state = AsyncData(await _load(current)); } catch (e, st) { state = AsyncError(e, st); } } Future setDueOnly(bool dueOnly) async { final current = state.valueOrNull ?? const MyMaintenanceState(); state = const AsyncLoading(); state = AsyncData( await _load(current.copyWith(dueOnly: dueOnly, page: 1)), ); } Future setPage(int page) async { final current = state.valueOrNull ?? const MyMaintenanceState(); state = const AsyncLoading(); state = AsyncData(await _load(current.copyWith(page: page))); } Future setPageSize(int limit) async { final current = state.valueOrNull ?? const MyMaintenanceState(); state = const AsyncLoading(); state = AsyncData( await _load(current.copyWith(limit: limit, page: 1)), ); } }