bharat_erp/lib/modules/grn/presentation/providers/grn_provider.dart
2026-07-22 18:17:43 +05:30

368 lines
11 KiB
Dart

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<GrnModel> 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<GrnModel>? 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, GrnListState>(
GrnListNotifier.new,
);
class GrnListNotifier extends AutoDisposeAsyncNotifier<GrnListState> {
final _columnSearch = ColumnSearchPaging();
@override
Future<GrnListState> build() async {
return _load(const GrnListQuery(limit: 20));
}
Future<GrnListState> _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<GrnModel>.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 = <PurchaseOrderModel>[
...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 = <String>{};
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<void> 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<void> 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<void> 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<ExportFileResult?> 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, GrnModel, String>(
GrnDetailNotifier.new,
);
class GrnDetailNotifier extends FamilyAsyncNotifier<GrnModel, String> {
@override
Future<GrnModel> 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<void> reload() async {
state = const AsyncLoading();
state = AsyncData(await build(arg));
}
Future<GrnModel> 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<List<int>> downloadPdf() async {
final repository = ref.read(grnRepositoryProvider);
final result = await repository.downloadGrnPdf(arg);
if (result.failure != null) throw result.failure!;
return result.data ?? [];
}
Future<GrnAttachmentModel> uploadAttachment({
required List<int> 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<List<int>> 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<void> 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, GrnModel?, String?>(
GrnFormNotifier.new,
);
class GrnFormNotifier extends FamilyAsyncNotifier<GrnModel?, String?> {
@override
Future<GrnModel?> 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<GrnModel> submitCreate(Map<String, dynamic> 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<GrnModel> submitUpdate(String id, Map<String, dynamic> 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!;
}
}