Files
jamshalat-diary/lib/data/services/notification_event_producer_service.dart
2026-03-18 00:07:10 +07:00

300 lines
8.9 KiB
Dart

import 'package:hive_flutter/hive_flutter.dart';
import 'package:intl/intl.dart';
import '../local/hive_boxes.dart';
import '../local/models/app_settings.dart';
import '../local/models/daily_worship_log.dart';
import 'notification_inbox_service.dart';
import 'notification_runtime_service.dart';
import 'notification_service.dart';
/// Creates in-app inbox events from runtime/system conditions.
class NotificationEventProducerService {
NotificationEventProducerService._();
static final NotificationEventProducerService instance =
NotificationEventProducerService._();
final NotificationInboxService _inbox = NotificationInboxService.instance;
final NotificationRuntimeService _runtime =
NotificationRuntimeService.instance;
Future<void> emitPermissionWarningsIfNeeded({
required AppSettings settings,
required NotificationPermissionStatus permissionStatus,
}) async {
if (!settings.adhanEnabled.values.any((v) => v)) return;
final dateKey = _todayKey();
if (!permissionStatus.notificationsAllowed) {
final title = 'Izin notifikasi dinonaktifkan';
final body =
'Aktifkan izin notifikasi agar pengingat adzan dan iqamah dapat muncul.';
if (settings.inboxEnabled) {
await _inbox.addItem(
title: title,
body: body,
type: 'system',
source: 'local',
deeplink: '/settings',
dedupeKey: 'system.permission.notifications.$dateKey',
expiresAt: DateTime.now().add(const Duration(days: 2)),
);
}
await _pushSystemIfAllowed(
settings: settings,
dedupeSeed: 'push.system.permission.notifications.$dateKey',
title: title,
body: body,
);
}
if (!permissionStatus.exactAlarmAllowed) {
final title = 'Izin alarm presisi belum aktif';
final body =
'Aktifkan alarm presisi agar pengingat adzan tepat waktu di perangkat Android.';
if (settings.inboxEnabled) {
await _inbox.addItem(
title: title,
body: body,
type: 'system',
source: 'local',
deeplink: '/settings',
dedupeKey: 'system.permission.exact_alarm.$dateKey',
expiresAt: DateTime.now().add(const Duration(days: 2)),
);
}
await _pushSystemIfAllowed(
settings: settings,
dedupeSeed: 'push.system.permission.exact_alarm.$dateKey',
title: title,
body: body,
);
}
}
Future<void> emitScheduleFallback({
required AppSettings settings,
required String cityId,
required bool locationUnavailable,
}) async {
final dateKey = _todayKey();
final title = locationUnavailable
? 'Lokasi belum tersedia'
: 'Jadwal online terganggu';
final body = locationUnavailable
? 'Lokasi perangkat belum aktif. Aplikasi menggunakan lokasi default sementara.'
: 'Aplikasi memakai perhitungan lokal sementara. Pastikan internet aktif untuk jadwal paling akurat.';
final scope = locationUnavailable ? 'loc' : 'net';
final dedupe = 'system.schedule.fallback.$cityId.$dateKey.$scope';
if (settings.inboxEnabled) {
await _inbox.addItem(
title: title,
body: body,
type: 'system',
source: 'local',
deeplink: '/imsakiyah',
dedupeKey: dedupe,
expiresAt: DateTime.now().add(const Duration(days: 1)),
meta: <String, dynamic>{
'cityId': cityId,
'date': dateKey,
'scope': scope,
},
);
}
await _pushSystemIfAllowed(
settings: settings,
dedupeSeed: 'push.$dedupe',
title: title,
body: body,
);
}
Future<void> emitNotificationSyncFailed({
required AppSettings settings,
required String cityId,
}) async {
final dateKey = _todayKey();
final title = 'Sinkronisasi alarm adzan gagal';
final body =
'Pengingat adzan belum tersinkron. Coba buka aplikasi lagi atau periksa pengaturan notifikasi.';
final dedupe = 'system.notification.sync_failed.$cityId.$dateKey';
if (settings.inboxEnabled) {
await _inbox.addItem(
title: title,
body: body,
type: 'system',
source: 'local',
deeplink: '/settings',
dedupeKey: dedupe,
expiresAt: DateTime.now().add(const Duration(days: 1)),
meta: <String, dynamic>{
'cityId': cityId,
'date': dateKey,
},
);
}
await _pushSystemIfAllowed(
settings: settings,
dedupeSeed: 'push.$dedupe',
title: title,
body: body,
);
}
Future<void> emitStreakRiskIfNeeded({
required AppSettings settings,
}) async {
if (!settings.inboxEnabled || !settings.streakRiskEnabled) return;
final now = DateTime.now();
if (now.hour < 18) return;
final dateKey = _todayKey();
final worshipBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
final log = worshipBox.get(dateKey);
if (log == null) return;
final tilawahRisk = log.tilawahLog != null && !log.tilawahLog!.isCompleted;
final dzikirRisk =
settings.trackDzikir && log.dzikirLog != null && !log.dzikirLog!.petang;
if (tilawahRisk) {
final title = 'Streak Tilawah berisiko terputus';
const body =
'Selesaikan target tilawah hari ini untuk menjaga konsistensi.';
final dedupe = 'streak.tilawah.$dateKey';
await _inbox.addItem(
title: title,
body: body,
type: 'streak_risk',
source: 'local',
deeplink: '/quran',
dedupeKey: dedupe,
expiresAt: DateTime(now.year, now.month, now.day, 23, 59),
);
await _pushHabitIfAllowed(
settings: settings,
dedupeSeed: 'push.$dedupe',
title: title,
body: body,
);
}
if (dzikirRisk) {
final title = 'Dzikir petang belum tercatat';
const body = 'Lengkapi dzikir petang untuk menjaga streak amalan harian.';
final dedupe = 'streak.dzikir.$dateKey';
await _inbox.addItem(
title: title,
body: body,
type: 'streak_risk',
source: 'local',
deeplink: '/tools/dzikir',
dedupeKey: dedupe,
expiresAt: DateTime(now.year, now.month, now.day, 23, 59),
);
await _pushHabitIfAllowed(
settings: settings,
dedupeSeed: 'push.$dedupe',
title: title,
body: body,
);
}
}
Future<void> emitWeeklySummaryIfNeeded({
required AppSettings settings,
}) async {
if (!settings.inboxEnabled || !settings.weeklySummaryEnabled) return;
final now = DateTime.now();
if (now.weekday != DateTime.monday || now.hour < 6) return;
final monday = now.subtract(Duration(days: now.weekday - 1));
final weekKey = DateFormat('yyyy-MM-dd').format(monday);
if (_runtime.lastWeeklySummaryWeekKey() == weekKey) return;
final worshipBox = Hive.box<DailyWorshipLog>(HiveBoxes.worshipLogs);
var completionDays = 0;
var totalPoints = 0;
for (int i = 1; i <= 7; i++) {
final date = now.subtract(Duration(days: i));
final key = DateFormat('yyyy-MM-dd').format(date);
final log = worshipBox.get(key);
if (log == null) continue;
if (log.completionPercent >= 70) completionDays++;
totalPoints += log.totalPoints;
}
await _inbox.addItem(
title: 'Ringkasan Ibadah Mingguan',
body:
'7 hari terakhir: $completionDays hari konsisten, total $totalPoints poin. Lihat detail laporan.',
type: 'summary',
source: 'local',
deeplink: '/laporan',
dedupeKey: 'summary.weekly.$weekKey',
expiresAt: now.add(const Duration(days: 7)),
);
await _runtime.setLastWeeklySummaryWeekKey(weekKey);
}
String _todayKey() => DateFormat('yyyy-MM-dd').format(DateTime.now());
Future<void> _pushSystemIfAllowed({
required AppSettings settings,
required String dedupeSeed,
required String title,
required String body,
}) async {
await _pushNonPrayer(
settings: settings,
dedupeSeed: dedupeSeed,
title: title,
body: body,
payloadType: 'system',
silent: true,
);
}
Future<void> _pushHabitIfAllowed({
required AppSettings settings,
required String dedupeSeed,
required String title,
required String body,
}) async {
await _pushNonPrayer(
settings: settings,
dedupeSeed: dedupeSeed,
title: title,
body: body,
payloadType: 'streak_risk',
silent: false,
);
}
Future<void> _pushNonPrayer({
required AppSettings settings,
required String dedupeSeed,
required String title,
required String body,
required String payloadType,
required bool silent,
}) async {
if (!settings.alertsEnabled) return;
final notif = NotificationService.instance;
await notif.showNonPrayerAlert(
settings: settings,
id: notif.nonPrayerNotificationId(dedupeSeed),
title: title,
body: body,
payloadType: payloadType,
silent: silent,
);
}
}