import 'package:freezed_annotation/freezed_annotation.dart'; part 'grn_model.freezed.dart'; part 'grn_model.g.dart'; String _idFromJson(Object? value) => value?.toString() ?? ''; int? _intFromJsonNullable(Object? value) { if (value == null) return null; if (value is int) return value; if (value is num) return value.toInt(); return int.tryParse(value.toString()); } double? _doubleFromJsonNullable(Object? value) { if (value == null) return null; if (value is double) return value; if (value is num) return value.toDouble(); return double.tryParse(value.toString()); } DateTime? _dateFromJsonNullable(Object? value) { if (value == null) return null; if (value is DateTime) return value; return DateTime.tryParse(value.toString()); } Object? _readNestedName(Map json, String flatKey, String nestedKey) { final flat = json[flatKey]; if (flat is String && flat.isNotEmpty) return flat; final nested = json[nestedKey]; if (nested is Map) { return nested['name'] ?? nested['vendor_name']; } return null; } Object? _readVendorName(Map json, String key) => _readNestedName(json, 'vendor_name', 'vendor'); Object? _readWarehouseName(Map json, String key) => _readNestedName(json, 'warehouse_name', 'warehouse'); Object? _readPoNumber(Map json, String key) { final grnNumber = json['grn_number']; if (grnNumber != null && grnNumber.toString().trim().isNotEmpty) { return grnNumber.toString(); } final grnNo = json['grn_no']; if (grnNo != null && grnNo.toString().trim().isNotEmpty) { return grnNo.toString(); } return null; } Object? _readPoRefNumber(Map json, String key) { final flat = json['po_number']; if (flat is String && flat.isNotEmpty) return flat; final nested = json['purchase_order'] ?? json['po']; if (nested is Map) { return nested['po_number'] ?? nested['po_no']; } return null; } Object? _readItemName(Map json, String key) { final flat = json['item_name']; if (flat is String && flat.isNotEmpty) return flat; final nested = json['item']; if (nested is Map) return nested['name'] ?? nested['item_name']; return null; } Object? _readItemCode(Map json, String key) { final flat = json['item_code']; if (flat is String && flat.isNotEmpty) return flat; final nested = json['item']; if (nested is Map) return nested['code'] ?? nested['item_code']; return null; } Object? _readUomName(Map json, String key) { final flat = json['uom_name']; if (flat is String && flat.isNotEmpty) return flat; final nested = json['uom']; if (nested is Map) return nested['name']; return null; } Object? _readGrnItemCategoryId(Map json, String key) { for (final flatKey in ['item_category_id', 'asset_category_id']) { final flat = json[flatKey]; if (flat != null) return flat; } final nested = json['item_category'] ?? json['asset_category']; if (nested is Map) return nested['id']; return null; } Object? _readGrnItemSubcategoryId(Map json, String key) { for (final flatKey in ['item_subcategory_id', 'asset_subcategory_id']) { final flat = json[flatKey]; if (flat != null) return flat; } final nested = json['item_subcategory'] ?? json['asset_subcategory']; if (nested is Map) return nested['id']; return null; } Object? _readUploadedByName(Map json, String key) { final flat = json['uploaded_by_name']; if (flat is String && flat.trim().isNotEmpty) return flat; final nested = json['uploaded_by_user'] ?? json['uploaded_by']; if (nested is Map) { return nested['full_name'] ?? nested['name']; } return null; } @freezed class GrnModel with _$GrnModel { const GrnModel._(); const factory GrnModel({ @JsonKey(fromJson: _idFromJson) required String id, @JsonKey(name: 'grn_number', readValue: _readPoNumber) String? grnNumber, @JsonKey(name: 'grn_date', fromJson: _dateFromJsonNullable) DateTime? grnDate, @Default('POSTED') String status, @JsonKey(name: 'po_id', fromJson: _intFromJsonNullable) int? poId, @JsonKey(name: 'po_number', readValue: _readPoRefNumber) String? poNumber, @JsonKey(name: 'vendor_id', fromJson: _intFromJsonNullable) int? vendorId, @JsonKey(name: 'vendor_name', readValue: _readVendorName) String? vendorName, @JsonKey(name: 'warehouse_id', fromJson: _intFromJsonNullable) int? warehouseId, @JsonKey(name: 'warehouse_name', readValue: _readWarehouseName) String? warehouseName, @JsonKey(name: 'vendor_invoice_no') String? vendorInvoiceNo, @JsonKey(name: 'vendor_invoice_date', fromJson: _dateFromJsonNullable) DateTime? vendorInvoiceDate, @JsonKey(name: 'vendor_invoice_amount', fromJson: _doubleFromJsonNullable) double? vendorInvoiceAmount, @JsonKey(name: 'vehicle_no') String? vehicleNo, @JsonKey(name: 'lr_no') String? lrNo, @JsonKey(name: 'lr_date', fromJson: _dateFromJsonNullable) DateTime? lrDate, @JsonKey(name: 'received_by', fromJson: _intFromJsonNullable) int? receivedBy, @JsonKey(name: 'quality_checked_by', fromJson: _intFromJsonNullable) int? qualityCheckedBy, String? remarks, @JsonKey(name: 'cancellation_reason') String? cancellationReason, @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt, @JsonKey(name: 'updated_at', fromJson: _dateFromJsonNullable) DateTime? updatedAt, @Default([]) List items, @Default([]) List attachments, }) = _GrnModel; factory GrnModel.fromJson(Map json) => _$GrnModelFromJson(json); bool get canEdit => status.toUpperCase() == 'POSTED'; bool get canCancel => status.toUpperCase() == 'POSTED'; /// Upload/delete attachments only while GRN is POSTED. bool get canManageAttachments => status.toUpperCase() == 'POSTED'; } @freezed class GrnAttachmentModel with _$GrnAttachmentModel { const GrnAttachmentModel._(); const factory GrnAttachmentModel({ @JsonKey(fromJson: _idFromJson) required String id, @JsonKey(name: 'grn_id', fromJson: _idFromJson) String? grnId, @JsonKey(name: 'file_name') String? fileName, @JsonKey(name: 'file_type') String? fileType, @JsonKey(name: 'file_size', fromJson: _intFromJsonNullable) int? fileSize, @JsonKey(name: 'uploaded_by_name', readValue: _readUploadedByName) String? uploadedByName, @JsonKey(name: 'created_at', fromJson: _dateFromJsonNullable) DateTime? createdAt, }) = _GrnAttachmentModel; factory GrnAttachmentModel.fromJson(Map json) => _$GrnAttachmentModelFromJson(json); bool get isPdf => (fileType ?? '').toLowerCase().contains('pdf') || (fileName ?? '').toLowerCase().endsWith('.pdf'); bool get isImage { final type = (fileType ?? '').toLowerCase(); final name = (fileName ?? '').toLowerCase(); return type.startsWith('image/') || name.endsWith('.jpg') || name.endsWith('.jpeg') || name.endsWith('.png') || name.endsWith('.webp'); } } @freezed class GrnItemModel with _$GrnItemModel { const factory GrnItemModel({ @JsonKey(fromJson: _idFromJson) required String id, @JsonKey(name: 'grn_id', fromJson: _idFromJson) String? grnId, @JsonKey(name: 'po_item_id', fromJson: _intFromJsonNullable) int? poItemId, @JsonKey(name: 'item_id', fromJson: _intFromJsonNullable) int? itemId, @JsonKey(name: 'item_code', readValue: _readItemCode) String? itemCode, @JsonKey(name: 'item_name', readValue: _readItemName) String? itemName, @JsonKey(name: 'line_no', fromJson: _intFromJsonNullable) int? lineNo, @JsonKey(name: 'current_qty', fromJson: _doubleFromJsonNullable) double? currentQty, @JsonKey(name: 'accepted_qty', fromJson: _doubleFromJsonNullable) double? acceptedQty, @JsonKey(name: 'damaged_qty', fromJson: _doubleFromJsonNullable) double? damagedQty, @JsonKey(name: 'short_qty', fromJson: _doubleFromJsonNullable) double? shortQty, @JsonKey(name: 'excess_qty', fromJson: _doubleFromJsonNullable) double? excessQty, @JsonKey(name: 'rejected_qty', fromJson: _doubleFromJsonNullable) double? rejectedQty, @JsonKey(name: 'rejection_reason') String? rejectionReason, @JsonKey(fromJson: _doubleFromJsonNullable) double? rate, @JsonKey(name: 'batch_no') String? batchNo, @JsonKey(name: 'mfg_date', fromJson: _dateFromJsonNullable) DateTime? mfgDate, @JsonKey(name: 'expiry_date', fromJson: _dateFromJsonNullable) DateTime? expiryDate, @JsonKey(name: 'storage_location') String? storageLocation, @JsonKey( name: 'item_category_id', readValue: _readGrnItemCategoryId, fromJson: _intFromJsonNullable, ) int? itemCategoryId, @JsonKey( name: 'item_subcategory_id', readValue: _readGrnItemSubcategoryId, fromJson: _intFromJsonNullable, ) int? itemSubcategoryId, @JsonKey(name: 'uom_id', fromJson: _intFromJsonNullable) int? uomId, @JsonKey(name: 'uom_name', readValue: _readUomName) String? uomName, String? remarks, }) = _GrnItemModel; factory GrnItemModel.fromJson(Map json) => _$GrnItemModelFromJson(json); } @freezed class GrnListQuery with _$GrnListQuery { const factory GrnListQuery({ @Default(1) int page, @Default(20) int limit, String? search, String? status, int? poId, int? vendorId, int? warehouseId, String? dateFrom, String? dateTo, }) = _GrnListQuery; } const grnStatusOptions = [ ('POSTED', 'Posted'), ('CANCELLED', 'Cancelled'), ]; String grnStatusLabel(String? value) { if (value == null) return '—'; return grnStatusOptions .where((e) => e.$1 == value.toUpperCase()) .map((e) => e.$2) .firstOrNull ?? value.replaceAll('_', ' '); }