368 lines
11 KiB
Dart
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!;
|
|
}
|
|
}
|