import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart' as p; import '../models/stop.dart'; import '../models/delivery_history.dart'; class DatabaseService { static final DatabaseService _instance = DatabaseService._internal(); factory DatabaseService() => _instance; DatabaseService._internal(); Database? _database; Future get database async { if (_database != null && _database!.isOpen) return _database!; _database = await _initDatabase(); return _database!; } Future _initDatabase() async { final dbPath = await getDatabasesPath(); return await openDatabase( p.join(dbPath, 'delivery.db'), version: 3, onCreate: (db, version) async { await _createTables(db); }, onUpgrade: (db, oldVersion, newVersion) async { if (oldVersion < 2) { await db.execute('ALTER TABLE stops ADD COLUMN notes TEXT DEFAULT \'\''); } if (oldVersion < 3) { await _createDeliveryHistoryTable(db); await _createSettingsTable(db); } }, ); } Future _createTables(Database db) async { await db.execute(''' CREATE TABLE routes ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, created_at TEXT ) '''); await db.execute(''' CREATE TABLE stops ( id INTEGER PRIMARY KEY AUTOINCREMENT, route_id INTEGER, street TEXT, house_number TEXT, newspapers TEXT, lat REAL, lng REAL, delivered INTEGER DEFAULT 0, sequence INTEGER DEFAULT 0, notes TEXT DEFAULT '', FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE ) '''); await _createDeliveryHistoryTable(db); await _createSettingsTable(db); } Future _createDeliveryHistoryTable(Database db) async { await db.execute(''' CREATE TABLE IF NOT EXISTS delivery_history ( id INTEGER PRIMARY KEY AUTOINCREMENT, route_id INTEGER, stop_id INTEGER, date TEXT, delivered_at TEXT, FOREIGN KEY (route_id) REFERENCES routes(id) ON DELETE CASCADE, FOREIGN KEY (stop_id) REFERENCES stops(id) ON DELETE CASCADE ) '''); await db.execute( 'CREATE INDEX IF NOT EXISTS idx_delivery_history_date ON delivery_history(date)', ); await db.execute( 'CREATE INDEX IF NOT EXISTS idx_delivery_history_route ON delivery_history(route_id)', ); } Future _createSettingsTable(Database db) async { await db.execute(''' CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT ) '''); } // ── Settings ──────────────────────────────────────────── Future getSetting(String key, T defaultValue) async { final db = await database; final rows = await db.query('settings', where: 'key = ?', whereArgs: [key]); if (rows.isEmpty) return defaultValue; final raw = rows.first['value'] as String; if (T == bool) return (raw == 'true') as T; if (T == int) return int.parse(raw) as T; if (T == double) return double.parse(raw) as T; return raw as T; } Future setSetting(String key, dynamic value) async { final db = await database; await db.insert( 'settings', {'key': key, 'value': value.toString()}, conflictAlgorithm: ConflictAlgorithm.replace, ); } // ── Routes ────────────────────────────────────────────── Future>> getRoutes() async { final db = await database; return db.query('routes', orderBy: 'created_at DESC'); } Future insertRoute(String name) async { final db = await database; return db.insert('routes', { 'name': name, 'created_at': DateTime.now().toIso8601String(), }); } Future deleteRoute(int routeId) async { final db = await database; await db.delete('delivery_history', where: 'route_id = ?', whereArgs: [routeId]); await db.delete('stops', where: 'route_id = ?', whereArgs: [routeId]); await db.delete('routes', where: 'id = ?', whereArgs: [routeId]); } // ── Stops ─────────────────────────────────────────────── Future> getStops(int routeId) async { final db = await database; final maps = await db.query( 'stops', where: 'route_id = ?', whereArgs: [routeId], orderBy: 'sequence', ); return maps.map(Stop.fromMap).toList(); } Future insertStop(Stop stop, int routeId) async { final db = await database; final map = stop.toMap(); map['route_id'] = routeId; return db.insert('stops', map); } Future updateStop(Stop stop) async { final db = await database; await db.update('stops', stop.toMap(), where: 'id = ?', whereArgs: [stop.id]); } Future deleteStop(int stopId) async { final db = await database; await db.delete('delivery_history', where: 'stop_id = ?', whereArgs: [stopId]); await db.delete('stops', where: 'id = ?', whereArgs: [stopId]); } Future updateStopSequence(int stopId, int sequence) async { final db = await database; await db.update('stops', {'sequence': sequence}, where: 'id = ?', whereArgs: [stopId]); } Future updateStopDelivered(int stopId, bool delivered) async { final db = await database; await db.update('stops', {'delivered': delivered ? 1 : 0}, where: 'id = ?', whereArgs: [stopId]); } Future updateStopNotes(int stopId, String notes) async { final db = await database; await db.update('stops', {'notes': notes}, where: 'id = ?', whereArgs: [stopId]); } Future updateStopCoords(int stopId, double lat, double lng) async { final db = await database; await db.update('stops', {'lat': lat, 'lng': lng}, where: 'id = ?', whereArgs: [stopId]); } Future getMaxSequence(int routeId) async { final db = await database; final result = await db.rawQuery( 'SELECT MAX(sequence) as m FROM stops WHERE route_id = ?', [routeId], ); return result.first['m'] as int?; } Future reorderStops(List stops) async { final db = await database; final batch = db.batch(); for (var i = 0; i < stops.length; i++) { batch.update('stops', {'sequence': i}, where: 'id = ?', whereArgs: [stops[i].id]); } await batch.commit(noResult: true); } // ── Delivery History ──────────────────────────────────── String _today() { final now = DateTime.now(); return '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}'; } Future recordDelivery(int routeId, int stopId) async { final db = await database; // Check if already recorded today final existing = await db.query( 'delivery_history', where: 'route_id = ? AND stop_id = ? AND date = ?', whereArgs: [routeId, stopId, _today()], ); if (existing.isEmpty) { await db.insert('delivery_history', { 'route_id': routeId, 'stop_id': stopId, 'date': _today(), 'delivered_at': DateTime.now().toIso8601String(), }); } } Future removeDeliveryRecord(int routeId, int stopId) async { final db = await database; await db.delete( 'delivery_history', where: 'route_id = ? AND stop_id = ? AND date = ?', whereArgs: [routeId, stopId, _today()], ); } /// Get the set of stop IDs delivered today for a route. Future> getTodayDeliveredStopIds(int routeId) async { final db = await database; final rows = await db.query( 'delivery_history', columns: ['stop_id'], where: 'route_id = ? AND date = ?', whereArgs: [routeId, _today()], ); return rows.map((r) => r['stop_id'] as int).toSet(); } /// Reset today's deliveries for a route (mark all stops undelivered). Future resetToday(int routeId) async { final db = await database; await db.delete( 'delivery_history', where: 'route_id = ? AND date = ?', whereArgs: [routeId, _today()], ); await db.update('stops', {'delivered': 0}, where: 'route_id = ?', whereArgs: [routeId]); } /// Get all unique delivery dates for a route. Future> getDeliveryDates(int routeId) async { final db = await database; final rows = await db.rawQuery( 'SELECT DISTINCT date FROM delivery_history WHERE route_id = ? ORDER BY date DESC', [routeId], ); return rows.map((r) => r['date'] as String).toList(); } /// Get delivery count for a specific date and route. Future getDeliveryCountForDate(int routeId, String date) async { final db = await database; final rows = await db.rawQuery( 'SELECT COUNT(*) as cnt FROM delivery_history WHERE route_id = ? AND date = ?', [routeId, date], ); return rows.first['cnt'] as int; } /// Get delivery stats for a date range. Future> getDeliveryStats(int routeId, {int days = 30}) async { final db = await database; final now = DateTime.now(); final startDate = now.subtract(Duration(days: days)); final startStr = '${startDate.year}-${startDate.month.toString().padLeft(2, '0')}-${startDate.day.toString().padLeft(2, '0')}'; // Get total stops for this route final totalRows = await db.rawQuery( 'SELECT COUNT(*) as cnt FROM stops WHERE route_id = ?', [routeId], ); final totalStops = totalRows.first['cnt'] as int; final rows = await db.rawQuery( '''SELECT date, COUNT(*) as cnt FROM delivery_history WHERE route_id = ? AND date >= ? GROUP BY date ORDER BY date DESC''', [routeId, startStr], ); return rows .map((r) => DailyStats( date: r['date'] as String, deliveredCount: r['cnt'] as int, totalCount: totalStops, )) .toList(); } /// Get total deliveries ever for a route. Future getTotalDeliveries(int routeId) async { final db = await database; final rows = await db.rawQuery( 'SELECT COUNT(*) as cnt FROM delivery_history WHERE route_id = ?', [routeId], ); return rows.first['cnt'] as int; } /// Get delivery count for current week (Mon-Sun). Future getWeeklyDeliveryCount(int routeId) async { final db = await database; final now = DateTime.now(); // Find Monday of this week final monday = now.subtract(Duration(days: now.weekday - 1)); final mondayStr = '${monday.year}-${monday.month.toString().padLeft(2, '0')}-${monday.day.toString().padLeft(2, '0')}'; final rows = await db.rawQuery( 'SELECT COUNT(*) as cnt FROM delivery_history WHERE route_id = ? AND date >= ?', [routeId, mondayStr], ); return rows.first['cnt'] as int; } Future close() async { final db = _database; if (db != null && db.isOpen) { await db.close(); _database = null; } } }