import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../core/utils/column_search_paging.dart'; import '../../../../core/utils/table_search.dart'; import '../../../../shared/models/export_file_result.dart'; import '../../../../shared/models/grn_model.dart'; import '../../../../shared/models/purchase_order_model.dart'; import '../../../purchase_orders/data/repositories/purchase_order_repository_impl.dart'; import '../../../purchase_orders/presentation/providers/purchase_order_lookups_provider.dart'; import '../../data/repositories/grn_repository_impl.dart'; import 'grn_lookups_provider.dart'; class GrnListState { const GrnListState({ this.grns = const [], this.query = const GrnListQuery(limit: 20), this.total = 0, this.totalPages = 1, this.isRefreshing = false, this.isExporting = false, this.actionError, this.actionSuccess, }); final List grns; final GrnListQuery query; final int total; final int totalPages; final bool isRefreshing; final bool isExporting; final String? actionError; final String? actionSuccess; GrnListState copyWith({ List? grns, GrnListQuery? query, int? total, int? totalPages, bool? isRefreshing, bool? isExporting, String? actionError, String? actionSuccess, bool clearMessages = false, }) { return GrnListState( grns: grns ?? this.grns, 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 grnListProvider = AsyncNotifierProvider.autoDispose( GrnListNotifier.new, ); class GrnListNotifier extends AutoDisposeAsyncNotifier { final _columnSearch = ColumnSearchPaging(); @override Future build() async { return _load(const GrnListQuery(limit: 20)); } Future _load(GrnListQuery query) async { final repository = ref.read(grnRepositoryProvider); final result = await repository.getGrns(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 PO number / vendor name — enrich via filters. if (search.isNotEmpty && query.poId == null && query.vendorId == null) { try { final grnLookups = await ref.read(grnLookupsProvider.future); final poCandidates = [ ...grnLookups.receivablePurchaseOrders, ]; // Include broader PO dropdown options so search works beyond receivable. final allPoOptions = await ref .read(purchaseOrderRepositoryProvider) .listPurchaseOrderOptions(); if (allPoOptions.failure == null && allPoOptions.data != null) { poCandidates.addAll(allPoOptions.data!); } final seenPoIds = {}; var poMatches = 0; for (final po in poCandidates) { if (!seenPoIds.add(po.id)) continue; if (!TableSearch.matches(search, [po.poNo, po.vendorName])) { continue; } if (++poMatches > 5) break; final poId = int.tryParse(po.id); if (poId == null) continue; final byPo = await repository.getGrns( query.copyWith(search: null, poId: poId), ); if (byPo.failure == null && byPo.data != null) { items = TableSearch.mergeById( items, byPo.data!.items, (grn) => grn.id, ); } } final poLookups = await ref.read(purchaseOrderLookupsProvider.future); var vendorMatches = 0; for (final vendor in poLookups.vendors) { if (!TableSearch.matches(search, [vendor.name])) continue; if (++vendorMatches > 5) break; final vendorId = int.tryParse(vendor.id); if (vendorId == null) continue; final byVendor = await repository.getGrns( query.copyWith(search: null, vendorId: vendorId), ); if (byVendor.failure == null && byVendor.data != null) { items = TableSearch.mergeById( items, byVendor.data!.items, (grn) => grn.id, ); } } } catch (_) { // Lookups enrichment is best-effort. } } return GrnListState( grns: items, query: query, total: page.total, totalPages: page.totalPages, ); } Future refresh() async { final current = state.valueOrNull ?? const GrnListState(); state = AsyncData(current.copyWith(isRefreshing: true, clearMessages: true)); try { state = AsyncData(await _load(current.query)); } catch (e, st) { state = AsyncError(e, st); } } Future applyQuery(GrnListQuery 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 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 exportGrns() async { final current = state.valueOrNull; if (current == null) return null; state = AsyncData(current.copyWith(isExporting: true, clearMessages: true)); final result = await ref.read(grnRepositoryProvider).exportGrns(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; } } final grnDetailProvider = AsyncNotifierProvider.family( GrnDetailNotifier.new, ); class GrnDetailNotifier extends FamilyAsyncNotifier { @override Future build(String arg) async { final repository = ref.read(grnRepositoryProvider); final result = await repository.getGrnById(arg); if (result.failure != null) throw result.failure!; return result.data!; } Future reload() async { state = const AsyncLoading(); state = AsyncData(await build(arg)); } Future cancel({required String cancellationReason}) async { final repository = ref.read(grnRepositoryProvider); final result = await repository.cancelGrn( arg, cancellationReason: cancellationReason, ); if (result.failure != null) throw result.failure!; state = AsyncData(result.data!); ref.invalidate(grnListProvider); return result.data!; } Future> downloadPdf() async { final repository = ref.read(grnRepositoryProvider); final result = await repository.downloadGrnPdf(arg); if (result.failure != null) throw result.failure!; return result.data ?? []; } Future uploadAttachment({ required List bytes, required String filename, }) async { final repository = ref.read(grnRepositoryProvider); final result = await repository.uploadAttachment( arg, bytes: bytes, filename: filename, ); if (result.failure != null) throw result.failure!; final current = state.valueOrNull; if (current != null) { state = AsyncData( current.copyWith( attachments: [...current.attachments, result.data!], ), ); } else { await reload(); } return result.data!; } Future> downloadAttachment(String attachmentId) async { final repository = ref.read(grnRepositoryProvider); 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(grnRepositoryProvider); final result = await repository.deleteAttachment(arg, attachmentId); if (result.failure != null) throw result.failure!; final current = state.valueOrNull; if (current != null) { state = AsyncData( current.copyWith( attachments: current.attachments .where((a) => a.id != attachmentId) .toList(), ), ); } else { await reload(); } } } final grnFormProvider = AsyncNotifierProvider.family( GrnFormNotifier.new, ); class GrnFormNotifier extends FamilyAsyncNotifier { @override Future build(String? arg) async { if (arg == null) return null; final repository = ref.read(grnRepositoryProvider); final result = await repository.getGrnById(arg); if (result.failure != null) throw result.failure!; return result.data; } Future submitCreate(Map data) async { final repository = ref.read(grnRepositoryProvider); final result = await repository.createGrn(data); if (result.failure != null) throw result.failure!; ref.invalidate(grnListProvider); return result.data!; } Future submitUpdate(String id, Map data) async { final repository = ref.read(grnRepositoryProvider); final result = await repository.updateGrn(id, data); if (result.failure != null) throw result.failure!; ref.invalidate(grnListProvider); ref.invalidate(grnDetailProvider(id)); return result.data!; } }