Added sqlite

This commit is contained in:
Courtney Arnold 2026-08-08 16:55:40 -05:00
parent 73a7c93e64
commit f01186df79
14 changed files with 784 additions and 382 deletions

View file

@ -9,6 +9,10 @@ class FuelEntry {
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,
@ -19,6 +23,7 @@ class FuelEntry {
required this.updatedAt,
this.receiptImagePath,
this.receiptDriveFileId,
this.deletedAt,
});
/// True once the receipt photo has been uploaded to Drive and the local
@ -27,13 +32,16 @@ class FuelEntry {
receiptImagePath == null && receiptDriveFileId != null;
/// True while a receipt photo exists only locally and still needs to be
/// uploaded next sync.
/// uploaded next sync. A row in this state is never pushed to Drive as-is
/// (a local file path is meaningless on another device) — sync uploads
/// the image first, which clears this.
bool get hasPendingLocalReceipt => receiptImagePath != null;
FuelEntry copyWith({
String? receiptImagePath,
String? receiptDriveFileId,
DateTime? updatedAt,
DateTime? deletedAt,
bool clearReceiptImagePath = false,
}) {
return FuelEntry(
@ -47,32 +55,35 @@ class FuelEntry {
receiptImagePath:
clearReceiptImagePath ? null : (receiptImagePath ?? this.receiptImagePath),
receiptDriveFileId: receiptDriveFileId ?? this.receiptDriveFileId,
deletedAt: deletedAt ?? this.deletedAt,
);
}
Map<String, dynamic> toJson() => {
Map<String, Object?> toMap() => {
'id': id,
'vehicleId': vehicleId,
'date': date.toIso8601String(),
'vehicle_id': vehicleId,
'date': date.millisecondsSinceEpoch,
'gallons': gallons,
'pricePerGallon': pricePerGallon,
'totalCost': totalCost,
'receiptImagePath': receiptImagePath,
'receiptDriveFileId': receiptDriveFileId,
'updatedAt': updatedAt.toIso8601String(),
'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.fromJson(Map<String, dynamic> json) => FuelEntry(
id: json['id'] as String,
vehicleId: json['vehicleId'] as String,
date: DateTime.parse(json['date'] as String),
gallons: (json['gallons'] as num).toDouble(),
pricePerGallon: (json['pricePerGallon'] as num).toDouble(),
totalCost: (json['totalCost'] as num).toDouble(),
receiptImagePath: json['receiptImagePath'] as String?,
receiptDriveFileId: json['receiptDriveFileId'] as String?,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: DateTime.fromMillisecondsSinceEpoch(0),
factory FuelEntry.fromMap(Map<String, Object?> 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,
);
}

View file

@ -6,6 +6,13 @@ class Vehicle {
final String licensePlate;
final DateTime updatedAt;
/// Soft-delete tombstone: null means active. Deleting sets this instead
/// of removing the row, so the deletion itself can be merged/synced like
/// any other change (newest `updatedAt` wins) instead of silently
/// disappearing and later being resurrected by a device that hasn't seen
/// the deletion yet.
final DateTime? deletedAt;
Vehicle({
required this.id,
required this.make,
@ -13,6 +20,7 @@ class Vehicle {
required this.color,
required this.licensePlate,
required this.updatedAt,
this.deletedAt,
});
String get displayName => '$color $make $model ($licensePlate)';
@ -23,6 +31,7 @@ class Vehicle {
String? color,
String? licensePlate,
DateTime? updatedAt,
DateTime? deletedAt,
}) {
return Vehicle(
id: id,
@ -31,26 +40,29 @@ class Vehicle {
color: color ?? this.color,
licensePlate: licensePlate ?? this.licensePlate,
updatedAt: updatedAt ?? this.updatedAt,
deletedAt: deletedAt ?? this.deletedAt,
);
}
Map<String, dynamic> toJson() => {
Map<String, Object?> toMap() => {
'id': id,
'make': make,
'model': model,
'color': color,
'licensePlate': licensePlate,
'updatedAt': updatedAt.toIso8601String(),
'license_plate': licensePlate,
'updated_at': updatedAt.millisecondsSinceEpoch,
'deleted_at': deletedAt?.millisecondsSinceEpoch,
};
factory Vehicle.fromJson(Map<String, dynamic> json) => Vehicle(
id: json['id'] as String,
make: json['make'] as String,
model: json['model'] as String,
color: json['color'] as String,
licensePlate: json['licensePlate'] as String,
updatedAt: json['updatedAt'] != null
? DateTime.parse(json['updatedAt'] as String)
: DateTime.fromMillisecondsSinceEpoch(0),
factory Vehicle.fromMap(Map<String, Object?> map) => Vehicle(
id: map['id'] as String,
make: map['make'] as String,
model: map['model'] as String,
color: map['color'] as String,
licensePlate: map['license_plate'] 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,
);
}