class FuelEntry { final String id; /// References [Vehicle.id] (the hidden, immutable identifier) — not the /// VIN, which is user-editable and so unsuitable as a foreign key. final String vehicleId; final DateTime date; final double gallons; final double pricePerGallon; final double totalCost; final String? receiptImagePath; final String? receiptDriveFileId; final DateTime updatedAt; /// Soft-delete tombstone: null means active. See [Vehicle.deletedAt] for /// why this is a flag rather than an actual row deletion. final DateTime? deletedAt; FuelEntry({ required this.id, required this.vehicleId, required this.date, required this.gallons, required this.pricePerGallon, required this.totalCost, required this.updatedAt, this.receiptImagePath, this.receiptDriveFileId, this.deletedAt, }); /// True once the receipt photo has been uploaded to Drive, regardless of /// whether a local copy is also being kept (see the "keep photos on this /// phone" setting). Viewing it locally is preferred when a local copy /// exists; otherwise it requires downloading from Drive on demand. bool get isReceiptUploadedToDrive => receiptDriveFileId != null; /// True while a receipt photo still needs to be uploaded to Drive: a /// local file exists but hasn't made it there yet. Once /// [receiptDriveFileId] is set this is false — even if a local copy is /// also being kept — since that field alone is what marks "no longer /// needs uploading", independent of local retention. A row in this state /// is never pushed to Drive as data (a local file path is meaningless on /// another device) — sync uploads the image first, which sets /// [receiptDriveFileId] and clears this. bool get needsReceiptUpload => receiptImagePath != null && receiptDriveFileId == null; FuelEntry copyWith({ String? receiptImagePath, String? receiptDriveFileId, DateTime? updatedAt, DateTime? deletedAt, bool clearReceiptImagePath = false, }) { return FuelEntry( id: id, vehicleId: vehicleId, date: date, gallons: gallons, pricePerGallon: pricePerGallon, totalCost: totalCost, updatedAt: updatedAt ?? this.updatedAt, receiptImagePath: clearReceiptImagePath ? null : (receiptImagePath ?? this.receiptImagePath), receiptDriveFileId: receiptDriveFileId ?? this.receiptDriveFileId, deletedAt: deletedAt ?? this.deletedAt, ); } Map toMap() => { 'id': id, 'vehicle_id': vehicleId, 'date': date.millisecondsSinceEpoch, 'gallons': gallons, 'price_per_gallon': pricePerGallon, 'total_cost': totalCost, 'receipt_image_path': receiptImagePath, 'receipt_drive_file_id': receiptDriveFileId, 'updated_at': updatedAt.millisecondsSinceEpoch, 'deleted_at': deletedAt?.millisecondsSinceEpoch, }; factory FuelEntry.fromMap(Map map) => FuelEntry( id: map['id'] as String, vehicleId: map['vehicle_id'] as String, date: DateTime.fromMillisecondsSinceEpoch(map['date'] as int), gallons: (map['gallons'] as num).toDouble(), pricePerGallon: (map['price_per_gallon'] as num).toDouble(), totalCost: (map['total_cost'] as num).toDouble(), receiptImagePath: map['receipt_image_path'] as String?, receiptDriveFileId: map['receipt_drive_file_id'] as String?, updatedAt: DateTime.fromMillisecondsSinceEpoch(map['updated_at'] as int, isUtc: true), deletedAt: map['deleted_at'] != null ? DateTime.fromMillisecondsSinceEpoch(map['deleted_at'] as int, isUtc: true) : null, ); }