Added sqlite
This commit is contained in:
parent
73a7c93e64
commit
f01186df79
14 changed files with 784 additions and 382 deletions
146
lib/services/database_service.dart
Normal file
146
lib/services/database_service.dart
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
|
||||
import '../models/fuel_entry.dart';
|
||||
import '../models/vehicle.dart';
|
||||
import 'db_schema.dart';
|
||||
|
||||
const dbFileName = 'fuel_tax_tracker.db';
|
||||
const receiptsFolderName = 'receipts';
|
||||
|
||||
/// Owns the local SQLite database (vehicles + fuel_entries) and the
|
||||
/// `receipts/` folder of not-yet-uploaded receipt photos, both under
|
||||
/// `ApplicationDocumentsDirectory/FuelTaxTracker`.
|
||||
///
|
||||
/// Every row carries `updated_at` (for newest-wins merging), `deleted_at`
|
||||
/// (a soft-delete tombstone — see [Vehicle.deletedAt]/[FuelEntry.deletedAt]
|
||||
/// for why deletes aren't real `DELETE`s), and `dirty` (local-only: "not
|
||||
/// yet pushed to Drive"). `dirty` is intentionally not exposed on the
|
||||
/// domain model classes — it's sync bookkeeping the UI layer never needs to
|
||||
/// know about; only [DriveSyncService] reads/clears it.
|
||||
class DatabaseService {
|
||||
late Directory _rootDirectory;
|
||||
late Database _db;
|
||||
|
||||
Directory get rootDirectory => _rootDirectory;
|
||||
|
||||
Directory get receiptsDirectory =>
|
||||
Directory(p.join(_rootDirectory.path, receiptsFolderName));
|
||||
|
||||
/// Raw handle for [DriveSyncService], which needs to run ATTACH-based
|
||||
/// merge SQL and VACUUM INTO snapshots that go beyond simple CRUD.
|
||||
Database get rawDb => _db;
|
||||
|
||||
String get databasePath => _db.path;
|
||||
|
||||
Future<void> init() async {
|
||||
final docsDir = await getApplicationDocumentsDirectory();
|
||||
_rootDirectory = Directory(p.join(docsDir.path, 'FuelTaxTracker'));
|
||||
await _rootDirectory.create(recursive: true);
|
||||
await receiptsDirectory.create(recursive: true);
|
||||
|
||||
final dbPath = p.join(_rootDirectory.path, dbFileName);
|
||||
_db = await openDatabase(dbPath, version: 1, onCreate: _onCreate);
|
||||
}
|
||||
|
||||
Future<void> _onCreate(Database db, int version) async {
|
||||
await db.execute(createVehiclesTableSql);
|
||||
await db.execute(createFuelEntriesTableSql);
|
||||
await db.execute(createFuelEntriesIndexSql);
|
||||
}
|
||||
|
||||
Future<List<Vehicle>> getVehicles() async {
|
||||
final rows = await _db.query('vehicles', where: 'deleted_at IS NULL');
|
||||
return rows.map(Vehicle.fromMap).toList();
|
||||
}
|
||||
|
||||
Future<List<FuelEntry>> getFuelEntries() async {
|
||||
final rows = await _db.query('fuel_entries', where: 'deleted_at IS NULL');
|
||||
return rows.map(FuelEntry.fromMap).toList();
|
||||
}
|
||||
|
||||
/// Inserts or fully overwrites a vehicle row and marks it dirty (pending
|
||||
/// push to Drive).
|
||||
Future<void> saveVehicle(Vehicle vehicle) async {
|
||||
final map = Map<String, Object?>.from(vehicle.toMap())..['dirty'] = 1;
|
||||
await _db.insert('vehicles', map, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> saveFuelEntry(FuelEntry entry) async {
|
||||
final map = Map<String, Object?>.from(entry.toMap())..['dirty'] = 1;
|
||||
await _db.insert('fuel_entries', map, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> softDeleteVehicle(String id, DateTime deletedAt) async {
|
||||
await _db.update(
|
||||
'vehicles',
|
||||
{
|
||||
'deleted_at': deletedAt.millisecondsSinceEpoch,
|
||||
'updated_at': deletedAt.millisecondsSinceEpoch,
|
||||
'dirty': 1,
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> softDeleteFuelEntry(String id, DateTime deletedAt) async {
|
||||
await _db.update(
|
||||
'fuel_entries',
|
||||
{
|
||||
'deleted_at': deletedAt.millisecondsSinceEpoch,
|
||||
'updated_at': deletedAt.millisecondsSinceEpoch,
|
||||
'dirty': 1,
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
/// Soft-deletes every active fuel entry for [vehicleId] (cascade for a
|
||||
/// vehicle deletion) and returns their local receipt image paths, if
|
||||
/// any, so the caller can clean those files up too.
|
||||
Future<List<String>> softDeleteFuelEntriesForVehicle(
|
||||
String vehicleId,
|
||||
DateTime deletedAt,
|
||||
) async {
|
||||
final rows = await _db.query(
|
||||
'fuel_entries',
|
||||
where: 'vehicle_id = ? AND deleted_at IS NULL',
|
||||
whereArgs: [vehicleId],
|
||||
);
|
||||
final localPaths =
|
||||
rows.map((r) => r['receipt_image_path'] as String?).whereType<String>().toList();
|
||||
|
||||
await _db.update(
|
||||
'fuel_entries',
|
||||
{
|
||||
'deleted_at': deletedAt.millisecondsSinceEpoch,
|
||||
'updated_at': deletedAt.millisecondsSinceEpoch,
|
||||
'dirty': 1,
|
||||
},
|
||||
where: 'vehicle_id = ? AND deleted_at IS NULL',
|
||||
whereArgs: [vehicleId],
|
||||
);
|
||||
|
||||
return localPaths;
|
||||
}
|
||||
|
||||
Future<String> storeReceiptImage(File sourceImage, String fuelEntryId) async {
|
||||
final ext = p.extension(sourceImage.path);
|
||||
final destPath = p.join(receiptsDirectory.path, '$fuelEntryId$ext');
|
||||
final copied = await sourceImage.copy(destPath);
|
||||
return copied.path;
|
||||
}
|
||||
|
||||
Future<void> deleteReceiptImageFile(String? path) async {
|
||||
if (path == null) return;
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue